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.

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.

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.

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).

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.

- 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

See also

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:

python -m tickit all my_shutter_simulation.yaml

Once run, we expect to see an output akin to:

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)

See also

See the Running a Simulation tutorial for a walk-through of running a simulation in a single or across multiple processes.