from abc import abstractmethod
from typing import Any
from pydantic import Field
from evolver.base import BaseConfig, BaseInterface
from evolver.calibration.interface import Calibrator
from evolver.settings import settings
class VialConfigBaseModel(BaseConfig):
vials: list[int] | None = list(range(settings.DEFAULT_NUMBER_OF_VIALS_PER_BOX))
class VialBaseModel(BaseConfig):
vial: int
[docs]
class HardwareDriver(BaseInterface):
class Config(BaseInterface.Config):
calibrator: Calibrator | None = Field(
None, description="The calibrator used to both calibrate and transform Input and/or Output data."
)
[docs]
def __init__(self, *args, evolver=None, **kwargs):
self.evolver = evolver
super().__init__(*args, **kwargs)
class VialHardwareDriver(HardwareDriver):
class Config(VialConfigBaseModel, HardwareDriver.Config): ...
[docs]
class SensorDriver(VialHardwareDriver):
class Config(VialHardwareDriver.Config): ...
class Output(VialBaseModel): ...
[docs]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.outputs: dict[int, self.Output] = {}
[docs]
def get(self) -> dict[int, Output]:
return self.outputs
[docs]
@abstractmethod
def read(self):
"""Communicate with connection to retrieve data. This must return ``self.outputs``.
The implementation is responsible for calling methods of ``self.output_transformer`` as deemed necessary.
"""
pass
[docs]
class EffectorDriver(VialHardwareDriver):
class Config(VialHardwareDriver.Config):
pass
class Input(VialBaseModel): ...
[docs]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.proposal: dict[int, self.Input] = {}
self.committed: dict[int, self.Input] = {}
def _get_input_from_args(self, *args, **kwargs):
if kwargs and args:
raise ValueError("Pass either an input model instance or input fields to set")
if args:
input = args[0]
elif kwargs:
input = kwargs
else:
raise ValueError("No input provided")
return self.Input.model_validate(input)
[docs]
def set(self, *args, **kwargs):
"""Set a value proposal for the hardware.
The value should either be an instance of the hardware Input model, or
fields that will be set on said model. This method proposes individual
values (e.g. for a single vial), which will be committed in bulk when
the commit method is called (done typically in the experiment loop by
the Evolver manager).
In most cases this method need not be overridden, the logic of
performing calibration transform, preparing data packets and
communicating with the underlying hardware is handled in the commit
method.
"""
validated_input = self._get_input_from_args(*args, **kwargs)
self.proposal[validated_input.vial] = validated_input
[docs]
@abstractmethod
def commit(self):
"""Commit all pending proposals to the underlying hardware device.
This method handles the logic of performing calibration transform,
preparing data packets and communicating with the underlying hardware.
It should be implemented by the hardware driver to perform the necessary
actions to commit the proposals to the hardware.
"""
pass
@abstractmethod
def off(self):
"""Immediately turn device into off state.
Used by framework in aborting an experiment. Implementations should
define off condition and implement in such a way that a commit call is
not necessary (i.e. the device turns off upon calling this method).
"""
pass