Repository: legend-exp/pygama (language: python)

Issue:
Update `IntroToDSP.ipynb` tutorial to work with `pygama` 1.1.0
I've updated @iguinn's `IntroToDSP.ipynb` tutorial to work with the current refactored version of `pygama` and added it to the notebooks folder in the sphinx documentation. I've built the docs on my local, and everything works well.

Most of the text is the same as Ian's version --- I updated the JSON config, the various `WaveformBrowser` kwargs being called, use the [legend-testdata-py](https://github.com/legend-exp/legend-testdata-py) package, and added the fix to the current calculation that's mentioned in legend-exp/dspeed#6 so the processors are plotted on the correct timescale.

Let me know if there's anything I should add to it!

I should note this is related to Issue legend-exp/pygama#307.

----

Before submitting a pull request, please make sure you've read and understood the pygama developer's guide: https://pygama.readthedocs.io/en/latest/developer.html. In particular, do not forget to:

- [x] Conform to our **coding conventions**
- [x] Update existing or add new **tests**
- [x] Update existing or add new **documentation**
- [x] Address any issue reported by GitHub checks



Discussion / hints:
Thanks Sam for the work! Here are some suggestions from my side:
* remove "Originally written by [...]"
* remove `prereqs` from JSON DSP config
* set `plt.rcParams["figure.figsize"] = (14, 4); plt.rcParams["figure.facecolor"] = "white"; plt.rcParams["font.size"] = 12` like in the `WaveformBrowser` tutorial: https://github.com/legend-exp/pygama/blob/main/docs/source/notebooks/WaveformBrowser.ipynb
* use `us` units in `WaveformBrowser`'s `x_lim` argument
* I would remove the conclusions, they don't fit pygama's manual imo
* Maybe we want to conclude the tutorial by running `build_dsp()` on the test file and show the ouput file contents?
* Try to use more markdown markup where possible, will look nicer on the website!

@gipert Thanks for the feedback! I've updated the tutorial with your suggestions, and added a bit at the end on using `build_dsp`. I also advertise the other tutorials that exist for more info, so that we don't have the same info in multiple spots.

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d27c7df72..dfb5e14c0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -57,6 +57,9 @@ jobs:
           pytest --cov=pygama --cov-report=xml
       - name: Upload Coverage to codecov.io
         uses: codecov/codecov-action@v3
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          fail_ci_if_error: true
 
   test-docs:
     name: Build documentation


Test patch:
diff --git a/tests/dsp/processors/test_fixed_time_pickoff.py b/tests/dsp/processors/test_fixed_time_pickoff.py
new file mode 100644
index 000000000..0a1bb522c
--- /dev/null
+++ b/tests/dsp/processors/test_fixed_time_pickoff.py
@@ -0,0 +1,116 @@
+import numpy as np
+import pytest
+
+from pygama.dsp.errors import DSPFatal
+from pygama.dsp.processors import fixed_time_pickoff
+
+
+def test_fixed_time_pickoff():
+    """Testing function for the fixed_time_pickoff processor."""
+
+    len_wf = 20
+
+    # test for nan if w_in has a nan
+    w_in = np.ones(len_wf)
+    w_in[4] = np.nan
+    assert np.isnan(fixed_time_pickoff(w_in, 1, ord("i")))
+
+    a_out = np.empty(len_wf)
+    fixed_time_pickoff.__wrapped__(w_in, 1, ord("i"), a_out)
+    assert np.isnan(a_out[0])
+
+    # test for nan if nan is passed to t_in
+    w_in = np.ones(len_wf)
+    assert np.isnan(fixed_time_pickoff(w_in, np.nan, ord("i")))
+
+    a_out = np.empty(len_wf)
+    fixed_time_pickoff.__wrapped__(w_in, np.nan, ord("i"), a_out)
+    assert np.isnan(a_out[0])
+
+    # test for nan if t_in is negative
+    w_in = np.ones(len_wf)
+    assert np.isnan(fixed_time_pickoff(w_in, -1, ord("i")))
+
+    a_out = np.empty(len_wf)
+    fixed_time_pickoff.__wrapped__(w_in, -1, ord("i"), a_out)
+    assert np.isnan(a_out[0])
+
+    # test for nan if t_in is too large
+    w_in = np.ones(len_wf)
+    assert np.isnan(fixed_time_pickoff(w_in, len_wf, ord("i")))
+
+    a_out = np.empty(len_wf)
+    fixed_time_pickoff.__wrapped__(w_in, len_wf, ord("i"), a_out)
+    assert np.isnan(a_out[0])
+
+    # test for DSPFatal errors being raised
+    # noninteger t_in with integer interpolation
+    with pytest.raises(DSPFatal):
+        w_in = np.ones(len_wf)
+        fixed_time_pickoff(w_in, 1.5, ord("i"))
+
+    with pytest.raises(DSPFatal):
+        a_out = np.empty(len_wf)
+        fixed_time_pickoff.__wrapped__(w_in, 1.5, ord("i"), a_out)
+
+    # unsupported mode_in character
+    with pytest.raises(DSPFatal):
+        w_in = np.ones(len_wf)
+        fixed_time_pickoff(w_in, 1.5, ord(" "))
+
+    with pytest.raises(DSPFatal):
+        a_out = np.empty(len_wf)
+        fixed_time_pickoff.__wrapped__(w_in, 1.5, ord(" "), a_out)
+
+    # linear tests
+    w_in = np.arange(len_wf, dtype=float)
+    assert fixed_time_pickoff(w_in, 3, ord("i")) == 3
+
+    a_out = np.empty(len_wf)
+    fixed_time_pickoff.__wrapped__(w_in, 3, ord("i"), a_out)
+    assert a_out[0] == 3
+
+    chars = ["n", "f", "c", "l", "h", "s"]
+    sols = [4, 3, 4, 3.5, 3.5, 3.5]
+
+    for char, sol in zip(chars, sols):
+        assert fixed_time_pickoff(w_in, 3.5, ord(char)) == sol
+
+        a_out = np.empty(len_wf)
+        fixed_time_pickoff.__wrapped__(w_in, 3.5, ord(char), a_out)
+        assert a_out[0] == sol
+
+    # sine wave tests
+    w_in = np.sin(np.arange(len_wf))
+
+    chars = ["n", "f", "c", "l", "h", "s"]
+    sols = [
+        0.1411200080598672,
+        0.1411200080598672,
+        -0.7568024953079282,
+        -0.08336061778208165,
+        -0.09054574599004982,
+        -0.10707938709427486,
+    ]
+
+    for char, sol in zip(chars, sols):
+        assert np.isclose(fixed_time_pickoff(w_in, 3.25, ord(char)), sol)
+
+        a_out = np.empty(len_wf)
+        fixed_time_pickoff.__wrapped__(w_in, 3.25, ord(char), a_out)
+        assert np.isclose(a_out[0], sol)
+
+    # last few corner cases of 'h'
+    w_in = np.sin(np.arange(len_wf))
+    ftps = [0.2, len_wf - 1.8]
+    sols = [
+        0.1806725096462211,
+        -0.6150034250096629,
+    ]
+
+    for ftp, sol in zip(ftps, sols):
+        assert np.isclose(fixed_time_pickoff(w_in, ftp, ord("h")), sol)
+
+        a_out = np.empty(len_wf)
+        fixed_time_pickoff.__wrapped__(w_in, ftp, ord("h"), a_out)
+        assert np.isclose(a_out[0], sol)