from annotypes import Anno, add_call_types
from scanpointgenerator import CompoundGenerator
from malcolm.core import APartName, Display, NumberMeta, Part, PartRegistrar, Widget
from ..hooks import (
AExposure,
AGenerator,
ConfigureHook,
ReportStatusHook,
UInfos,
ValidateHook,
)
from ..infos import ExposureDeadtimeInfo, ParameterTweakInfo
from ..util import exposure_attribute
readout_desc = "Subtract this time from frame duration when calculating exposure"
with Anno(readout_desc):
AReadoutTime = float
frequency_accuracy_desc = "In ppm. Subtract duration*this/1e6 when calculating exposure"
with Anno(frequency_accuracy_desc):
AAccuracy = float
with Anno("The minimum exposure time this detector will accept"):
AMinExposure = float
with Anno("Frames per detector step"):
ADetectorFramesPerStep = int
# Pull re-used annotypes into our namespace in case we are subclassed
APartName = APartName
[docs]class ExposureDeadtimePart(Part):
def __init__(
self,
name: APartName,
readout_time: AReadoutTime = 0.0,
frequency_accuracy: AAccuracy = 50.0,
min_exposure: AMinExposure = 0.0,
) -> None:
super().__init__(name)
self.readout_time = NumberMeta(
"float64",
readout_desc,
tags=[Widget.TEXTUPDATE.tag()],
display=Display(precision=6, units="s"),
).create_attribute_model(readout_time)
self.frequency_accuracy = NumberMeta(
"float64",
frequency_accuracy_desc,
tags=[Widget.TEXTUPDATE.tag()],
display=Display(precision=3, units="ppm"),
).create_attribute_model(frequency_accuracy)
self.min_exposure = min_exposure
self.exposure = exposure_attribute(min_exposure)
def setup(self, registrar: PartRegistrar) -> None:
super().setup(registrar)
# Hooks
registrar.hook(ReportStatusHook, self.on_report_status)
registrar.hook(ValidateHook, self.on_validate)
registrar.hook(ConfigureHook, self.on_configure)
# Attributes
registrar.add_attribute_model("readoutTime", self.readout_time)
registrar.add_attribute_model("frequencyAccuracy", self.frequency_accuracy)
registrar.add_attribute_model("exposure", self.exposure)
# Tell the controller to expose some extra configure parameters
registrar.report(ConfigureHook.create_info(self.on_configure))
# Tell the controller to expose some extra validate parameters
registrar.report(ConfigureHook.create_info(self.on_validate))
[docs] @add_call_types
def on_report_status(self) -> UInfos:
# Make an info so we can pass it to the detector
info = ExposureDeadtimeInfo(
self.readout_time.value, self.frequency_accuracy.value, self.min_exposure
)
return info
[docs] @add_call_types
def on_validate(
self,
generator: AGenerator,
exposure: AExposure = 0.0,
frames_per_step: ADetectorFramesPerStep = 1,
) -> UInfos:
info = self.on_report_status()
# Check if we need to calculate a generator duration
if generator.duration == 0.0:
if exposure == 0.0:
# Get minimum exposure time
exposure = info.calculate_exposure(generator.duration, exposure)
# Calculate generator duration
new_duration = info.calculate_minimum_duration(exposure) * frames_per_step
# Add a tiny fractional amount so we don't run into floating point
# comparison issues
new_duration *= 1 + 1e-12
serialized = generator.to_dict()
new_generator = CompoundGenerator.from_dict(serialized)
new_generator.duration = new_duration
self.log.debug(f"{self.name}: tweaking duration to {new_duration}")
# Only tweak the duration for now
return ParameterTweakInfo("generator", new_generator)
else:
# Check if we need to tweak the exposure time
if exposure == 0.0:
new_exposure = info.calculate_exposure(generator.duration, exposure)
self.log.debug(f"{self.name}: tweaking exposure to {new_exposure}")
return ParameterTweakInfo("exposure", new_exposure)
# Otherwise check the provided parameters are compatible
else:
# Check provided exposure against minimum exposure
min_exposure = info.min_exposure
assert (
exposure >= info.min_exposure
), f"{self.name} given exposure {exposure} below min {min_exposure}"
# Check provided exposure against maximum possible exposure
max_exposure = info.calculate_maximum_exposure(generator.duration)
assert exposure <= max_exposure, (
f"{self.name} given exposure {exposure} above max {max_exposure} "
f"based on a duration per frame of {generator.duration}"
)
return None