Repository: MideTechnology/endaq-python (language: python)

Issue:
Feature/93 ide docs examples
Incorporated the `endaq.ide` README into the docs, and created a page for `endaq.ide.measurement`. Some minor formatting changes.

This PR is specifically focused on `endaq.ide` documentation; while other sections also need expansion, those should probably be their own PRs.

See issue #93


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index d01245c..6af4259 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,11 @@ This package consists of several submodules, you can read more about how these a
 
 The docs for this package can be found [here](https://docs.endaq.com/en/latest/).
 
+To locally build the [Sphinx](https://www.sphinx-doc.org) documentation from a clone of the repo:
+1. `cd <repo root dir>`
+2. `pip install -e .[docs]`
+3. `sphinx-build -W -b html docs docs/_build`
+
 ## License 
 
 The endaq-python repository is licensed under the MIT license. The full text can be found in the [LICENSE file](https://github.com/MideTechnology/endaq-python/blob/main/LICENSE).
diff --git a/docs/api_ref.rst b/docs/api_ref.rst
index 2c54740..b1bb277 100644
--- a/docs/api_ref.rst
+++ b/docs/api_ref.rst
@@ -2,7 +2,7 @@ enDAQ API Reference
 ===================
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 3
    :caption: Contents:
 
    endaq/ide
diff --git a/docs/endaq/batch.rst b/docs/endaq/batch.rst
index ab44f94..fefb117 100644
--- a/docs/endaq/batch.rst
+++ b/docs/endaq/batch.rst
@@ -1,6 +1,6 @@
-#############
-`endaq.batch`
-#############
+###############
+``endaq.batch``
+###############
 
 
 .. automodule:: endaq.batch
diff --git a/docs/endaq/calc.rst b/docs/endaq/calc.rst
index daa1c13..2c00e61 100644
--- a/docs/endaq/calc.rst
+++ b/docs/endaq/calc.rst
@@ -1,46 +1,56 @@
-############
-`endaq.calc`
-############
+##############
+``endaq.calc``
+##############
 
+:py:mod:`endaq.calc` is a module comprising a collection of common calculations
+for vibration analysis. It leverages the standard Python scientific
+stack (NumPy, SciPy, Pandas) in order to enable engineers to perform
+domain-specific calculations in a few lines of code, without having to
+first master Python and its scientific stack in their entireties.
 
+.. toctree::
+   :maxdepth: 2
 
-`endaq.calc.filters`
-====================
+   calc_usage
+
+
+``endaq.calc.filters``
+======================
 
 .. automodule:: endaq.calc.filters
   :members:
 
 
-`endaq.calc.integrate`
-======================
+``endaq.calc.integrate``
+========================
 
 .. automodule:: endaq.calc.integrate
   :members:
 
 
-`endaq.calc.psd`
-================
+``endaq.calc.psd``
+==================
 
 .. automodule:: endaq.calc.psd
   :members:
 
 
-`endaq.calc.shock`
-==================
+``endaq.calc.shock``
+====================
 
 .. automodule:: endaq.calc.shock
   :members:
 
 
-`endaq.calc.stats`
-==================
+``endaq.calc.stats``
+====================
 
 .. automodule:: endaq.calc.stats
   :members:
 
 
-`endaq.calc.utils`
-==================
+``endaq.calc.utils``
+====================
 
 .. automodule:: endaq.calc.utils
   :members:
diff --git a/docs/endaq/calc_usage.rst b/docs/endaq/calc_usage.rst
new file mode 100644
index 0000000..000d473
--- /dev/null
+++ b/docs/endaq/calc_usage.rst
@@ -0,0 +1,66 @@
+=============================
+``endaq.calc`` Usage Examples
+=============================
+
+
+Filters
+~~~~~~~
+
+.. code:: python
+
+   df_accel_highpass = endaq.calc.filters.butterworth(df_accel, low_cutoff=1, high_cutoff=None)
+   df_accel_lowpass = endaq.calc.filters.butterworth(df_accel, low_cutoff=None, high_cutoff=100)
+
+Integration
+~~~~~~~~~~~
+
+.. code:: python
+
+   dfs_integrate = endaq.calc.integrate.integrals(df_accel, n=2, highpass_cutoff=1.0, tukey_percent=0.05)
+
+PSD
+~~~
+
+Linearly-spaced
+^^^^^^^^^^^^^^^
+
+.. code:: python
+
+   df_accel_psd = endaq.calc.psd.welch(df_accel, bin_width=1/11)
+
+Octave-spaced
+^^^^^^^^^^^^^
+
+.. code:: python
+
+   df_accel_psd_oct = endaq.calc.psd.to_octave(df_accel_psd, fstart=1, octave_bins=3)
+
+Derivatives & Integrals
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+   df_vel_psd = endaq.calc.psd.differentiate(df_accel_psd, n=-1)
+   df_jerk_psd = endaq.calc.psd.differentiate(df_accel_psd, n=1)
+
+Vibration Criterion (VC) Curves
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+   df_accel_vc = endaq.calc.psd.vc_curves(df_accel_psd, fstart=1, octave_bins=3)
+
+Shock Analysis
+~~~~~~~~~~~~~~
+
+.. code:: python
+
+   df_accel_pvss = endaq.calc.shock.shock_spectrum(df_accel, freqs=2 ** np.arange(-10, 13, 0.25), damp=0.05, mode="pvss")
+   df_accel_srs = endaq.calc.shock.shock_spectrum(df_accel, freqs=[1, 10, 100, 1000], damp=0.05, mode="srs")
+
+Shock Characterization: Half-Sine-Wave Pulse
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code:: python
+
+   half_sine_params = endaq.calc.shock.enveloping_half_sine(df_accel_pvss, damp=0.05)
diff --git a/docs/endaq/cloud.rst b/docs/endaq/cloud.rst
index 6fa6e9b..b7a0b8c 100644
--- a/docs/endaq/cloud.rst
+++ b/docs/endaq/cloud.rst
@@ -1,6 +1,13 @@
-#############
-`endaq.cloud`
-#############
+###############
+``endaq.cloud``
+###############
+
+This module houses tools and functions to make both interacting with the enDAQ Cloud API and the [enDAQ Cloud](https://cloud.endaq.com) itself easier.
+
+.. toctree::
+   :maxdepth: 2
+
+   cloud_API_wrapper
 
 
 `endaq.cloud.core`
diff --git a/docs/endaq/cloud_API_wrapper.rst b/docs/endaq/cloud_API_wrapper.rst
index 9f129fb..6ac5fa1 100644
--- a/docs/endaq/cloud_API_wrapper.rst
+++ b/docs/endaq/cloud_API_wrapper.rst
@@ -1,6 +1,6 @@
-#########################
-`endaq.cloud` API Wrapper
-#########################
+#######################
+enDAQ Cloud API Wrapper
+#######################
 
 The API Wrapper provides a simple command-line interface for accessing basic file and device information from the enDAQ Cloud API. Output of all commands except ``account`` and `attributes` are in ``csv`` files in the ``output`` folder.
 
diff --git a/docs/endaq/ide.rst b/docs/endaq/ide.rst
index d771d30..436d01f 100644
--- a/docs/endaq/ide.rst
+++ b/docs/endaq/ide.rst
@@ -1,7 +1,15 @@
-###########
-`endaq.ide`
-###########
+#############
+``endaq.ide``
+#############
 
+The submodule ``endaq.ide`` contains utility functions for accessing data in *Mide Instrumentation Data Exchange*
+(``.IDE``) files, the native format generated by enDAQ data recorders.
+
+.. toctree::
+   :maxdepth: 2
+
+   ide_measurement
+   ide_usage
 
 .. automodule:: endaq.ide
   :members:
diff --git a/docs/endaq/ide_measurement.rst b/docs/endaq/ide_measurement.rst
new file mode 100644
index 0000000..26199ff
--- /dev/null
+++ b/docs/endaq/ide_measurement.rst
@@ -0,0 +1,8 @@
+#########################
+``endaq.ide.measurement``
+#########################
+
+
+.. automodule:: endaq.ide.measurement
+   :members:
+   :exclude-members: filter_channels, get_channels, get_measurement_type
\ No newline at end of file
diff --git a/docs/endaq/ide_usage.rst b/docs/endaq/ide_usage.rst
new file mode 100644
index 0000000..dadb554
--- /dev/null
+++ b/docs/endaq/ide_usage.rst
@@ -0,0 +1,685 @@
+############################
+``endaq.ide`` Usage Examples
+############################
+
+Note: For brevity, the following examples assume everything has been imported
+from :py:mod:`endaq.ide`:
+
+.. code:: python3
+
+    from endaq.ide import *
+
+Opening IDE files: :py:func:`endaq.ide.get_doc()`
+-------------------------------------------------
+
+:py:mod:`endaq.ide` includes a convenient shortcut for importing IDE data:
+:py:func:`~endaq.ide.get_doc()`. It can load data from local files, or read data directly
+from a URL.
+
+.. code:: python3
+
+    doc = get_doc("tests/test.ide")
+    doc1 = get_doc("https://info.endaq.com/hubfs/data/surgical-instrument.ide")
+
+IDE files can be retrieved directly from Google Drive using a Drive
+'sharable link' URL. The file must be set to allow access to "Anyone
+with the link."
+
+.. code:: python3
+
+    doc2 = get_doc("https://drive.google.com/file/d/1t3JqbZGhuZbIK9agH24YZIdVE26-NOF5/view?usp=sharing")
+
+Whether opening a local file or a URL, :py:func:`~endaq.ide.get_doc()` can be used to
+import only a specific interval by way of its ``start`` and ``end``
+parameters:
+
+.. code:: python3
+
+    doc3 = get_doc("tests/test.ide", start="5s", end="10s")
+
+Summarizing IDE files: :py:func:`endaq.ide.get_channel_table()`
+---------------------------------------------------------------
+
+Once an IDE file has been loaded, :py:func:`~endaq.ide.get_channel_table()` will
+retrieve basic summary information about its contents.
+
+.. code:: python3
+
+    get_channel_table(doc)
+
+
+
+
+.. raw:: html
+
+    <style type="text/css">
+    </style>
+    <table id="T_d2cbb_">
+      <thead>
+        <tr>
+          <th class="blank level0" >&nbsp;</th>
+          <th class="col_heading level0 col0" >channel</th>
+          <th class="col_heading level0 col1" >name</th>
+          <th class="col_heading level0 col2" >type</th>
+          <th class="col_heading level0 col3" >units</th>
+          <th class="col_heading level0 col4" >start</th>
+          <th class="col_heading level0 col5" >end</th>
+          <th class="col_heading level0 col6" >duration</th>
+          <th class="col_heading level0 col7" >samples</th>
+          <th class="col_heading level0 col8" >rate</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <th id="T_d2cbb_level0_row0" class="row_heading level0 row0" >0</th>
+          <td id="T_d2cbb_row0_col0" class="data row0 col0" >32.0</td>
+          <td id="T_d2cbb_row0_col1" class="data row0 col1" >X (16g)</td>
+          <td id="T_d2cbb_row0_col2" class="data row0 col2" >Acceleration</td>
+          <td id="T_d2cbb_row0_col3" class="data row0 col3" >g</td>
+          <td id="T_d2cbb_row0_col4" class="data row0 col4" >00:00.0952</td>
+          <td id="T_d2cbb_row0_col5" class="data row0 col5" >00:19.0012</td>
+          <td id="T_d2cbb_row0_col6" class="data row0 col6" >00:18.0059</td>
+          <td id="T_d2cbb_row0_col7" class="data row0 col7" >7113</td>
+          <td id="T_d2cbb_row0_col8" class="data row0 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row1" class="row_heading level0 row1" >1</th>
+          <td id="T_d2cbb_row1_col0" class="data row1 col0" >32.1</td>
+          <td id="T_d2cbb_row1_col1" class="data row1 col1" >Y (16g)</td>
+          <td id="T_d2cbb_row1_col2" class="data row1 col2" >Acceleration</td>
+          <td id="T_d2cbb_row1_col3" class="data row1 col3" >g</td>
+          <td id="T_d2cbb_row1_col4" class="data row1 col4" >00:00.0952</td>
+          <td id="T_d2cbb_row1_col5" class="data row1 col5" >00:19.0012</td>
+          <td id="T_d2cbb_row1_col6" class="data row1 col6" >00:18.0059</td>
+          <td id="T_d2cbb_row1_col7" class="data row1 col7" >7113</td>
+          <td id="T_d2cbb_row1_col8" class="data row1 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row2" class="row_heading level0 row2" >2</th>
+          <td id="T_d2cbb_row2_col0" class="data row2 col0" >32.2</td>
+          <td id="T_d2cbb_row2_col1" class="data row2 col1" >Z (16g)</td>
+          <td id="T_d2cbb_row2_col2" class="data row2 col2" >Acceleration</td>
+          <td id="T_d2cbb_row2_col3" class="data row2 col3" >g</td>
+          <td id="T_d2cbb_row2_col4" class="data row2 col4" >00:00.0952</td>
+          <td id="T_d2cbb_row2_col5" class="data row2 col5" >00:19.0012</td>
+          <td id="T_d2cbb_row2_col6" class="data row2 col6" >00:18.0059</td>
+          <td id="T_d2cbb_row2_col7" class="data row2 col7" >7113</td>
+          <td id="T_d2cbb_row2_col8" class="data row2 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row3" class="row_heading level0 row3" >3</th>
+          <td id="T_d2cbb_row3_col0" class="data row3 col0" >80.0</td>
+          <td id="T_d2cbb_row3_col1" class="data row3 col1" >X (8g)</td>
+          <td id="T_d2cbb_row3_col2" class="data row3 col2" >Acceleration</td>
+          <td id="T_d2cbb_row3_col3" class="data row3 col3" >g</td>
+          <td id="T_d2cbb_row3_col4" class="data row3 col4" >00:00.0948</td>
+          <td id="T_d2cbb_row3_col5" class="data row3 col5" >00:19.0013</td>
+          <td id="T_d2cbb_row3_col6" class="data row3 col6" >00:18.0064</td>
+          <td id="T_d2cbb_row3_col7" class="data row3 col7" >9070</td>
+          <td id="T_d2cbb_row3_col8" class="data row3 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row4" class="row_heading level0 row4" >4</th>
+          <td id="T_d2cbb_row4_col0" class="data row4 col0" >80.1</td>
+          <td id="T_d2cbb_row4_col1" class="data row4 col1" >Y (8g)</td>
+          <td id="T_d2cbb_row4_col2" class="data row4 col2" >Acceleration</td>
+          <td id="T_d2cbb_row4_col3" class="data row4 col3" >g</td>
+          <td id="T_d2cbb_row4_col4" class="data row4 col4" >00:00.0948</td>
+          <td id="T_d2cbb_row4_col5" class="data row4 col5" >00:19.0013</td>
+          <td id="T_d2cbb_row4_col6" class="data row4 col6" >00:18.0064</td>
+          <td id="T_d2cbb_row4_col7" class="data row4 col7" >9070</td>
+          <td id="T_d2cbb_row4_col8" class="data row4 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row5" class="row_heading level0 row5" >5</th>
+          <td id="T_d2cbb_row5_col0" class="data row5 col0" >80.2</td>
+          <td id="T_d2cbb_row5_col1" class="data row5 col1" >Z (8g)</td>
+          <td id="T_d2cbb_row5_col2" class="data row5 col2" >Acceleration</td>
+          <td id="T_d2cbb_row5_col3" class="data row5 col3" >g</td>
+          <td id="T_d2cbb_row5_col4" class="data row5 col4" >00:00.0948</td>
+          <td id="T_d2cbb_row5_col5" class="data row5 col5" >00:19.0013</td>
+          <td id="T_d2cbb_row5_col6" class="data row5 col6" >00:18.0064</td>
+          <td id="T_d2cbb_row5_col7" class="data row5 col7" >9070</td>
+          <td id="T_d2cbb_row5_col8" class="data row5 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row6" class="row_heading level0 row6" >6</th>
+          <td id="T_d2cbb_row6_col0" class="data row6 col0" >36.0</td>
+          <td id="T_d2cbb_row6_col1" class="data row6 col1" >Pressure/Temperature:00</td>
+          <td id="T_d2cbb_row6_col2" class="data row6 col2" >Pressure</td>
+          <td id="T_d2cbb_row6_col3" class="data row6 col3" >Pa</td>
+          <td id="T_d2cbb_row6_col4" class="data row6 col4" >00:00.0945</td>
+          <td id="T_d2cbb_row6_col5" class="data row6 col5" >00:19.0175</td>
+          <td id="T_d2cbb_row6_col6" class="data row6 col6" >00:18.0230</td>
+          <td id="T_d2cbb_row6_col7" class="data row6 col7" >20</td>
+          <td id="T_d2cbb_row6_col8" class="data row6 col8" >1.10 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row7" class="row_heading level0 row7" >7</th>
+          <td id="T_d2cbb_row7_col0" class="data row7 col0" >36.1</td>
+          <td id="T_d2cbb_row7_col1" class="data row7 col1" >Pressure/Temperature:01</td>
+          <td id="T_d2cbb_row7_col2" class="data row7 col2" >Temperature</td>
+          <td id="T_d2cbb_row7_col3" class="data row7 col3" >°C</td>
+          <td id="T_d2cbb_row7_col4" class="data row7 col4" >00:00.0945</td>
+          <td id="T_d2cbb_row7_col5" class="data row7 col5" >00:19.0175</td>
+          <td id="T_d2cbb_row7_col6" class="data row7 col6" >00:18.0230</td>
+          <td id="T_d2cbb_row7_col7" class="data row7 col7" >20</td>
+          <td id="T_d2cbb_row7_col8" class="data row7 col8" >1.10 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row8" class="row_heading level0 row8" >8</th>
+          <td id="T_d2cbb_row8_col0" class="data row8 col0" >70.0</td>
+          <td id="T_d2cbb_row8_col1" class="data row8 col1" >X</td>
+          <td id="T_d2cbb_row8_col2" class="data row8 col2" >Quaternion</td>
+          <td id="T_d2cbb_row8_col3" class="data row8 col3" >q</td>
+          <td id="T_d2cbb_row8_col4" class="data row8 col4" >00:01.0132</td>
+          <td id="T_d2cbb_row8_col5" class="data row8 col5" >00:18.0954</td>
+          <td id="T_d2cbb_row8_col6" class="data row8 col6" >00:17.0821</td>
+          <td id="T_d2cbb_row8_col7" class="data row8 col7" >1755</td>
+          <td id="T_d2cbb_row8_col8" class="data row8 col8" >98.47 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row9" class="row_heading level0 row9" >9</th>
+          <td id="T_d2cbb_row9_col0" class="data row9 col0" >70.1</td>
+          <td id="T_d2cbb_row9_col1" class="data row9 col1" >Y</td>
+          <td id="T_d2cbb_row9_col2" class="data row9 col2" >Quaternion</td>
+          <td id="T_d2cbb_row9_col3" class="data row9 col3" >q</td>
+          <td id="T_d2cbb_row9_col4" class="data row9 col4" >00:01.0132</td>
+          <td id="T_d2cbb_row9_col5" class="data row9 col5" >00:18.0954</td>
+          <td id="T_d2cbb_row9_col6" class="data row9 col6" >00:17.0821</td>
+          <td id="T_d2cbb_row9_col7" class="data row9 col7" >1755</td>
+          <td id="T_d2cbb_row9_col8" class="data row9 col8" >98.47 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row10" class="row_heading level0 row10" >10</th>
+          <td id="T_d2cbb_row10_col0" class="data row10 col0" >70.2</td>
+          <td id="T_d2cbb_row10_col1" class="data row10 col1" >Z</td>
+          <td id="T_d2cbb_row10_col2" class="data row10 col2" >Quaternion</td>
+          <td id="T_d2cbb_row10_col3" class="data row10 col3" >q</td>
+          <td id="T_d2cbb_row10_col4" class="data row10 col4" >00:01.0132</td>
+          <td id="T_d2cbb_row10_col5" class="data row10 col5" >00:18.0954</td>
+          <td id="T_d2cbb_row10_col6" class="data row10 col6" >00:17.0821</td>
+          <td id="T_d2cbb_row10_col7" class="data row10 col7" >1755</td>
+          <td id="T_d2cbb_row10_col8" class="data row10 col8" >98.47 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row11" class="row_heading level0 row11" >11</th>
+          <td id="T_d2cbb_row11_col0" class="data row11 col0" >70.3</td>
+          <td id="T_d2cbb_row11_col1" class="data row11 col1" >W</td>
+          <td id="T_d2cbb_row11_col2" class="data row11 col2" >Quaternion</td>
+          <td id="T_d2cbb_row11_col3" class="data row11 col3" >q</td>
+          <td id="T_d2cbb_row11_col4" class="data row11 col4" >00:01.0132</td>
+          <td id="T_d2cbb_row11_col5" class="data row11 col5" >00:18.0954</td>
+          <td id="T_d2cbb_row11_col6" class="data row11 col6" >00:17.0821</td>
+          <td id="T_d2cbb_row11_col7" class="data row11 col7" >1755</td>
+          <td id="T_d2cbb_row11_col8" class="data row11 col8" >98.47 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row12" class="row_heading level0 row12" >12</th>
+          <td id="T_d2cbb_row12_col0" class="data row12 col0" >59.0</td>
+          <td id="T_d2cbb_row12_col1" class="data row12 col1" >Control Pad Pressure</td>
+          <td id="T_d2cbb_row12_col2" class="data row12 col2" >Pressure</td>
+          <td id="T_d2cbb_row12_col3" class="data row12 col3" >Pa</td>
+          <td id="T_d2cbb_row12_col4" class="data row12 col4" >00:00.0979</td>
+          <td id="T_d2cbb_row12_col5" class="data row12 col5" >00:18.0910</td>
+          <td id="T_d2cbb_row12_col6" class="data row12 col6" >00:17.0931</td>
+          <td id="T_d2cbb_row12_col7" class="data row12 col7" >180</td>
+          <td id="T_d2cbb_row12_col8" class="data row12 col8" >10.04 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row13" class="row_heading level0 row13" >13</th>
+          <td id="T_d2cbb_row13_col0" class="data row13 col0" >59.1</td>
+          <td id="T_d2cbb_row13_col1" class="data row13 col1" >Control Pad Temperature</td>
+          <td id="T_d2cbb_row13_col2" class="data row13 col2" >Temperature</td>
+          <td id="T_d2cbb_row13_col3" class="data row13 col3" >°C</td>
+          <td id="T_d2cbb_row13_col4" class="data row13 col4" >00:00.0979</td>
+          <td id="T_d2cbb_row13_col5" class="data row13 col5" >00:18.0910</td>
+          <td id="T_d2cbb_row13_col6" class="data row13 col6" >00:17.0931</td>
+          <td id="T_d2cbb_row13_col7" class="data row13 col7" >180</td>
+          <td id="T_d2cbb_row13_col8" class="data row13 col8" >10.04 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row14" class="row_heading level0 row14" >14</th>
+          <td id="T_d2cbb_row14_col0" class="data row14 col0" >76.0</td>
+          <td id="T_d2cbb_row14_col1" class="data row14 col1" >Lux</td>
+          <td id="T_d2cbb_row14_col2" class="data row14 col2" >Light</td>
+          <td id="T_d2cbb_row14_col3" class="data row14 col3" >Ill</td>
+          <td id="T_d2cbb_row14_col4" class="data row14 col4" >00:00.0000</td>
+          <td id="T_d2cbb_row14_col5" class="data row14 col5" >00:18.0737</td>
+          <td id="T_d2cbb_row14_col6" class="data row14 col6" >00:18.0737</td>
+          <td id="T_d2cbb_row14_col7" class="data row14 col7" >71</td>
+          <td id="T_d2cbb_row14_col8" class="data row14 col8" >3.79 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_d2cbb_level0_row15" class="row_heading level0 row15" >15</th>
+          <td id="T_d2cbb_row15_col0" class="data row15 col0" >76.1</td>
+          <td id="T_d2cbb_row15_col1" class="data row15 col1" >UV</td>
+          <td id="T_d2cbb_row15_col2" class="data row15 col2" >Light</td>
+          <td id="T_d2cbb_row15_col3" class="data row15 col3" >Index</td>
+          <td id="T_d2cbb_row15_col4" class="data row15 col4" >00:00.0000</td>
+          <td id="T_d2cbb_row15_col5" class="data row15 col5" >00:18.0737</td>
+          <td id="T_d2cbb_row15_col6" class="data row15 col6" >00:18.0737</td>
+          <td id="T_d2cbb_row15_col7" class="data row15 col7" >71</td>
+          <td id="T_d2cbb_row15_col8" class="data row15 col8" >3.79 Hz</td>
+        </tr>
+      </tbody>
+    </table>
+
+
+
+
+The results can be filtered by :doc:`measurement type <ide_measurement>`:
+
+.. code:: python3
+
+    get_channel_table(doc, ACCELERATION)
+
+
+
+
+.. raw:: html
+
+    <style type="text/css">
+    </style>
+    <table id="T_9f9cf_">
+      <thead>
+        <tr>
+          <th class="blank level0" >&nbsp;</th>
+          <th class="col_heading level0 col0" >channel</th>
+          <th class="col_heading level0 col1" >name</th>
+          <th class="col_heading level0 col2" >type</th>
+          <th class="col_heading level0 col3" >units</th>
+          <th class="col_heading level0 col4" >start</th>
+          <th class="col_heading level0 col5" >end</th>
+          <th class="col_heading level0 col6" >duration</th>
+          <th class="col_heading level0 col7" >samples</th>
+          <th class="col_heading level0 col8" >rate</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <th id="T_9f9cf_level0_row0" class="row_heading level0 row0" >0</th>
+          <td id="T_9f9cf_row0_col0" class="data row0 col0" >32.0</td>
+          <td id="T_9f9cf_row0_col1" class="data row0 col1" >X (16g)</td>
+          <td id="T_9f9cf_row0_col2" class="data row0 col2" >Acceleration</td>
+          <td id="T_9f9cf_row0_col3" class="data row0 col3" >g</td>
+          <td id="T_9f9cf_row0_col4" class="data row0 col4" >00:00.0952</td>
+          <td id="T_9f9cf_row0_col5" class="data row0 col5" >00:19.0012</td>
+          <td id="T_9f9cf_row0_col6" class="data row0 col6" >00:18.0059</td>
+          <td id="T_9f9cf_row0_col7" class="data row0 col7" >7113</td>
+          <td id="T_9f9cf_row0_col8" class="data row0 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_9f9cf_level0_row1" class="row_heading level0 row1" >1</th>
+          <td id="T_9f9cf_row1_col0" class="data row1 col0" >32.1</td>
+          <td id="T_9f9cf_row1_col1" class="data row1 col1" >Y (16g)</td>
+          <td id="T_9f9cf_row1_col2" class="data row1 col2" >Acceleration</td>
+          <td id="T_9f9cf_row1_col3" class="data row1 col3" >g</td>
+          <td id="T_9f9cf_row1_col4" class="data row1 col4" >00:00.0952</td>
+          <td id="T_9f9cf_row1_col5" class="data row1 col5" >00:19.0012</td>
+          <td id="T_9f9cf_row1_col6" class="data row1 col6" >00:18.0059</td>
+          <td id="T_9f9cf_row1_col7" class="data row1 col7" >7113</td>
+          <td id="T_9f9cf_row1_col8" class="data row1 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_9f9cf_level0_row2" class="row_heading level0 row2" >2</th>
+          <td id="T_9f9cf_row2_col0" class="data row2 col0" >32.2</td>
+          <td id="T_9f9cf_row2_col1" class="data row2 col1" >Z (16g)</td>
+          <td id="T_9f9cf_row2_col2" class="data row2 col2" >Acceleration</td>
+          <td id="T_9f9cf_row2_col3" class="data row2 col3" >g</td>
+          <td id="T_9f9cf_row2_col4" class="data row2 col4" >00:00.0952</td>
+          <td id="T_9f9cf_row2_col5" class="data row2 col5" >00:19.0012</td>
+          <td id="T_9f9cf_row2_col6" class="data row2 col6" >00:18.0059</td>
+          <td id="T_9f9cf_row2_col7" class="data row2 col7" >7113</td>
+          <td id="T_9f9cf_row2_col8" class="data row2 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_9f9cf_level0_row3" class="row_heading level0 row3" >3</th>
+          <td id="T_9f9cf_row3_col0" class="data row3 col0" >80.0</td>
+          <td id="T_9f9cf_row3_col1" class="data row3 col1" >X (8g)</td>
+          <td id="T_9f9cf_row3_col2" class="data row3 col2" >Acceleration</td>
+          <td id="T_9f9cf_row3_col3" class="data row3 col3" >g</td>
+          <td id="T_9f9cf_row3_col4" class="data row3 col4" >00:00.0948</td>
+          <td id="T_9f9cf_row3_col5" class="data row3 col5" >00:19.0013</td>
+          <td id="T_9f9cf_row3_col6" class="data row3 col6" >00:18.0064</td>
+          <td id="T_9f9cf_row3_col7" class="data row3 col7" >9070</td>
+          <td id="T_9f9cf_row3_col8" class="data row3 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_9f9cf_level0_row4" class="row_heading level0 row4" >4</th>
+          <td id="T_9f9cf_row4_col0" class="data row4 col0" >80.1</td>
+          <td id="T_9f9cf_row4_col1" class="data row4 col1" >Y (8g)</td>
+          <td id="T_9f9cf_row4_col2" class="data row4 col2" >Acceleration</td>
+          <td id="T_9f9cf_row4_col3" class="data row4 col3" >g</td>
+          <td id="T_9f9cf_row4_col4" class="data row4 col4" >00:00.0948</td>
+          <td id="T_9f9cf_row4_col5" class="data row4 col5" >00:19.0013</td>
+          <td id="T_9f9cf_row4_col6" class="data row4 col6" >00:18.0064</td>
+          <td id="T_9f9cf_row4_col7" class="data row4 col7" >9070</td>
+          <td id="T_9f9cf_row4_col8" class="data row4 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_9f9cf_level0_row5" class="row_heading level0 row5" >5</th>
+          <td id="T_9f9cf_row5_col0" class="data row5 col0" >80.2</td>
+          <td id="T_9f9cf_row5_col1" class="data row5 col1" >Z (8g)</td>
+          <td id="T_9f9cf_row5_col2" class="data row5 col2" >Acceleration</td>
+          <td id="T_9f9cf_row5_col3" class="data row5 col3" >g</td>
+          <td id="T_9f9cf_row5_col4" class="data row5 col4" >00:00.0948</td>
+          <td id="T_9f9cf_row5_col5" class="data row5 col5" >00:19.0013</td>
+          <td id="T_9f9cf_row5_col6" class="data row5 col6" >00:18.0064</td>
+          <td id="T_9f9cf_row5_col7" class="data row5 col7" >9070</td>
+          <td id="T_9f9cf_row5_col8" class="data row5 col8" >502.09 Hz</td>
+        </tr>
+      </tbody>
+    </table>
+
+
+
+
+Measurement types can be combined to retrieve more than one:
+
+.. code:: python3
+
+    get_channel_table(doc, ACCELERATION+TEMPERATURE)
+
+
+
+
+.. raw:: html
+
+    <style type="text/css">
+    </style>
+    <table id="T_68598_">
+      <thead>
+        <tr>
+          <th class="blank level0" >&nbsp;</th>
+          <th class="col_heading level0 col0" >channel</th>
+          <th class="col_heading level0 col1" >name</th>
+          <th class="col_heading level0 col2" >type</th>
+          <th class="col_heading level0 col3" >units</th>
+          <th class="col_heading level0 col4" >start</th>
+          <th class="col_heading level0 col5" >end</th>
+          <th class="col_heading level0 col6" >duration</th>
+          <th class="col_heading level0 col7" >samples</th>
+          <th class="col_heading level0 col8" >rate</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <th id="T_68598_level0_row0" class="row_heading level0 row0" >0</th>
+          <td id="T_68598_row0_col0" class="data row0 col0" >32.0</td>
+          <td id="T_68598_row0_col1" class="data row0 col1" >X (16g)</td>
+          <td id="T_68598_row0_col2" class="data row0 col2" >Acceleration</td>
+          <td id="T_68598_row0_col3" class="data row0 col3" >g</td>
+          <td id="T_68598_row0_col4" class="data row0 col4" >00:00.0952</td>
+          <td id="T_68598_row0_col5" class="data row0 col5" >00:19.0012</td>
+          <td id="T_68598_row0_col6" class="data row0 col6" >00:18.0059</td>
+          <td id="T_68598_row0_col7" class="data row0 col7" >7113</td>
+          <td id="T_68598_row0_col8" class="data row0 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row1" class="row_heading level0 row1" >1</th>
+          <td id="T_68598_row1_col0" class="data row1 col0" >32.1</td>
+          <td id="T_68598_row1_col1" class="data row1 col1" >Y (16g)</td>
+          <td id="T_68598_row1_col2" class="data row1 col2" >Acceleration</td>
+          <td id="T_68598_row1_col3" class="data row1 col3" >g</td>
+          <td id="T_68598_row1_col4" class="data row1 col4" >00:00.0952</td>
+          <td id="T_68598_row1_col5" class="data row1 col5" >00:19.0012</td>
+          <td id="T_68598_row1_col6" class="data row1 col6" >00:18.0059</td>
+          <td id="T_68598_row1_col7" class="data row1 col7" >7113</td>
+          <td id="T_68598_row1_col8" class="data row1 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row2" class="row_heading level0 row2" >2</th>
+          <td id="T_68598_row2_col0" class="data row2 col0" >32.2</td>
+          <td id="T_68598_row2_col1" class="data row2 col1" >Z (16g)</td>
+          <td id="T_68598_row2_col2" class="data row2 col2" >Acceleration</td>
+          <td id="T_68598_row2_col3" class="data row2 col3" >g</td>
+          <td id="T_68598_row2_col4" class="data row2 col4" >00:00.0952</td>
+          <td id="T_68598_row2_col5" class="data row2 col5" >00:19.0012</td>
+          <td id="T_68598_row2_col6" class="data row2 col6" >00:18.0059</td>
+          <td id="T_68598_row2_col7" class="data row2 col7" >7113</td>
+          <td id="T_68598_row2_col8" class="data row2 col8" >393.86 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row3" class="row_heading level0 row3" >3</th>
+          <td id="T_68598_row3_col0" class="data row3 col0" >80.0</td>
+          <td id="T_68598_row3_col1" class="data row3 col1" >X (8g)</td>
+          <td id="T_68598_row3_col2" class="data row3 col2" >Acceleration</td>
+          <td id="T_68598_row3_col3" class="data row3 col3" >g</td>
+          <td id="T_68598_row3_col4" class="data row3 col4" >00:00.0948</td>
+          <td id="T_68598_row3_col5" class="data row3 col5" >00:19.0013</td>
+          <td id="T_68598_row3_col6" class="data row3 col6" >00:18.0064</td>
+          <td id="T_68598_row3_col7" class="data row3 col7" >9070</td>
+          <td id="T_68598_row3_col8" class="data row3 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row4" class="row_heading level0 row4" >4</th>
+          <td id="T_68598_row4_col0" class="data row4 col0" >80.1</td>
+          <td id="T_68598_row4_col1" class="data row4 col1" >Y (8g)</td>
+          <td id="T_68598_row4_col2" class="data row4 col2" >Acceleration</td>
+          <td id="T_68598_row4_col3" class="data row4 col3" >g</td>
+          <td id="T_68598_row4_col4" class="data row4 col4" >00:00.0948</td>
+          <td id="T_68598_row4_col5" class="data row4 col5" >00:19.0013</td>
+          <td id="T_68598_row4_col6" class="data row4 col6" >00:18.0064</td>
+          <td id="T_68598_row4_col7" class="data row4 col7" >9070</td>
+          <td id="T_68598_row4_col8" class="data row4 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row5" class="row_heading level0 row5" >5</th>
+          <td id="T_68598_row5_col0" class="data row5 col0" >80.2</td>
+          <td id="T_68598_row5_col1" class="data row5 col1" >Z (8g)</td>
+          <td id="T_68598_row5_col2" class="data row5 col2" >Acceleration</td>
+          <td id="T_68598_row5_col3" class="data row5 col3" >g</td>
+          <td id="T_68598_row5_col4" class="data row5 col4" >00:00.0948</td>
+          <td id="T_68598_row5_col5" class="data row5 col5" >00:19.0013</td>
+          <td id="T_68598_row5_col6" class="data row5 col6" >00:18.0064</td>
+          <td id="T_68598_row5_col7" class="data row5 col7" >9070</td>
+          <td id="T_68598_row5_col8" class="data row5 col8" >502.09 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row6" class="row_heading level0 row6" >6</th>
+          <td id="T_68598_row6_col0" class="data row6 col0" >36.1</td>
+          <td id="T_68598_row6_col1" class="data row6 col1" >Pressure/Temperature:01</td>
+          <td id="T_68598_row6_col2" class="data row6 col2" >Temperature</td>
+          <td id="T_68598_row6_col3" class="data row6 col3" >°C</td>
+          <td id="T_68598_row6_col4" class="data row6 col4" >00:00.0945</td>
+          <td id="T_68598_row6_col5" class="data row6 col5" >00:19.0175</td>
+          <td id="T_68598_row6_col6" class="data row6 col6" >00:18.0230</td>
+          <td id="T_68598_row6_col7" class="data row6 col7" >20</td>
+          <td id="T_68598_row6_col8" class="data row6 col8" >1.10 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_68598_level0_row7" class="row_heading level0 row7" >7</th>
+          <td id="T_68598_row7_col0" class="data row7 col0" >59.1</td>
+          <td id="T_68598_row7_col1" class="data row7 col1" >Control Pad Temperature</td>
+          <td id="T_68598_row7_col2" class="data row7 col2" >Temperature</td>
+          <td id="T_68598_row7_col3" class="data row7 col3" >°C</td>
+          <td id="T_68598_row7_col4" class="data row7 col4" >00:00.0979</td>
+          <td id="T_68598_row7_col5" class="data row7 col5" >00:18.0910</td>
+          <td id="T_68598_row7_col6" class="data row7 col6" >00:17.0931</td>
+          <td id="T_68598_row7_col7" class="data row7 col7" >180</td>
+          <td id="T_68598_row7_col8" class="data row7 col8" >10.04 Hz</td>
+        </tr>
+      </tbody>
+    </table>
+
+
+
+
+Information about a specific interval can be retrieved by using the
+``start`` and/or ``end`` arguments. Note that due to different sampling
+rates, the start and end times for slower channels may not precisely
+match the specified ``start`` or ``end``.
+
+.. code:: python3
+
+    get_channel_table(doc, ACCELERATION+TEMPERATURE, start="0:05", end="0:10")
+
+
+
+
+.. raw:: html
+
+    <style type="text/css">
+    </style>
+    <table id="T_6ade9_">
+      <thead>
+        <tr>
+          <th class="blank level0" >&nbsp;</th>
+          <th class="col_heading level0 col0" >channel</th>
+          <th class="col_heading level0 col1" >name</th>
+          <th class="col_heading level0 col2" >type</th>
+          <th class="col_heading level0 col3" >units</th>
+          <th class="col_heading level0 col4" >start</th>
+          <th class="col_heading level0 col5" >end</th>
+          <th class="col_heading level0 col6" >duration</th>
+          <th class="col_heading level0 col7" >samples</th>
+          <th class="col_heading level0 col8" >rate</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <th id="T_6ade9_level0_row0" class="row_heading level0 row0" >0</th>
+          <td id="T_6ade9_row0_col0" class="data row0 col0" >32.0</td>
+          <td id="T_6ade9_row0_col1" class="data row0 col1" >X (16g)</td>
+          <td id="T_6ade9_row0_col2" class="data row0 col2" >Acceleration</td>
+          <td id="T_6ade9_row0_col3" class="data row0 col3" >g</td>
+          <td id="T_6ade9_row0_col4" class="data row0 col4" >00:05.0000</td>
+          <td id="T_6ade9_row0_col5" class="data row0 col5" >00:10.0001</td>
+          <td id="T_6ade9_row0_col6" class="data row0 col6" >00:05.0000</td>
+          <td id="T_6ade9_row0_col7" class="data row0 col7" >1969</td>
+          <td id="T_6ade9_row0_col8" class="data row0 col8" >393.75 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row1" class="row_heading level0 row1" >1</th>
+          <td id="T_6ade9_row1_col0" class="data row1 col0" >32.1</td>
+          <td id="T_6ade9_row1_col1" class="data row1 col1" >Y (16g)</td>
+          <td id="T_6ade9_row1_col2" class="data row1 col2" >Acceleration</td>
+          <td id="T_6ade9_row1_col3" class="data row1 col3" >g</td>
+          <td id="T_6ade9_row1_col4" class="data row1 col4" >00:05.0000</td>
+          <td id="T_6ade9_row1_col5" class="data row1 col5" >00:10.0001</td>
+          <td id="T_6ade9_row1_col6" class="data row1 col6" >00:05.0000</td>
+          <td id="T_6ade9_row1_col7" class="data row1 col7" >1969</td>
+          <td id="T_6ade9_row1_col8" class="data row1 col8" >393.75 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row2" class="row_heading level0 row2" >2</th>
+          <td id="T_6ade9_row2_col0" class="data row2 col0" >32.2</td>
+          <td id="T_6ade9_row2_col1" class="data row2 col1" >Z (16g)</td>
+          <td id="T_6ade9_row2_col2" class="data row2 col2" >Acceleration</td>
+          <td id="T_6ade9_row2_col3" class="data row2 col3" >g</td>
+          <td id="T_6ade9_row2_col4" class="data row2 col4" >00:05.0000</td>
+          <td id="T_6ade9_row2_col5" class="data row2 col5" >00:10.0001</td>
+          <td id="T_6ade9_row2_col6" class="data row2 col6" >00:05.0000</td>
+          <td id="T_6ade9_row2_col7" class="data row2 col7" >1969</td>
+          <td id="T_6ade9_row2_col8" class="data row2 col8" >393.75 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row3" class="row_heading level0 row3" >3</th>
+          <td id="T_6ade9_row3_col0" class="data row3 col0" >80.0</td>
+          <td id="T_6ade9_row3_col1" class="data row3 col1" >X (8g)</td>
+          <td id="T_6ade9_row3_col2" class="data row3 col2" >Acceleration</td>
+          <td id="T_6ade9_row3_col3" class="data row3 col3" >g</td>
+          <td id="T_6ade9_row3_col4" class="data row3 col4" >00:05.0000</td>
+          <td id="T_6ade9_row3_col5" class="data row3 col5" >00:10.0001</td>
+          <td id="T_6ade9_row3_col6" class="data row3 col6" >00:05.0000</td>
+          <td id="T_6ade9_row3_col7" class="data row3 col7" >2510</td>
+          <td id="T_6ade9_row3_col8" class="data row3 col8" >501.98 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row4" class="row_heading level0 row4" >4</th>
+          <td id="T_6ade9_row4_col0" class="data row4 col0" >80.1</td>
+          <td id="T_6ade9_row4_col1" class="data row4 col1" >Y (8g)</td>
+          <td id="T_6ade9_row4_col2" class="data row4 col2" >Acceleration</td>
+          <td id="T_6ade9_row4_col3" class="data row4 col3" >g</td>
+          <td id="T_6ade9_row4_col4" class="data row4 col4" >00:05.0000</td>
+          <td id="T_6ade9_row4_col5" class="data row4 col5" >00:10.0001</td>
+          <td id="T_6ade9_row4_col6" class="data row4 col6" >00:05.0000</td>
+          <td id="T_6ade9_row4_col7" class="data row4 col7" >2510</td>
+          <td id="T_6ade9_row4_col8" class="data row4 col8" >501.98 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row5" class="row_heading level0 row5" >5</th>
+          <td id="T_6ade9_row5_col0" class="data row5 col0" >80.2</td>
+          <td id="T_6ade9_row5_col1" class="data row5 col1" >Z (8g)</td>
+          <td id="T_6ade9_row5_col2" class="data row5 col2" >Acceleration</td>
+          <td id="T_6ade9_row5_col3" class="data row5 col3" >g</td>
+          <td id="T_6ade9_row5_col4" class="data row5 col4" >00:05.0000</td>
+          <td id="T_6ade9_row5_col5" class="data row5 col5" >00:10.0001</td>
+          <td id="T_6ade9_row5_col6" class="data row5 col6" >00:05.0000</td>
+          <td id="T_6ade9_row5_col7" class="data row5 col7" >2510</td>
+          <td id="T_6ade9_row5_col8" class="data row5 col8" >501.98 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row6" class="row_heading level0 row6" >6</th>
+          <td id="T_6ade9_row6_col0" class="data row6 col0" >36.1</td>
+          <td id="T_6ade9_row6_col1" class="data row6 col1" >Pressure/Temperature:01</td>
+          <td id="T_6ade9_row6_col2" class="data row6 col2" >Temperature</td>
+          <td id="T_6ade9_row6_col3" class="data row6 col3" >°C</td>
+          <td id="T_6ade9_row6_col4" class="data row6 col4" >00:04.0954</td>
+          <td id="T_6ade9_row6_col5" class="data row6 col5" >00:10.0966</td>
+          <td id="T_6ade9_row6_col6" class="data row6 col6" >00:06.0011</td>
+          <td id="T_6ade9_row6_col7" class="data row6 col7" >6</td>
+          <td id="T_6ade9_row6_col8" class="data row6 col8" >1.00 Hz</td>
+        </tr>
+        <tr>
+          <th id="T_6ade9_level0_row7" class="row_heading level0 row7" >7</th>
+          <td id="T_6ade9_row7_col0" class="data row7 col0" >59.1</td>
+          <td id="T_6ade9_row7_col1" class="data row7 col1" >Control Pad Temperature</td>
+          <td id="T_6ade9_row7_col2" class="data row7 col2" >Temperature</td>
+          <td id="T_6ade9_row7_col3" class="data row7 col3" >°C</td>
+          <td id="T_6ade9_row7_col4" class="data row7 col4" >00:05.0086</td>
+          <td id="T_6ade9_row7_col5" class="data row7 col5" >00:10.0095</td>
+          <td id="T_6ade9_row7_col6" class="data row7 col6" >00:05.0008</td>
+          <td id="T_6ade9_row7_col7" class="data row7 col7" >50</td>
+          <td id="T_6ade9_row7_col8" class="data row7 col8" >9.98 Hz</td>
+        </tr>
+      </tbody>
+    </table>
+
+
+
+
+Extracting intervals: :py:func:`endaq.ide.extract_time()`
+---------------------------------------------------------
+
+A portion of an IDE file can be saved to another, new IDE. The source
+can be a local filename or an opened IDE (from a file or URL).
+
+.. code:: python3
+
+    extract_time("tests/test.ide", "doc_extracted.ide", start="0:05", end="0:10")
+    extract_time(doc1, "doc1_extracted.ide", start="0:05", end="0:10")
+
+Additional sample IDE recording files
+-------------------------------------
+
+Here are a number of example IDE files, which may be used with
+:py:mod:`endaq.ide`:
+
+.. code:: python3
+
+    file_urls = ['https://info.endaq.com/hubfs/data/surgical-instrument.ide',
+                 'https://info.endaq.com/hubfs/data/97c3990f-Drive-Home_70-1616632444.ide',
+                 'https://info.endaq.com/hubfs/data/High-Drop.ide',
+                 'https://info.endaq.com/hubfs/data/HiTest-Shock.ide',
+                 'https://info.endaq.com/hubfs/data/Drive-Home_01.ide',
+                 'https://info.endaq.com/hubfs/data/Tower-of-Terror.ide',
+                 'https://info.endaq.com/hubfs/data/Punching-Bag.ide',
+                 'https://info.endaq.com/hubfs/data/Gun-Stock.ide',
+                 'https://info.endaq.com/hubfs/data/Seat-Base_21.ide',
+                 'https://info.endaq.com/hubfs/data/Seat-Top_09.ide',
+                 'https://info.endaq.com/hubfs/data/Bolted.ide',
+                 'https://info.endaq.com/hubfs/data/Motorcycle-Car-Crash.ide',
+                 'https://info.endaq.com/hubfs/data/train-passing.ide',
+                 'https://info.endaq.com/hubfs/data/baseball.ide',
+                 'https://info.endaq.com/hubfs/data/Clean-Room-VC.ide',
+                 'https://info.endaq.com/hubfs/data/enDAQ_Cropped.ide',
+                 'https://info.endaq.com/hubfs/data/Drive-Home_07.ide',
+                 'https://info.endaq.com/hubfs/data/ford_f150.ide',
+                 'https://info.endaq.com/hubfs/data/Drive-Home.ide',
+                 'https://info.endaq.com/hubfs/data/Mining-Data.ide',
+                 'https://info.endaq.com/hubfs/data/Mide-Airport-Drive-Lexus-Hybrid-Dash-W8.ide']
+
+These can be directly read from endaq.com using :py:func:`endaq.ide.get_doc()`,
+as previously described.
\ No newline at end of file
diff --git a/docs/endaq/plot.rst b/docs/endaq/plot.rst
index 89aea42..776308c 100644
--- a/docs/endaq/plot.rst
+++ b/docs/endaq/plot.rst
@@ -1,6 +1,11 @@
-############
-`endaq.plot`
-############
+##############
+``endaq.plot``
+##############
+
+.. toctree::
+   :maxdepth: 2
+
+   plot_usage
 
 
 `endaq.plot`
diff --git a/docs/endaq/plot_usage.rst b/docs/endaq/plot_usage.rst
new file mode 100644
index 0000000..cb29e28
--- /dev/null
+++ b/docs/endaq/plot_usage.rst
@@ -0,0 +1,67 @@
+``endaq.plot`` Usage Examples
+=============================
+
+For these examples we assume there is a Pandas DataFrame named ``df``
+which has it’s index as time stamps and it’s one column being sensor
+values (e.g. x-axis accleration, or pressure). It also assumes there is
+a Pandas DataFrame ``attribute_df`` which contains all the attribute
+data about various data files. More information can be found about how
+to get this data from enDAQ IDE files in the `endaq-cloud
+readme <https://github.com/MideTechnology/endaq-python/tree/main/endaq/cloud>`__.
+
+.. code:: python
+
+   from endaq.plot import octave_spectrogram, multi_file_plot_attributes, octave_psd_bar_plot
+   from endaq.plot.utilities import set_theme
+
+Setting The Aesthetic Theme
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+   set_theme(theme='endaq')
+
+Creating Spectrograms With Octave Spaced Frequencies
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+   data_df, fig = octave_spectrogram(df, window=.15)
+   fig.show()
+
+.. figure:: https://i.imgur.com/929aszu.png
+   :alt: Spectrogram With Octave Spaced Frequencies
+
+   Spectrogram With Octave Spaced Frequencies
+
+Creating PSD Bar Plots With Octave Spaced Frequencies
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+   fig = octave_psd_bar_plot(df, yaxis_title="Magnitude")
+   fig.show()
+
+.. figure:: https://i.imgur.com/ueqcVTQ.png
+   :alt: PSD Bar Plot With Octave Spaced Frequencies
+
+   PSD Bar Plot With Octave Spaced Frequencies
+
+Plot Attributes In Figure With Subplots
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+   fig = multi_file_plot_attributes(attribute_df)
+   fig.show()
+
+.. figure:: https://i.imgur.com/5Yy4DN7.png
+   :alt: Attributes Plotted As Subplots
+
+   Attributes Plotted As Subplots
+
+Other Links
+-----------
+
+-  the endaq package - https://github.com/MideTechnology/endaq-python
+-  the enDAQ homepage - https://endaq.com/
diff --git a/endaq/batch/analyzer.py b/endaq/batch/analyzer.py
index 028fa1a..343a534 100644
--- a/endaq/batch/analyzer.py
+++ b/endaq/batch/analyzer.py
@@ -1,3 +1,8 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+import typing
+from typing import List, Tuple, Optional
 import sys
 import warnings
 
@@ -6,7 +11,6 @@
 else:
     from backports.cached_property import cached_property
 
-
 import numpy as np
 import pandas as pd
 
@@ -24,7 +28,34 @@
 M_TO_MM = 1000
 
 
-class DatasetChannelCache:
+@dataclass
+class CalcParams:
+    """
+    The parameters for configuring the calculation routines in
+    `CalcCache`.
+
+    Each of these parameters is *intentionally* left w/o a default value.
+    Instead, defaults are provided at the function signatures for the
+    `endaq.batch.core` functions. This ensures that data is passed correctly
+    to them from the `CalcParam` object.
+    """
+
+    accel_highpass_cutoff: Optional[float]
+    accel_integral_tukey_percent: float
+    accel_integral_zero: typing.Literal["start", "mean", "median"]
+    accel_start_time: Optional[np.timedelta64]
+    accel_end_time: Optional[np.timedelta64]
+    accel_start_margin: Optional[np.timedelta64]
+    accel_end_margin: Optional[np.timedelta64]
+    psd_freq_bin_width: float
+    psd_window: str
+    pvss_init_freq: float
+    pvss_bins_per_octave: float
+    vc_init_freq: float
+    vc_bins_per_octave: float
+
+
+class CalcCache:
     """
     A wrapper for `idelib.dataset.Dataset` that caches channel data streams as
     `pandas.Dataset`s.
@@ -32,39 +63,32 @@ class DatasetChannelCache:
 
     PV_NATURAL_FREQS = np.logspace(0, 12, base=2, num=12 * 12 + 1, endpoint=True)
 
-    def __init__(
-        self,
-        dataset,
-        *,
-        preferred_chs=[],
-        accel_highpass_cutoff,
-        accel_integral_tukey_percent,
-        accel_integral_zero,
-        accel_start_time,
-        accel_end_time,
-        accel_start_margin,
-        accel_end_margin,
-        psd_freq_bin_width,
-        psd_window="hanning",
-        pvss_init_freq,
-        pvss_bins_per_octave,
-        vc_init_freq,
-        vc_bins_per_octave,
-    ):
+    def __init__(self, data, params: CalcParams):
         """
         Copies out the numpy arrays for the highest priority channel for each
         sensor type, and any relevant metadata.  Cuts them into chunks.
         """
-        if accel_start_time is not None and accel_start_margin is not None:
+        if (
+            params.accel_start_time is not None
+            and params.accel_start_margin is not None
+        ):
             raise ValueError(
                 "only one of `accel_start_time` and `accel_start_margin` may be set at once"
             )
-        if accel_end_time is not None and accel_end_margin is not None:
+        if params.accel_end_time is not None and params.accel_end_margin is not None:
             raise ValueError(
                 "only one of `accel_end_time` and `accel_end_margin` may be set at once"
             )
 
-        self._channels = ide_utils.dict_chs_best(
+        self._channels = data
+        self._params = params
+
+    @classmethod
+    def from_ide(cls, dataset, params: CalcParams, preferred_chs: List[int] = []):
+        """
+        Instantiate a new `CalcCache` object from an IDE file.
+        """
+        data = ide_utils.dict_chs_best(
             (
                 (utype, ch_struct)
                 for (utype, ch_struct) in ide_utils.chs_by_utype(dataset)
@@ -73,21 +97,46 @@ def __init__(
             max_key=lambda x: (x.channel.id in preferred_chs, len(x.eventarray)),
         )
 
-        self._filename = dataset.filename
-        self._accelerationFs = None  # gets set in `_accelerationData`
-        self._accel_highpass_cutoff = accel_highpass_cutoff
-        self._accel_start_time = accel_start_time
-        self._accel_end_time = accel_end_time
-        self._accel_start_margin = accel_start_margin
-        self._accel_end_margin = accel_end_margin
-        self._accel_integral_tukey_percent = accel_integral_tukey_percent
-        self._accel_integral_zero = accel_integral_zero
-        self._psd_window = psd_window
-        self._psd_freq_bin_width = psd_freq_bin_width
-        self._pvss_init_freq = pvss_init_freq
-        self._pvss_bins_per_octave = pvss_bins_per_octave
-        self._vc_init_freq = vc_init_freq
-        self._vc_bins_per_octave = vc_bins_per_octave
+        return cls(data, params=params)
+
+    @dataclass
+    class InputDataWrapper:
+        data: pd.DataFrame
+        units: Tuple[str, str]
+
+        def to_pandas(self, time_mode="datetime"):
+            expected_index_types = dict(
+                timedelta=(pd.TimedeltaIndex, "TimedeltaIndex"),
+                datetime=(pd.DatetimeIndex, "DatetimeIndex"),
+                seconds=(
+                    (pd.Float64Index, pd.Int64Index, pd.UInt64Index, pd.RangeIndex),
+                    "{Float64/Int64/UInt64/Range}Index",
+                ),
+            )
+            if not isinstance(self.data.index, expected_index_types[time_mode][0]):
+                raise ValueError(
+                    f"expected '{time_mode}' data index to be of type"
+                    f"`{expected_index_types[time_mode][1]}`, "
+                    f"instead found {type(self.data.index)}"
+                )
+
+            self.data.index.name = "timestamp"
+            self.data.columns.name = "axis"
+
+            return self.data
+
+    @classmethod
+    def from_raw_data(
+        cls, data: List[Tuple[pd.DataFrame, Tuple[str, str]]], params: CalcParams
+    ):
+        """
+        Instantiate a new `CalcCache` object from raw DataFrame / metadata pairs.
+        """
+        data = {
+            ide_utils.UTYPE_GROUPS[units[0]]: cls.InputDataWrapper(data, units)
+            for (data, units) in data
+        }
+        return cls(data, params=params)
 
     # ==========================================================================
     # Data Processing, just to make init cleaner
@@ -98,7 +147,7 @@ def _accelerationData(self):
         """Populate the _acceleration* fields, including splitting and extending data."""
         ch_struct = self._channels.get("acc", None)
         if ch_struct is None:
-            warnings.warn(f"no acceleration channel in {self._filename}")
+            warnings.warn(f"no acceleration channel in data")
             return pd.DataFrame(
                 np.empty((0, 3), dtype=float),
                 index=pd.Series([], dtype="timedelta64[ns]", name="time"),
@@ -114,29 +163,23 @@ def _accelerationData(self):
         except KeyError:
             raise ValueError(f'unknown acceleration channel units "{aUnits}"')
 
-        self._accelerationName = ch_struct.channel.name
-        self._accelerationFs = ch_struct.fs
-
         aData = conversionFactor * ch_struct.to_pandas(time_mode="timedelta")
+        dt = endaq.calc.sample_spacing(aData, convert=None)
 
-        if self._accel_start_margin is not None:
-            margin = int(
-                np.ceil(
-                    ch_struct.fs * self._accel_start_margin / np.timedelta64(1, "s")
-                )
-            )
+        if self._params.accel_start_margin is not None:
+            margin = int(np.ceil(self._params.accel_start_margin / dt))
             aData = aData.iloc[margin:]
-        elif self._accel_start_time is not None:
-            aData = aData.loc[self._accel_start_time :]
-        if self._accel_end_margin is not None:
-            margin = int(
-                np.ceil(ch_struct.fs * self._accel_end_margin / np.timedelta64(1, "s"))
-            )
+        elif self._params.accel_start_time is not None:
+            aData = aData.loc[self._params.accel_start_time :]
+        if self._params.accel_end_margin is not None:
+            margin = int(np.ceil(self._params.accel_end_margin / dt))
             aData = aData.iloc[: (-margin or None)]
-        elif self._accel_end_time is not None:
-            aData = aData.loc[: self._accel_end_time]
+        elif self._params.accel_end_time is not None:
+            aData = aData.loc[: self._params.accel_end_time]
 
-        aData = filters.butterworth(aData, low_cutoff=self._accel_highpass_cutoff)
+        aData = filters.butterworth(
+            aData, low_cutoff=self._params.accel_highpass_cutoff
+        )
 
         assert isinstance(aData, pd.DataFrame)
         return aData
@@ -163,11 +206,7 @@ def _microphoneData(self):
         if units.lower() != "a":
             raise ValueError(f'unknown microphone channel units "{units}"')
 
-        self._micName = ch_struct.channel.name
-        self._micFs = ch_struct.fs
-        data = ch_struct.to_pandas(time_mode="timedelta")
-
-        return data
+        return ch_struct.to_pandas(time_mode="timedelta")
 
     @cached_property
     def _velocityData(self):
@@ -175,7 +214,7 @@ def _velocityData(self):
         if aData.size == 0:
             return aData
 
-        if not self._accel_highpass_cutoff:
+        if not self._params.accel_highpass_cutoff:
             warnings.warn(
                 "no highpass filter used before integration; "
                 "velocity calculation may be unstable"
@@ -184,10 +223,10 @@ def _velocityData(self):
         return integrate._integrate(
             filters.butterworth(
                 aData,
-                low_cutoff=self._accel_highpass_cutoff,
-                tukey_percent=self._accel_integral_tukey_percent,
+                low_cutoff=self._params.accel_highpass_cutoff,
+                tukey_percent=self._params.accel_integral_tukey_percent,
             ),
-            zero=self._accel_integral_zero,
+            zero=self._params.accel_integral_zero,
         )
 
     @cached_property
@@ -203,13 +242,20 @@ def _displacementData(self):
         if vData.size == 0:
             return vData
 
-        if not self._accel_highpass_cutoff:
+        if not self._params.accel_highpass_cutoff:
             warnings.warn(
                 "no highpass filter used before integration; "
                 "displacement calculation may be unstable"
             )
 
-        return integrate._integrate(vData)
+        return integrate._integrate(
+            filters.butterworth(
+                vData,
+                low_cutoff=self._params.accel_highpass_cutoff,
+                tukey_percent=self._params.accel_integral_tukey_percent,
+            ),
+            zero=self._params.accel_integral_zero,
+        )
 
     @cached_property
     def _displacementResultantData(self):
@@ -227,11 +273,13 @@ def _PVSSData(self):
             return pvss
 
         freqs = endaq.calc.logfreqs(
-            aData, self._pvss_init_freq, self._pvss_bins_per_octave
+            aData, self._params.pvss_init_freq, self._params.pvss_bins_per_octave
         )
-        freqs = freqs[(freqs >= self._accelerationFs / self._accelerationData.shape[0])]
+        freqs = freqs[
+            (freqs >= 1 / (endaq.calc.sample_spacing(aData) * aData.shape[0]))
+        ]
         pv = shock.shock_spectrum(
-            self._accelerationData,
+            aData,
             freqs,
             damp=0.05,
             mode="pvss",
@@ -248,11 +296,13 @@ def _PVSSResultantData(self):
             return pvss
 
         freqs = endaq.calc.logfreqs(
-            aData, self._pvss_init_freq, self._pvss_bins_per_octave
+            aData, self._params.pvss_init_freq, self._params.pvss_bins_per_octave
         )
-        freqs = freqs[(freqs >= self._accelerationFs / self._accelerationData.shape[0])]
+        freqs = freqs[
+            (freqs >= 1 / (endaq.calc.sample_spacing(aData) * aData.shape[0]))
+        ]
         pv = shock.shock_spectrum(
-            self._accelerationData,
+            aData,
             freqs,
             damp=0.05,
             mode="pvss",
@@ -271,8 +321,8 @@ def _PSDData(self):
 
         return endaq.calc.psd.welch(
             aData,
-            bin_width=self._psd_freq_bin_width,
-            window=self._psd_window,
+            bin_width=self._params.psd_freq_bin_width,
+            window=self._params.psd_window,
             average="median",
         )
 
@@ -291,8 +341,8 @@ def _VCCurveData(self):
 
         return psd.vc_curves(
             self._PSDData,
-            fstart=self._vc_init_freq,
-            octave_bins=self._vc_bins_per_octave,
+            fstart=self._params.vc_init_freq,
+            octave_bins=self._params.vc_bins_per_octave,
         )
 
     @cached_property
@@ -316,11 +366,7 @@ def _pressureData(self):
         except KeyError:
             raise ValueError(f'unknown pressure channel units "{units}"')
 
-        self._preName = ch_struct.channel.name
-        self._preFs = ch_struct.fs
-        data = conversionFactor * ch_struct.to_pandas(time_mode="timedelta")
-
-        return data
+        return conversionFactor * ch_struct.to_pandas(time_mode="timedelta")
 
     @cached_property
     def _temperatureData(self):
@@ -343,14 +389,10 @@ def _temperatureData(self):
         except KeyError:
             raise ValueError(f'unknown temperature channel units "{units}"')
 
-        self._tmpName = ch_struct.channel.name
-        self._tmpFs = ch_struct.fs
-        data = conversionOffset + conversionFactor * ch_struct.to_pandas(
+        return conversionOffset + conversionFactor * ch_struct.to_pandas(
             time_mode="timedelta"
         )
 
-        return data
-
     @cached_property
     def _gyroscopeData(self):
         """Populate the _gyro* fields, including splitting and extending data."""
@@ -362,18 +404,15 @@ def _gyroscopeData(self):
                 columns=pd.Series(["X", "Y", "Z"], name="axis"),
             )
 
-        self._gyroName = ch_struct.channel.name
-        self._gyroFs = ch_struct.fs
-
+        data = ch_struct.to_pandas(time_mode="timedelta")
+        dt = endaq.calc.sample_spacing(data, convert="to_seconds")
         units = ch_struct.units[1]
         if units.lower() == "q":
-            quat_df = ch_struct.to_pandas(time_mode="timedelta")
-            quat_raw = quat_df[["W", "X", "Y", "Z"]].to_numpy()
+            quat_raw = data[["W", "X", "Y", "Z"]].to_numpy()
 
             data = pd.DataFrame(
-                (180 / np.pi)
-                * quat.quat_to_angvel(quat_raw, 1 / self._gyroFs, qaxis=1),
-                index=quat_df.index,
+                (180 / np.pi) * quat.quat_to_angvel(quat_raw, dt, qaxis=1),
+                index=data.index,
                 columns=pd.Series(["X", "Y", "Z"], name="axis"),
             )
 
@@ -396,11 +435,9 @@ def strip_invalid_prefix(data, prefix_len):
                 return data
 
             data = strip_invalid_prefix(
-                data, prefix_len=max(4, int(np.ceil(0.25 * self._gyroFs)))
+                data, prefix_len=max(4, int(np.ceil(0.25 / dt)))
             )
-        elif units.lower() in ("dps", "deg/s"):
-            data = ch_struct.to_pandas(time_mode="timedelta")
-        else:
+        elif units.lower() not in ("dps", "deg/s"):
             raise ValueError(f'unknown gyroscope channel units "{units}"')
 
         return data
@@ -419,13 +456,8 @@ def _gpsPositionData(self):
         if units.lower() != "degrees":
             raise ValueError(f'unknown GPS position channel units "{units}"')
 
-        data = ch_struct.to_pandas(time_mode="timedelta")
-
-        self._gpsName = ch_struct.channel.name
-        self._gpsFs = ch_struct.fs
         # resampling destroys last values -> no resampling
-
-        return data
+        return ch_struct.to_pandas(time_mode="timedelta")
 
     @cached_property
     def _gpsSpeedData(self):
@@ -441,12 +473,7 @@ def _gpsSpeedData(self):
         if units != "m/s":
             raise ValueError(f'unknown GPS ground speed channel units "{units}"')
 
-        data = MPS_TO_KMPH * ch_struct.to_pandas(time_mode="timedelta")
-
-        self._gpsSpeedName = ch_struct.channel.name
-        self._gpsSpeedFs = ch_struct.fs
-
-        return data
+        return MPS_TO_KMPH * ch_struct.to_pandas(time_mode="timedelta")
 
     # ==========================================================================
     # Analyses
diff --git a/endaq/batch/core.py b/endaq/batch/core.py
index ad27da3..427cd27 100644
--- a/endaq/batch/core.py
+++ b/endaq/batch/core.py
@@ -124,7 +124,7 @@ def _make_peak_windows(ch_data_cache, margin_len):
     if df_accel.size == 0:
         return None
 
-    dt = 1 / ch_data_cache._accelerationFs
+    dt = endaq.calc.sample_spacing(df_accel)
 
     data_noidx = df_accel.reset_index(drop=True)
     peak_indices = data_noidx.abs().idxmax(axis="rows")
@@ -247,6 +247,13 @@ def __init__(
         :param accel_end_margin: the numper of samples after which to reject
             recording data; cannot be used in conjunction with
             `accel_end_time`
+        :param accel_integral_tukey_percent: the alpha parameter of a tukey
+            window applied to the acceleration before integrating into
+            velocity & displacement; see the `tukey_percent` parameter in
+            ``endaq.calc.integrate.integrals`` for details
+        :param accel_integral_zero: the output quantity driven to zero when
+            integrating the acceleration into velocity & displacement; see the
+            `zero` parameter in ``endaq.calc.integrate.integrals`` for details
         """
         if accel_start_time is not None and accel_start_margin is not None:
             raise ValueError(
@@ -260,7 +267,6 @@ def __init__(
         self._metrics_queue = {}  # dict maintains insertion order, unlike set
 
         self._ch_data_cache_kwargs = dict(
-            preferred_chs=preferred_chs,
             accel_highpass_cutoff=accel_highpass_cutoff,
             accel_start_time=accel_start_time,
             accel_end_time=accel_end_time,
@@ -269,8 +275,9 @@ def __init__(
             accel_integral_tukey_percent=accel_integral_tukey_percent,
             accel_integral_zero=accel_integral_zero,
         )
+        self._preferred_chs = preferred_chs
 
-        # Even unused parameters MUST be set; used to instantiate `DatasetChannelCache` in `_get_data`
+        # Even unused parameters MUST be set; used to instantiate `CalcCache` in `_get_data`
         self._psd_freq_bin_width = None
         self._psd_freq_start_octave = None
         self._psd_bins_per_octave = None
@@ -388,15 +395,18 @@ def _get_data(self, filename):
 
         data = {}
         with endaq.ide.get_doc(filename) as ds:
-            ch_data_cache = endaq.batch.analyzer.DatasetChannelCache(
+            ch_data_cache = endaq.batch.analyzer.CalcCache.from_ide(
                 ds,
-                **self._ch_data_cache_kwargs,
-                psd_window=self._psd_window,
-                psd_freq_bin_width=self._psd_freq_bin_width,
-                pvss_init_freq=self._pvss_init_freq,
-                pvss_bins_per_octave=self._pvss_bins_per_octave,
-                vc_init_freq=self._vc_init_freq,
-                vc_bins_per_octave=self._vc_bins_per_octave,
+                endaq.batch.analyzer.CalcParams(
+                    **self._ch_data_cache_kwargs,
+                    psd_window=self._psd_window,
+                    psd_freq_bin_width=self._psd_freq_bin_width,
+                    pvss_init_freq=self._pvss_init_freq,
+                    pvss_bins_per_octave=self._pvss_bins_per_octave,
+                    vc_init_freq=self._vc_init_freq,
+                    vc_bins_per_octave=self._vc_bins_per_octave,
+                ),
+                preferred_chs=self._preferred_chs,
             )
 
             data["meta"] = _make_meta(ds)
diff --git a/endaq/calc/filters.py b/endaq/calc/filters.py
index 832816f..62e384a 100644
--- a/endaq/calc/filters.py
+++ b/endaq/calc/filters.py
@@ -30,7 +30,7 @@ def butterworth(
         this value are rejected, and frequencies below this value are preserved
     :param half_order: half of the order of the filter; higher orders provide
         more aggressive stopband reduction
-    :param tukey_percent: the alpha parameter of a preconditioning tukey filter;
+    :param tukey_percent: the alpha parameter of a preconditioning Tukey filter;
         if 0 (default), no filter is applied
     :return: the filtered data
 
diff --git a/endaq/calc/integrate.py b/endaq/calc/integrate.py
index 9467fb6..30daf39 100644
--- a/endaq/calc/integrate.py
+++ b/endaq/calc/integrate.py
@@ -38,7 +38,6 @@ def iter_integrals(
     df: pd.DataFrame,
     zero: Union[typing.Literal["start", "mean", "median"], Callable] = "start",
     highpass_cutoff: Optional[float] = None,
-    filter_half_order: int = 3,
     tukey_percent: float = 0.0,
 ) -> Iterable[pd.DataFrame]:
     """
@@ -51,9 +50,7 @@ def iter_integrals(
         ``-np.median(output)``
     :param highpass_cutoff: the cutoff frequency of a preconditioning highpass
         filter; if None, no filter is applied
-    :param filter_half_order: the half-order of the preconditioning highpass
-        filter, if used
-    :param tukey_percent: the alpha parameter of a preconditioning tukey filter;
+    :param tukey_percent: the alpha parameter of a preconditioning Tukey filter;
         if 0 (default), no filter is applied
     :return: an iterable over the data's successive integrals; the first item
         is the preconditioned input data
@@ -74,7 +71,6 @@ def iter_integrals(
         yield df.copy()  # otherwise, edits to the yielded item would alter the results
         df = filters.butterworth(
             df,
-            half_order=filter_half_order,
             low_cutoff=highpass_cutoff,
             high_cutoff=None,
             tukey_percent=tukey_percent,
@@ -100,7 +96,7 @@ def integrals(
         ``-np.median(output)``
     :param highpass_cutoff: the cutoff frequency for the initial highpass filter;
         this is used to remove artifacts caused by DC trends
-    :param tukey_percent: the alpha parameter of a preconditioning tukey filter;
+    :param tukey_percent: the alpha parameter of a preconditioning Tukey filter;
         if 0 (default), no filter is applied
     :return: a length `n+1` list of the kth-order integrals from 0 to n (inclusive)
 
@@ -124,7 +120,6 @@ def integrals(
                 df,
                 zero=zero,
                 highpass_cutoff=highpass_cutoff,
-                filter_half_order=n // 2 + 1,  # ensures zero DC content in nth integral
                 tukey_percent=tukey_percent,
             ),
         )
diff --git a/endaq/calc/psd.py b/endaq/calc/psd.py
index 36b63a4..acbddc7 100644
--- a/endaq/calc/psd.py
+++ b/endaq/calc/psd.py
@@ -227,7 +227,7 @@ def to_octave(
 
 def loglog_linear_approx(
     df: pd.DataFrame,
-    knots: typing.List[int],
+    knots: typing.List[float],
     window: int = 1,
     freqs_out: typing.Union[typing.Literal["knots", "input"], np.ndarray] = "knots",
 ):
@@ -244,6 +244,15 @@ def loglog_linear_approx(
         - `"knots"` (default): the knot frequencies
         - `"input"`: the frequencies on the input data (i.e., ``df.index``)
     """
+    knots = np.asarray(knots)
+    if not np.all(np.diff(knots) > 0):
+        raise ValueError("knots must be strictly increasing")
+    if np.any(df.index.to_numpy() <= 0):
+        warnings.warn("removed samples at non-positive frequencies from input")
+        df = df.iloc[df.index > 0]
+    if knots[0] <= df.index[0] or df.index[-1] <= knots[-1]:
+        raise ValueError("found knots outside of input data range")
+
     if window != 1:
         df = df.rolling(window).mean().dropna()
 
@@ -318,7 +327,7 @@ def vc_curves(
         df_vel,
         fstart=fstart,  # Hz
         octave_bins=octave_bins,
-        agg=np.sum,
+        agg="sum",
     )
 
     # The PSD must already scale by ∆f -> need only scale by √∆f?
diff --git a/endaq/calc/utils.py b/endaq/calc/utils.py
index 8ac3a1b..87f881d 100644
--- a/endaq/calc/utils.py
+++ b/endaq/calc/utils.py
@@ -12,18 +12,23 @@
 
 
 def sample_spacing(
-    df: pd.DataFrame, convert: typing.Literal[None, "to_seconds"] = "to_seconds"
+    data: Union[np.ndarray, pd.DataFrame],
+    convert: typing.Literal[None, "to_seconds"] = "to_seconds",
 ) -> Union[float, np.timedelta64]:
     """
     Calculate the average spacing between individual samples.
 
     For time indices, this calculates the sampling period `dt`.
 
-    :param df: the input data
+    :param data: the input data; either a pandas DataFrame with the samples
+        spaced along its index, or a 1D-array-like of sample times
     :param convert: if `"to_seconds"` (default), convert any time objects into
         floating-point seconds
     """
-    dt = (df.index[-1] - df.index[0]) / (len(df.index) - 1)
+    if isinstance(data, (pd.DataFrame, pd.Series)):
+        data = data.index
+
+    dt = (data[-1] - data[0]) / (len(data) - 1)
     if convert == "to_seconds" and isinstance(dt, (np.timedelta64, pd.Timedelta)):
         dt = dt / np.timedelta64(1, "s")
 
@@ -131,7 +136,7 @@ def resample(df: pd.DataFrame, sample_rate: Optional[float] = None) -> pd.DataFr
     resampled_data, resampled_time = scipy.signal.resample(
         df,
         num_samples_after_resampling,
-        t=df.index,
+        t=df.index.values.astype('datetime64[s]'),
     )
     resampled_df = pd.DataFrame(
         resampled_data,
diff --git a/endaq/cloud/scripting.py b/endaq/cloud/scripting.py
index 353ad56..571e675 100644
--- a/endaq/cloud/scripting.py
+++ b/endaq/cloud/scripting.py
@@ -3,7 +3,7 @@
 import plotly.io as pio
 import json
 
-from .core import ENV_PRODUCTION, ENV_STAGING, ENV_DEVELOP
+from .core import EndaqCloud, ENV_PRODUCTION, ENV_STAGING, ENV_DEVELOP
 
 
 __all__ = [
@@ -34,8 +34,8 @@ def create_cloud_dashboard_output(name_to_fig: dict) -> str:
     return "[" + ", ".join([v.to_json()[:-1] + ', "title": "' + k + '"}' for k, v in name_to_fig.items()]) + "]"
 
 
-def produce_dashboard_plots(dashboard_script_fn: FunctionType, api_key: str, environment: str = 'production',
-                            display_plots: bool = True) -> list:
+def produce_dashboard_plots(dashboard_script_fn: FunctionType, api_key: str, max_num_files: int = 100,
+                            environment: str = 'production', display_plots: bool = True) -> list:
     """
     A function used to simulate a run of a desired enDAQ Cloud custom report script without needing to use
     cloud.endaq.com
@@ -44,29 +44,34 @@ def produce_dashboard_plots(dashboard_script_fn: FunctionType, api_key: str, env
      the exact code that would be put into a enDAQ Cloud custom report script, followed by one final line:
      `return output`
     :param api_key: The enDAQ Cloud API key
+    :param max_num_files: The maximum number of files to get data about.  Specifically, this is used to
+     specify how many of the most recently uploaded IDE files in the cloud will have their info passed to
+     your custom report script (through a list of json blobs, as parameter 'files')
     :param environment: The version of the enDAQ Cloud to communicate with, the options are 'production', 'staging',
      or 'develop'.  This should only be used internally at Mide
     :param display_plots: If the plots being produced should be displayed
     :return: A list of the 4 plotly figures produced
     """
     if environment == 'production':
-        api_access_url = ENV_PRODUCTION + '/api/v1/'
+        api_access_url = ENV_PRODUCTION
     elif environment == 'staging':
-        api_access_url = ENV_STAGING + '/api/v1/'
+        api_access_url = ENV_STAGING
     elif environment == 'develop':
-        api_access_url = ENV_DEVELOP + '/api/v1/'
+        api_access_url = ENV_DEVELOP
     else:
         raise ValueError("Only 'production', 'staging', and 'develop' may be given for the 'environment' parameter, "
                          f" but {environment} was given instead.")
 
     parameters = {"x-api-key": api_key}
 
-    files = requests.get(api_access_url + "files?limit=100&attributes=all", headers=parameters).json()['data']
+    cloud_obj = EndaqCloud(api_key, env=api_access_url)
+
+    files = cloud_obj._get_files_json_response(limit=max_num_files)
 
     most_recent_file_id = files[-1]['id']
 
     file_download_url = requests.get(
-        api_access_url + "files/download/" + most_recent_file_id,
+        api_access_url + '/api/v1/files/download/' + most_recent_file_id,
         headers=parameters
     ).json()['url']
 
diff --git a/endaq/ide/files.py b/endaq/ide/files.py
index 4c77e55..47d4f3b 100644
--- a/endaq/ide/files.py
+++ b/endaq/ide/files.py
@@ -222,10 +222,11 @@ def get_doc(name=None, filename=None, url=None, parsed=True, start=0, end=None,
 
 def extract_time(doc, out, start=0, end=None, channels=None, **kwargs):
     """
-    Efficiently extract data within a certain interval from an IDE file. Note
-    that due to the way data is stored in an IDE, the exported interval will
-    be slightly wider than the specified start and end times; this ensures
-    the data is copied verbatim and without loss.
+    Efficiently extract data within a certain interval from an IDE file,
+    writing it to another file. Note that due to the way data is stored in an
+    IDE, the exported interval will be slightly wider than the specified
+    start and end times; this ensures the data is copied verbatim and without
+    loss.
 
     The `start` and `end` times, if used, may be specified in several
     ways:
diff --git a/endaq/ide/info.py b/endaq/ide/info.py
index 10b22fc..6328e6a 100644
--- a/endaq/ide/info.py
+++ b/endaq/ide/info.py
@@ -301,12 +301,14 @@ def to_pandas(
 
         :param channel: a `Channel` object, as produced from `Dataset.channels`
             or `endaq.ide.get_channels`
-        :kwarg time_mode: how to temporally index samples; each mode uses either
+        :param time_mode: how to temporally index samples; each mode uses either
             relative times (with respect to the start of the recording) or
             absolute times (i.e., date-times):
-            - "seconds" - a `pandas.Float64Index` of relative timestamps, in seconds
-            - "timedelta" - a `pandas.TimeDeltaIndex` of relative timestamps
-            - "datetime" - a `pandas.DateTimeIndex` of absolute timestamps
+
+            * `"seconds"` - a `pandas.Float64Index` of relative timestamps, in seconds
+            * `"timedelta"` - a `pandas.TimeDeltaIndex` of relative timestamps
+            * `"datetime"` - a `pandas.DateTimeIndex` of absolute timestamps
+
         :return: a `pandas.DataFrame` containing the channel's data
     """
     data = channel.getSession().arraySlice()
@@ -319,7 +321,7 @@ def to_pandas(
         t = t + np.datetime64(channel.dataset.lastUtcTime, "s")
     elif time_mode != "timedelta":
         raise ValueError(f'invalid time mode "{time_mode}"')
-    
+
     if hasattr(channel, "subchannels"):
         columns = [sch.name for sch in channel.subchannels]
     else:
diff --git a/endaq/ide/measurement.py b/endaq/ide/measurement.py
index 09369a2..b2b0333 100644
--- a/endaq/ide/measurement.py
+++ b/endaq/ide/measurement.py
@@ -1,6 +1,28 @@
 """
-Functions for filtering data by measurement types, and singleton objects
-representing different measurement types.
+The module :py:mod:`endaq.ide.measurement` provides an abstract representation of measurement types for
+easily retrieving specific data from ``.ide`` files. Several functions in :py:mod:`endaq.ide` accept
+combinations of ``MeasurementType`` constants for filtering datasets by sensor type.
+
+Measurement types are represented by a set of singleton instances of :py:class:`MeasurementType`.
+
+.. code:: python3
+
+    import endaq.ide
+    from endaq.ide.measurement import *
+
+    doc = endaq.ide.get_doc("https://info.endaq.com/hubfs/data/surgical-instrument.ide")
+    endaq.ide.get_channels(doc, ACCELERATION)
+    endaq.ide.get_channels(doc, TEMPERATURE+PRESSURE)
+
+Strings may also be used, either in combination with or instead of, the :py:class:`MeasurementType` instances.
+The strings can be abbreviated, but no shorter than three characters.
+
+.. code:: python3
+
+    endaq.ide.get_channels(doc, "acceleration")
+    endaq.ide.get_channels(doc, "accel")
+    endaq.ide.get_channels(doc, ACCELERATION+"temp")
+
 """
 
 __all__ = ['ANY', 'ACCELERATION', 'ALTITUDE', 'ANG_RATE', 'AUDIO', 'DIRECTION',
@@ -173,40 +195,62 @@ def match(self, channel):
 ANY = MeasurementType("Any/all", "*", "any", "all",
     doc="Marker object for matching any/all measurement types",
     labels=("*",))
+""" Marker object for matching any/all measurement types """
 
 ACCELERATION = MeasurementType("Acceleration", "acc", "g",
     doc="Marker object for filtering channels with acceleration data",
     labels=())
+""" Marker object for filtering channels with acceleration data """
+
 ORIENTATION = MeasurementType("Orientation", "imu", "qua", "gyr",
     doc="Marker object for filtering channels with rotation/orientation data",
     labels=("quaternion", "euler", "orientation"))
+"Marker object for filtering channels with rotation/orientation data"
+
 ROTATION = MeasurementType("Rotation", "rot", "ang",
     doc="Marker object for filtering channels with angular change rate data",
     labels=("rotation", "gyro"))
+"Marker object for filtering channels with angular change rate data"
+
 AUDIO = MeasurementType("Audio", "mic",
     doc="Marker object for filtering channels with sound level data",
     labels=("mic",))
+"Marker object for filtering channels with sound level data"
+
 LIGHT = MeasurementType("Light", "lux",
     doc="Marker object for filtering channels with light intensity data",
     labels=("lux", "uv"))
+"Marker object for filtering channels with light intensity data"
+
 PRESSURE = MeasurementType("Pressure",
     doc="Marker object for filtering channels with air pressure data",
     labels=())  # pressures
+"Marker object for filtering channels with air pressure data"
+
 TEMPERATURE = MeasurementType("Temperature",
     doc="Marker object for filtering channels with temperature data",
     labels=())  # temperature
+"Marker object for filtering channels with temperature data"
+
 HUMIDITY = MeasurementType("Relative Humidity", "hum",
     doc="Marker object for filtering channels with (relative) humidity data",
     labels=())  # Humidity
+"Marker object for filtering channels with (relative) humidity data"
+
 LOCATION = MeasurementType("Location", "pos", "gps",
     doc="Marker object for filtering channels with location data",
     labels=("pos",))  # GPS
+"Marker object for filtering channels with location data"
+
 SPEED = MeasurementType("Speed",
     doc="Marker object for filtering channels with rate-of-speed data",
     labels=("velocity",))  # GPS Ground Speed
+"Marker object for filtering channels with rate-of-speed data"
+
 TIME = MeasurementType("Time", "epo",
     doc="Marker object for filtering channels with time data",
     labels=("epoch",))  # GPS Epoch Time
+"Marker object for filtering channels with time data"
 
 # For potential future use
 GENERIC = MeasurementType("Generic/Unspecified", "adc", "raw",
diff --git a/endaq/plot/t_sne.py b/endaq/plot/t_sne.py
index 7b2c361..33dfdef 100644
--- a/endaq/plot/t_sne.py
+++ b/endaq/plot/t_sne.py
@@ -82,9 +82,13 @@ def find_optimal_sigmas(distances: np.ndarray, target_perplexity: int, max_itera
     return np.array(sigmas)
 
 
-def get_tsne_plot(df: pd.DataFrame, perplexity: int = 20, random_seed: int = 0, num_iterations: int = 1000,
+def get_tsne_plot(df: pd.DataFrame,
+                  perplexity: int = 20,
+                  random_seed: int = 0,
+                  num_iterations: int = 1000,
                   learning_rate: int = 500,
-                  serials_to_indices: dict = None,
+                  color_col: str = 'serial_number_id',
+                  color_discrete_map: Optional[dict] = None,
                   momentum: int = 0) -> go.Figure:
     """
     Estimates a SNE model, and plots the visualization of the given data.
@@ -100,6 +104,10 @@ def get_tsne_plot(df: pd.DataFrame, perplexity: int = 20, random_seed: int = 0,
     :param random_seed: Integer value to set the seed value for the random
     :param num_iterations: The number of iterations to train for
     :param learning_rate: The rate at which to update teh values in the model
+    :param color_col: The column name in the given dataframe (as merged_df) that is used to color
+     data points with.   This is used in combination with the color_discrete_map parameter
+    :param color_discrete_map: A dictionary which maps the values given to color data points based on (see the
+     color_col parameter description) to the colors that these data points should be
     :param momentum:  The momentum to be used when applying updates to the model
     :return: Plotly figure of the, low-dimensional representation of the given data
     """
@@ -150,9 +158,8 @@ def get_tsne_plot(df: pd.DataFrame, perplexity: int = 20, random_seed: int = 0,
     fig = px.scatter(
         x=Y[:, 0],
         y=Y[:, 1],
-        # color=colors,
-        color=df['serial_number_id'],
-        color_discrete_map=serials_to_indices,
+        color=df[color_col],
+        color_discrete_map=color_discrete_map,
         hover_name=df.index,
     )
 
diff --git a/setup.py b/setup.py
index dd27c3a..c8c398e 100644
--- a/setup.py
+++ b/setup.py
@@ -60,7 +60,7 @@
         project_urls={
             "Bug Tracker": "https://github.com/MideTechnology/endaq-python/issues",
             "Documentation": "https://docs.endaq.com/en/latest/",
-            "Source Code": "https://github.com/MideTechnology/endaq-python",
+            "Source Code": "https://github.com/MideTechnology/endaq-python/tree/main",
             },
         install_requires=INSTALL_REQUIRES,
         extras_require={


Test patch:
diff --git a/tests/batch/test_analyzer.py b/tests/batch/test_analyzer.py
index a84d8e2..b0cfcf5 100644
--- a/tests/batch/test_analyzer.py
+++ b/tests/batch/test_analyzer.py
@@ -5,6 +5,9 @@
 import numpy as np
 import pandas as pd
 import pytest
+import hypothesis as hyp
+import hypothesis.strategies as hyp_st
+import hypothesis.extra.numpy as hyp_np
 
 import endaq.batch.analyzer
 from endaq.calc.stats import rms, L2_norm
@@ -22,15 +25,13 @@ def ide_SSX70065():
 @pytest.fixture()
 def analyzer_raw():
     analyzer_mock = mock.create_autospec(
-        endaq.batch.analyzer.DatasetChannelCache, spec_set=False, instance=True
+        endaq.batch.analyzer.CalcCache, spec_set=False, instance=True
     )
 
     analyzer_mock.MPS2_TO_G = endaq.batch.analyzer.MPS2_TO_G
     analyzer_mock.MPS_TO_MMPS = endaq.batch.analyzer.MPS_TO_MMPS
     analyzer_mock.M_TO_MM = endaq.batch.analyzer.M_TO_MM
-    analyzer_mock.PV_NATURAL_FREQS = (
-        endaq.batch.analyzer.DatasetChannelCache.PV_NATURAL_FREQS
-    )
+    analyzer_mock.PV_NATURAL_FREQS = endaq.batch.analyzer.CalcCache.PV_NATURAL_FREQS
 
     return analyzer_mock
 
@@ -98,10 +99,110 @@ def analyzer_bulk(analyzer_raw):
 
 
 class TestAnalyzer:
+    def test_from_ide_vs_from_literal(self, ide_SSX70065):
+        dataset = ide_SSX70065
+        calc_params = endaq.batch.analyzer.CalcParams(
+            accel_start_time=None,
+            accel_end_time=None,
+            accel_start_margin=None,
+            accel_end_margin=None,
+            accel_highpass_cutoff=1,
+            accel_integral_tukey_percent=0,
+            accel_integral_zero="mean",
+            psd_freq_bin_width=1,
+            psd_window="hann",
+            pvss_init_freq=1,
+            pvss_bins_per_octave=12,
+            vc_init_freq=1,
+            vc_bins_per_octave=3,
+        )
+
+        dataset_cache = endaq.batch.analyzer.CalcCache.from_ide(dataset, calc_params)
+
+        raw_cache = endaq.batch.analyzer.CalcCache.from_raw_data(
+            [
+                (
+                    endaq.ide.to_pandas(dataset.channels[32], time_mode="timedelta"),
+                    ("Acceleration", "g"),
+                ),
+                (
+                    endaq.ide.to_pandas(
+                        dataset.channels[36].subchannels[0], time_mode="timedelta"
+                    ),
+                    ("Pressure", "Pa"),
+                ),
+                (
+                    endaq.ide.to_pandas(
+                        dataset.channels[36].subchannels[1], time_mode="timedelta"
+                    ),
+                    ("Temperature", "°C"),
+                ),
+            ],
+            calc_params,
+        )
+
+        assert set(dataset_cache._channels) == set(raw_cache._channels)
+
+        for (ds_struct, raw_struct) in (
+            (dataset_cache._channels[measure_key], raw_cache._channels[measure_key])
+            for measure_key in dataset_cache._channels
+        ):
+            assert ds_struct.units == raw_struct.units
+            pd.testing.assert_frame_equal(
+                ds_struct.to_pandas(time_mode="timedelta"),
+                raw_struct.to_pandas(time_mode="timedelta"),
+            )
+
+    @hyp.given(
+        df=hyp_np.arrays(
+            elements=hyp_st.floats(-1e7, 1e7),
+            shape=(20, 2),
+            dtype=np.float64,
+        ).map(
+            lambda array: pd.DataFrame(
+                array, index=np.timedelta64(200, "ms") * np.arange(20)
+            )
+        ),
+    )
+    def test_accelerationData(self, df):
+        calc_params = endaq.batch.analyzer.CalcParams(
+            accel_start_time=None,
+            accel_end_time=None,
+            accel_start_margin=None,
+            accel_end_margin=None,
+            accel_highpass_cutoff=1,
+            accel_integral_tukey_percent=0,
+            accel_integral_zero="mean",
+            psd_freq_bin_width=1,
+            psd_window="hann",
+            pvss_init_freq=1,
+            pvss_bins_per_octave=12,
+            vc_init_freq=1,
+            vc_bins_per_octave=3,
+        )
+        data_cache = endaq.batch.analyzer.CalcCache.from_raw_data(
+            [(df, ("Acceleration", "m/s\u00b2"))], calc_params
+        )
+
+        df_accel = endaq.calc.filters.butterworth(
+            df, low_cutoff=calc_params.accel_highpass_cutoff
+        )
+        pd.testing.assert_frame_equal(data_cache._accelerationData, df_accel)
+
+        (_df_accel, df_vel, df_displ) = endaq.calc.integrate.integrals(
+            df_accel,
+            n=2,
+            zero=calc_params.accel_integral_zero,
+            highpass_cutoff=calc_params.accel_highpass_cutoff,
+            tukey_percent=calc_params.accel_integral_tukey_percent,
+        )
+        pd.testing.assert_frame_equal(data_cache._velocityData, df_vel)
+        pd.testing.assert_frame_equal(data_cache._displacementData, df_displ)
+
     def test_accRMSFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.accRMSFull.func(
-            analyzer_bulk
-        )["Resultant"]
+        calc_result = endaq.batch.analyzer.CalcCache.accRMSFull.func(analyzer_bulk)[
+            "Resultant"
+        ]
         expt_result = endaq.batch.analyzer.MPS2_TO_G * rms(
             analyzer_bulk._accelerationData.apply(L2_norm, axis="columns")
         )
@@ -109,27 +210,27 @@ def test_accRMSFull(self, analyzer_bulk):
         assert calc_result == pytest.approx(expt_result)
 
     def test_velRMSFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.velRMSFull.func(
-            analyzer_bulk
-        )["Resultant"]
+        calc_result = endaq.batch.analyzer.CalcCache.velRMSFull.func(analyzer_bulk)[
+            "Resultant"
+        ]
         expt_result = endaq.batch.analyzer.MPS_TO_MMPS * rms(
             analyzer_bulk._velocityData.apply(L2_norm, axis="columns")
         )
         assert calc_result == pytest.approx(expt_result)
 
     def test_disRMSFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.disRMSFull.func(
-            analyzer_bulk
-        )["Resultant"]
+        calc_result = endaq.batch.analyzer.CalcCache.disRMSFull.func(analyzer_bulk)[
+            "Resultant"
+        ]
         expt_result = endaq.batch.analyzer.M_TO_MM * rms(
             analyzer_bulk._displacementData.apply(L2_norm, axis="columns")
         )
         assert calc_result == pytest.approx(expt_result)
 
     def test_accPeakFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.accPeakFull.func(
-            analyzer_bulk
-        )["Resultant"]
+        calc_result = endaq.batch.analyzer.CalcCache.accPeakFull.func(analyzer_bulk)[
+            "Resultant"
+        ]
         expt_result = endaq.batch.analyzer.MPS2_TO_G * rms(
             analyzer_bulk._accelerationData.apply(L2_norm, axis="columns").max()
         )
@@ -149,25 +250,25 @@ def test_gyroRMSFull(self, analyzer_bulk):
         pass
 
     def test_micRMSFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.micRMSFull.func(
-            analyzer_bulk
-        )["Mic"]
+        calc_result = endaq.batch.analyzer.CalcCache.micRMSFull.func(analyzer_bulk)[
+            "Mic"
+        ]
         expt_result = rms(analyzer_bulk._microphoneData)
 
         assert calc_result == pytest.approx(expt_result)
 
     def test_pressFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.pressFull.func(
-            analyzer_bulk
-        )["Control"]
+        calc_result = endaq.batch.analyzer.CalcCache.pressFull.func(analyzer_bulk)[
+            "Control"
+        ]
         expt_result = analyzer_bulk._pressureData.mean()
 
         assert calc_result == pytest.approx(expt_result)
 
     def test_tempFull(self, analyzer_bulk):
-        calc_result = endaq.batch.analyzer.DatasetChannelCache.tempFull.func(
-            analyzer_bulk
-        )["Control"]
+        calc_result = endaq.batch.analyzer.CalcCache.tempFull.func(analyzer_bulk)[
+            "Control"
+        ]
         expt_result = analyzer_bulk._temperatureData.mean()
 
         assert calc_result == pytest.approx(expt_result)
@@ -176,20 +277,23 @@ def test_tempFull(self, analyzer_bulk):
     # Live File Tests
 
     def testLiveFile(self, ide_SSX70065):
-        analyzer = endaq.batch.analyzer.DatasetChannelCache(
+        analyzer = endaq.batch.analyzer.CalcCache.from_ide(
             ide_SSX70065,
-            accel_start_time=None,
-            accel_end_time=None,
-            accel_start_margin=None,
-            accel_end_margin=None,
-            accel_highpass_cutoff=1,
-            accel_integral_tukey_percent=0,
-            accel_integral_zero="mean",
-            psd_freq_bin_width=1,
-            pvss_init_freq=1,
-            pvss_bins_per_octave=12,
-            vc_init_freq=1,
-            vc_bins_per_octave=3,
+            endaq.batch.analyzer.CalcParams(
+                accel_start_time=None,
+                accel_end_time=None,
+                accel_start_margin=None,
+                accel_end_margin=None,
+                accel_highpass_cutoff=1,
+                accel_integral_tukey_percent=0,
+                accel_integral_zero="mean",
+                psd_freq_bin_width=1,
+                psd_window="hann",
+                pvss_init_freq=1,
+                pvss_bins_per_octave=12,
+                vc_init_freq=1,
+                vc_bins_per_octave=3,
+            ),
         )
 
         raw_accel = ide_SSX70065.channels[32].getSession().arrayValues()
@@ -223,20 +327,23 @@ def testLiveFile(self, ide_SSX70065):
     )
     def testLiveFiles12(self, filename):
         ds = idelib.importFile(filename)
-        analyzer = endaq.batch.analyzer.DatasetChannelCache(
+        analyzer = endaq.batch.analyzer.CalcCache.from_ide(
             ds,
-            accel_start_time=None,
-            accel_end_time=None,
-            accel_start_margin=None,
-            accel_end_margin=None,
-            accel_highpass_cutoff=1,
-            accel_integral_tukey_percent=0,
-            accel_integral_zero="mean",
-            psd_freq_bin_width=1,
-            pvss_init_freq=1,
-            pvss_bins_per_octave=12,
-            vc_init_freq=1,
-            vc_bins_per_octave=3,
+            endaq.batch.analyzer.CalcParams(
+                accel_start_time=None,
+                accel_end_time=None,
+                accel_start_margin=None,
+                accel_end_margin=None,
+                accel_highpass_cutoff=1,
+                accel_integral_tukey_percent=0,
+                accel_integral_zero="mean",
+                psd_freq_bin_width=1,
+                psd_window="hann",
+                pvss_init_freq=1,
+                pvss_bins_per_octave=12,
+                vc_init_freq=1,
+                vc_bins_per_octave=3,
+            ),
         )
 
         raw_accel = ds.channels[8].getSession().arrayValues(subchannels=[0, 1, 2])
@@ -274,20 +381,23 @@ def testLiveFiles12(self, filename):
     )
     def testLiveFile3(self, filename):
         ds = idelib.importFile(filename)
-        analyzer = endaq.batch.analyzer.DatasetChannelCache(
+        analyzer = endaq.batch.analyzer.CalcCache.from_ide(
             ds,
-            accel_start_time=None,
-            accel_end_time=None,
-            accel_start_margin=None,
-            accel_end_margin=None,
-            accel_highpass_cutoff=1,
-            accel_integral_tukey_percent=0,
-            accel_integral_zero="mean",
-            psd_freq_bin_width=1,
-            pvss_init_freq=1,
-            pvss_bins_per_octave=12,
-            vc_init_freq=1,
-            vc_bins_per_octave=3,
+            endaq.batch.analyzer.CalcParams(
+                accel_start_time=None,
+                accel_end_time=None,
+                accel_start_margin=None,
+                accel_end_margin=None,
+                accel_highpass_cutoff=1,
+                accel_integral_tukey_percent=0,
+                accel_integral_zero="mean",
+                psd_freq_bin_width=1,
+                psd_window="hann",
+                pvss_init_freq=1,
+                pvss_bins_per_octave=12,
+                vc_init_freq=1,
+                vc_bins_per_octave=3,
+            ),
         )
 
         ch_rot = ds.channels[84]
@@ -306,20 +416,23 @@ def testLiveFile3(self, filename):
     )
     def testLiveFileGPS(self, filename, sample_index):
         ds = idelib.importFile(filename)
-        analyzer = endaq.batch.analyzer.DatasetChannelCache(
+        analyzer = endaq.batch.analyzer.CalcCache.from_ide(
             ds,
-            accel_start_time=None,
-            accel_end_time=None,
-            accel_start_margin=None,
-            accel_end_margin=None,
-            accel_highpass_cutoff=1,
-            accel_integral_tukey_percent=0,
-            accel_integral_zero="mean",
-            psd_freq_bin_width=1,
-            pvss_init_freq=1,
-            pvss_bins_per_octave=12,
-            vc_init_freq=1,
-            vc_bins_per_octave=3,
+            endaq.batch.analyzer.CalcParams(
+                accel_start_time=None,
+                accel_end_time=None,
+                accel_start_margin=None,
+                accel_end_margin=None,
+                accel_highpass_cutoff=1,
+                accel_integral_tukey_percent=0,
+                accel_integral_zero="mean",
+                psd_freq_bin_width=1,
+                psd_window="hann",
+                pvss_init_freq=1,
+                pvss_bins_per_octave=12,
+                vc_init_freq=1,
+                vc_bins_per_octave=3,
+            ),
         )
 
         ch_gps = ds.channels[88]
diff --git a/tests/batch/test_core.py b/tests/batch/test_core.py
index c1bd6c5..01678d1 100644
--- a/tests/batch/test_core.py
+++ b/tests/batch/test_core.py
@@ -53,20 +53,23 @@ def test_make_peak_windows(filename):
         utc_start_time = ds.lastUtcTime
         axis_names = accel_ch.axis_names
 
-        analyzer = endaq.batch.analyzer.DatasetChannelCache(
+        analyzer = endaq.batch.analyzer.CalcCache.from_ide(
             ds,
-            accel_highpass_cutoff=None,
-            accel_start_time=None,
-            accel_end_time=None,
-            accel_start_margin=None,
-            accel_end_margin=None,
-            accel_integral_tukey_percent=0,
-            accel_integral_zero="mean",
-            psd_freq_bin_width=None,
-            pvss_init_freq=None,
-            pvss_bins_per_octave=None,
-            vc_init_freq=None,
-            vc_bins_per_octave=None,
+            endaq.batch.analyzer.CalcParams(
+                accel_highpass_cutoff=None,
+                accel_start_time=None,
+                accel_end_time=None,
+                accel_start_margin=None,
+                accel_end_margin=None,
+                accel_integral_tukey_percent=0,
+                accel_integral_zero="mean",
+                psd_freq_bin_width=None,
+                psd_window="hann",
+                pvss_init_freq=None,
+                pvss_bins_per_octave=None,
+                vc_init_freq=None,
+                vc_bins_per_octave=None,
+            ),
         )
         calc_meta = endaq.batch.core._make_meta(ds)
         calc_peaks = endaq.batch.core._make_peak_windows(analyzer, margin_len=10)
diff --git a/tests/calc/test_integrate.py b/tests/calc/test_integrate.py
index 61a9e86..0a3ddb0 100644
--- a/tests/calc/test_integrate.py
+++ b/tests/calc/test_integrate.py
@@ -1,3 +1,5 @@
+import itertools
+
 import pytest
 import hypothesis as hyp
 import hypothesis.strategies as hyp_st
@@ -50,3 +52,22 @@ def test_integrals():
     assert np.all(calc_result[0] == df)
     for dx_dt, x in zip(calc_result[:-1], calc_result[1:]):
         assert np.all(x == integrate._integrate(dx_dt))
+
+
+@hyp.given(
+    df=hyp_np.arrays(
+        elements=hyp_st.floats(-1e7, 1e7),
+        shape=(20, 2),
+        dtype=np.float64,
+    ).map(lambda array: pd.DataFrame(array, index=0.2 * np.arange(20))),
+    zero=hyp_st.sampled_from(["start", "mean", "median"]),
+    highpass_cutoff=hyp_st.one_of(hyp_st.floats(0.25, 1), hyp_st.just(None)),
+    tukey_percent=hyp_st.floats(0, 0.2),
+)
+def test_integrals_iter_vs_list(df, **kwargs):
+    n = 3
+    result1 = list(itertools.islice(integrate.iter_integrals(df, **kwargs), 2))
+    result2 = integrate.integrals(df, n, **kwargs)
+
+    for r1, r2 in zip(result1, result2):
+        pd.testing.assert_frame_equal(r1, r2)
diff --git a/tests/calc/test_utils.py b/tests/calc/test_utils.py
index 49fd493..60b42af 100644
--- a/tests/calc/test_utils.py
+++ b/tests/calc/test_utils.py
@@ -58,3 +58,12 @@ def test_to_dB_scale(value, reference, squared):
     assert utils.to_dB(10 * value, reference, squared) == pytest.approx(
         scale + utils.to_dB(value, reference, squared)
     )
+
+
+def test_uniform_resample_is_uniform_for_datetime64ns():
+    """
+    Tests if the resample function for np.datetime64[ns] dtype arrays will produce non-uniform timestamps
+    """
+    times = np.arange('2021-02', '2021-03', np.timedelta64(1, 's'), dtype='datetime64[ns]')
+    df = pd.DataFrame(np.arange(len(times)), index=times)
+    assert len(np.unique(np.diff(utils.resample(df).index))) == 1