Creating a Device ================= This tutorial shows how to create a simple `Device` for use in the tickit framework. This device will act as a simple shutter which can vary the transmission of ``flux`` by changing ``position``. Device Module File ------------------ We shall begin by creating a new python module named ``my_shutter.py``, and open it with our preferred editor. This file will be used to store the `Shutter` class which will determine the operation of our device. Device Class ------------ We shall begin by defining the Shutter class which inherits `Device` - by doing so a confiuration dataclass will automatically be created for the device, allowing for easy YAML configuration. .. code-block:: python from tickit.core.device import Device class ShutterDevice(Device): Device Constructor and Configuration ------------------------------------ Next, we shall create the ``__init__`` method, allowing for the device to be instantiated. We will pass two arguments to this method; a required argument, ``default_position``, which will specify the ``target_position`` of the shutter in the absence of any other instruction; and an optional argument, ``initial_position``, which when specified will set the initial ``position`` of the shutter, if unspecified the initial ``position`` will be random. .. code-block:: python from random import random from typing import Optional from tickit.core.device import Device class ShutterDevice(Device): def __init__( self, default_position: float, initial_position: Optional[float] = None ) -> None: self.target_position = default_position self.position = initial_position if initial_position else random() .. note:: Arguments to the ``__init__`` method may be specified in the simulation config file if the device inherits `Device`. Device Logic ------------ The core logic of the device will be implemented in the ``update`` method which recieves the arguments ``time`` - the current simulation time in nanoseconds - and ``inputs`` - a mapping of input ports to their value - and returns an `DeviceUpdate` which consists of ``outputs`` - a mapping of output ports and their value - and ``call_at`` - the time at which the device should next be updated. .. code-block:: python from random import random from typing import Optional from typing_extensions import TypedDict from tickit.core.device import Device, DeviceUpdate from tickit.core.typedefs import SimTime class ShutterDevice(Device): Inputs = TypedDict("Inputs", {"flux": float}) Outputs = TypedDict("Outputs", {"flux": float}) def __init__( self, default_position: float, initial_position: Optional[float] = None ) -> None: self.target_position = default_position self.position = initial_position if initial_position else random() self.rate = 2e-10 self.last_time: Optional[SimTime] = None @staticmethod def move(position: float, target: float, rate: float, period: SimTime) -> float: if position < target: position = min(position + rate * period, target) elif position > target: position = max(position - rate * period, target) return position def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: if self.last_time: self.position = Shutter.move( self.position, self.target_position, self.rate, SimTime(time - self.last_time), ) self.last_time = time call_at = None if self.position == self.target_position else SimTime(time + int(1e8)) output_flux = inputs["flux"] * self.position return DeviceUpdate(Shutter.Outputs(flux=output_flux), call_at) Creating a ComponentConfig -------------------------- In order to run the Device as a simulation, it requires a `ComponentConfig` that knows how to instantiate that Device. This will be defined in the same file as the device, and defines any default initial configuration values. As well as this, we overwrite the magic method `__call__()`, which returns a `DeviceSimulation` object. This object takes the component name, as well as it's device. We will return to this if the device requires any adapters to control it externally (see `Creating an Adapter`). .. code-block:: python from tickit.core.components.component import Component, ComponentConfig from tickit.core.components.device_simulation import DeviceSimulation @dataclass class Shutter(ComponentConfig): default_position: float initial_position: Optional[float] = None def __call__(self) -> Component: return DeviceSimulation( name=self.name, device=ShutterDevice( default_position=self.default_position, initial_position=self.initial_position, ), ) Using the Device ---------------- In order to use the device we must first create a simulation configuration file, we shall create one named ``my_shutter_simulation.yaml``, and open it with our preferred editor. This file will be used to set up a simulation consisting of a `Source` named source which will produce a constant flux, the shutter which will act on the flux as per our implementation, and a `Sink` named sink which will recieve the resulting flux. .. code-block:: yaml - tickit.devices.source.Source: name: source inputs: {} value: 42.0 - examples.devices.shutter.Shutter: name: shutter inputs: flux: source:value default_position: 0.2 initial_position: 0.24 - tickit.devices.sink.Sink: name: sink inputs: flux: shutter:flux .. seealso:: See the `Creating a Simulation` tutorial for a walk-through of creating simulation configurations. Finally, we likely wish to run the simulation, this may be performed by running the following command: .. code-block:: bash python -m tickit all my_shutter_simulation.yaml Once run, we expect to see an output akin to: .. code-block:: bash Doing tick @ 0 source got Input(target='source', time=0, changes=immutables.Map({})) Sourced 42.0 Scheduler got Output(source='source', time=0, changes=immutables.Map({'value': 42.0}), call_in=None) shutter got Input(target='shutter', time=0, changes=immutables.Map({'flux': 42.0})) Scheduler got Output(source='shutter', time=0, changes=immutables.Map({'flux': 10.08}), call_in=100000000) Scheduling Wakeup(component='shutter', when=100000000) sink got Input(target='sink', time=0, changes=immutables.Map({'flux': 10.08})) Sunk {'flux': 10.08} Scheduler got Output(source='sink', time=0, changes=immutables.Map({}), call_in=None) Doing tick @ 100000000 shutter got Input(target='shutter', time=100000000, changes=immutables.Map({})) Scheduler got Output(source='shutter', time=100000000, changes=immutables.Map({}), call_in=100000000) Scheduling Wakeup(component='shutter', when=200000000) sink got Input(target='sink', time=100000000, changes=immutables.Map({})) Sunk {'flux': 10.08} Scheduler got Output(source='sink', time=100000000, changes=immutables.Map({}), call_in=None) Doing tick @ 200000000 shutter got Input(target='shutter', time=200000000, changes=immutables.Map({})) Scheduler got Output(source='shutter', time=200000000, changes=immutables.Map({'flux': 9.24}), call_in=100000000) Scheduling Wakeup(component='shutter', when=300000000) sink got Input(target='sink', time=200000000, changes=immutables.Map({'flux': 9.24})) Sunk {'flux': 9.24} Scheduler got Output(source='sink', time=200000000, changes=immutables.Map({}), call_in=None) Doing tick @ 300000000 shutter got Input(target='shutter', time=300000000, changes=immutables.Map({})) Scheduler got Output(source='shutter', time=300000000, changes=immutables.Map({'flux': 8.4}), call_in=None) sink got Input(target='sink', time=300000000, changes=immutables.Map({'flux': 8.4})) Sunk {'flux': 8.4} Scheduler got Output(source='sink', time=300000000, changes=immutables.Map({}), call_in=None) .. seealso:: See the `Running a Simulation` tutorial for a walk-through of running a simulation in a single or across multiple processes.