diff --git a/doc/pvs.md b/doc/pvs.md index a449281..2f1b197 100644 --- a/doc/pvs.md +++ b/doc/pvs.md @@ -148,9 +148,25 @@ This is `num_periods * num_spectra * num_time_channels * 8 bytes`. These PVs provide the X points (the time-of-flight bin centres) for a time of flight spectrum. This array has length `NUMTIMECHANNELS`. +### `SPEC:::XE` + +These PVs provide the X bin *edges* for a time of flight spectrum. This array has length `NUMTIMECHANNELS + 1`. + ### `SPEC:::Y` -These PVs provide the Y points (the number of counts in the relevant bin) for a time of flight spectrum. This array has length `NUMTIMECHANNELS`. +These PVs provide the normalised Y data (the number of counts in the relevant bin, divided by bin width) for a time of +flight spectrum. This array has length `NUMTIMECHANNELS`. + +### `SPEC:::YC` + +These PVs provide the raw Y data (the number of counts in the relevant bin) for a time of flight spectrum. +This array has length `NUMTIMECHANNELS`. + +::::{important} +`` is {external+ibex_developers_manual:doc}`one-indexed in all user-facing PVs ` +for backwards compatibility, so the PV `SPEC:1:57:X` will return data that was streamed with `period=0` in Kafka. +Asking for period zero, as in a PV like `SPEC:0:57:X`, will return data for the *current* period. +:::: ::: ## Meta-diagnostics diff --git a/src/kafka_dae_diagnostics/data.py b/src/kafka_dae_diagnostics/data.py index cd3d6cc..dfdfdb7 100644 --- a/src/kafka_dae_diagnostics/data.py +++ b/src/kafka_dae_diagnostics/data.py @@ -200,6 +200,11 @@ class Data: Veto names, as a numpy array of strings. """ + current_period: int = 0 + """ + The zero-indexed period into which the most recent data was received. + """ + @property def enabled_vetos_array(self) -> npt.NDArray[np.int32]: """Array describing whether each of the 32 veto bits is currently enabled.""" diff --git a/src/kafka_dae_diagnostics/kafka/handlers.py b/src/kafka_dae_diagnostics/kafka/handlers.py index 637ff71..96bbe74 100644 --- a/src/kafka_dae_diagnostics/kafka/handlers.py +++ b/src/kafka_dae_diagnostics/kafka/handlers.py @@ -160,6 +160,8 @@ def handle_pu00(data: Data, msg: NonEmptyMessage) -> None: data.raw_frames += 1 data.raw_uah += proton_charge + data.current_period = period + try: data.raw_frames_pd[period] += 1 data.raw_uah_pd[period] += proton_charge diff --git a/src/kafka_dae_diagnostics/pvs/spectrum_handlers.py b/src/kafka_dae_diagnostics/pvs/spectrum_handlers.py index 70d71d2..356cc6a 100644 --- a/src/kafka_dae_diagnostics/pvs/spectrum_handlers.py +++ b/src/kafka_dae_diagnostics/pvs/spectrum_handlers.py @@ -29,7 +29,7 @@ def __init__(self, prefix: str, data: Data) -> None: """ self._data = data self._prefix = prefix - self._channel_regex = re.compile(rf"^{re.escape(prefix)}SPEC:(\d+):(\d+):([XY])$") + self._channel_regex = re.compile(rf"^{re.escape(prefix)}SPEC:(\d+):(\d+):(X|Y|XE|YC)$") def testChannel(self, name: str) -> bool | str: """Test whether a channel with the given name can be served by this handler. @@ -41,7 +41,7 @@ def testChannel(self, name: str) -> bool | str: match = self._channel_regex.fullmatch(name) return ( match is not None - and int(match.group(1)) < self._data.num_periods + and int(match.group(1)) <= self._data.num_periods and int(match.group(2)) < self._data.num_spectra ) @@ -57,22 +57,42 @@ def makeChannel(self, name: str, peer: str) -> SharedPV: match = self._channel_regex.fullmatch(name) assert match is not None, "No match in makeChannel after there was a match in testChannel?" - period = int(match.group(1)) + user_period = int(match.group(1)) det = int(match.group(2)) typ = match.group(3) callback_id = f"{name}#{uuid.uuid4()}" def extract_data( - typ: str = typ, period: int = period, det: int = det + typ: str = typ, user_period: int = user_period, det: int = det ) -> npt.NDArray[np.float64]: + + # user_period starts from 1, with 0 corresponding to "current period". + # Internally, the histogram is zero-indexed. + if user_period == 0: + period = self._data.current_period + else: + period = user_period - 1 + match typ: case "Y": + # Counts in each histogram bin, divided by histogram + # bin width. Units: counts/us + return ( + self._data.spectra[period][det] + / (self._data.bin_boundaries[1:] - self._data.bin_boundaries[:-1]) + ).astype(np.float64) + case "YC": + # Raw counts in each histogram bin. Units: counts return self._data.spectra[period][det] case "X": + # Bin-centres along the x (time-of-flight) dimension. Units: ns return ( (self._data.bin_boundaries[1:] + self._data.bin_boundaries[:-1]) / 2 ).astype(np.float64) + case "XE": + # Bin-edges along the x (time-of-flight) dimension. Units: ns + return self._data.bin_boundaries.astype(np.float64) case _: # pragma: no cover (unreachable) raise ValueError(f"Unknown channel type: {typ}") diff --git a/tests/pvs/test_spectrum_handlers.py b/tests/pvs/test_spectrum_handlers.py index 659857a..8774cdd 100644 --- a/tests/pvs/test_spectrum_handlers.py +++ b/tests/pvs/test_spectrum_handlers.py @@ -22,6 +22,8 @@ def handler(data: Data): def test_spectrum_handler_test_channel(handler: SpectrumHandler): assert handler.testChannel("UNITTEST:SPEC:0:0:X") assert handler.testChannel("UNITTEST:SPEC:0:0:Y") + assert handler.testChannel("UNITTEST:SPEC:0:0:XE") + assert handler.testChannel("UNITTEST:SPEC:0:0:YC") assert not handler.testChannel("UNITTEST:SPEC:0:0:") assert not handler.testChannel("UNITTEST:SPEC:0:0:Y2") assert not handler.testChannel("BLAHBLAH:SPEC:0:0:Y") @@ -29,7 +31,10 @@ def test_spectrum_handler_test_channel(handler: SpectrumHandler): assert not handler.testChannel("some_random_pv") -@pytest.mark.parametrize("pvname", ["UNITTEST:SPEC:0:0:X", "UNITTEST:SPEC:0:0:Y"]) +@pytest.mark.parametrize( + "pvname", + ["UNITTEST:SPEC:0:0:X", "UNITTEST:SPEC:0:0:Y", "UNITTEST:SPEC:1:0:XE", "UNITTEST:SPEC:1:0:YC"], +) def test_spectrum_handler_make_channel(handler: SpectrumHandler, pvname: str, data: Data): assert len(data.callbacks) == 0 pv = handler.makeChannel(pvname, "localhost")