Creating a Device#

This tutorial shows how to create a simple amplifier Device for use in the tickit framework. This device will act as a simple amplifier which can multiply the signal received.

Device Class#

We shall begin by defining the amplifier class, which inherits Device.

from tickit.core.device import Device


class AmplifierDevice(Device):

As stated in devices, a new device must have an update method which returns a device update, and must have Input and Output maps as members. As such we shall put in the following boilerplate.

from typing_extensions import TypedDict

from tickit.core.device import Device, DeviceUpdate
from tickit.core.typedefs import SimTime


class AmplifierDevice(Device):

class Inputs(TypedDict):
    ...
class Outputs(TypedDict):
    ...

def __init__(self) -> None:

def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]:

    return DeviceUpdate(self.Outputs(), None)

Device Constructor and Configuration#

Next, we shall populate the __init__ method, allowing for the device to be instantiated. For this amplifier we want an amplification of 2, so we assign that here.

from typing_extensions import TypedDict

from tickit.core.device import Device, DeviceUpdate
from tickit.core.typedefs import SimTime


class AmplifierDevice(Device):

class Inputs(TypedDict):
    ...
class Outputs(TypedDict):
    ...

def __init__(self, initial_amplification: float = 2) -> None:
        self.amplification = initial_amplification

def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]:

    return DeviceUpdate(self.Outputs(), None)

Device Logic#

In order to have a signal to amplifier we will make the following small system where the amplifier receives a signal from a source and outputs it to a sink. The sink and source are already available in tickit.devices.

../../_images/tickit-create-device-amplifier.svg

The core logic of the device will be implemented in the update method, which receives the simulation time and any Inputs into the device. We want to receive an input signal from the source, amplify it, and output it to our sink. As such we define our inputs and outputs in the maps, and the line of logic in the update.

from typing_extensions import TypedDict

from tickit.core.device import Device, DeviceUpdate
from tickit.core.typedefs import SimTime


class AmplifierDevice(Device):

    class Inputs(TypedDict):
        initial_signal: float
    class Outputs(TypedDict):
        amplified_signal: float

    def __init__(self, initial_amplification: float = 2.0) -> None:
        self.amplification = initial_amplification

    def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]:
        amplified_value = inputs["initial_signal"] * self.amplification
        return DeviceUpdate(self.Outputs(amplified_signal=amplified_value), None)

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 DeviceComponent object. This object takes the component name, as well as its device. We will return to this if the device requires any adapters to control it externally.

import pydantic.v1.dataclasses

from tickit.core.components.component import Component, ComponentConfig
from tickit.core.components.device_component import DeviceComponent


@pydantic.v1.dataclasses.dataclass
class Amplifier(ComponentConfig):
    initial_amplification: float

    def __call__(self) -> Component:
        return DeviceComponent(
            name=self.name,
            device=AmplifierDevice(
                initial_amplification=self.initial_amplification,
            ),
        )

Using the Device#

In order to use the device we must first create a simulation configuration yaml file. This file will be used to set up our simulation and will consist of: a Source, named source, which will produce a constant input signal; our amplifier; and a Sink, named sink, which will receive the amplified signal.

- type: tickit.devices.source.Source
  name: source
  inputs: {}
  value: 10.0
- type: amplifier.Amplifier
  name: amp
  inputs:
    initial_signal:
      component: source
      port: value
  initial_amplification: 2.0
- type: tickit.devices.sink.Sink
  name: sink
  inputs:
    input:
      component: amp
      port: amplified_signal

Where in amplifier.Amplifier amplifier is the name of the .py file the amplifier is written in, and Amplifier is the name of the ComponentConfig for the amplifier.

See also

See the Creating a Simulation tutorial for a walk-through of creating simulation configurations.

Finally, to run the simulation:

python -m tickit all amplifier.yaml

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

DEBUG:asyncio:Using selector: EpollSelector
DEBUG:tickit.core.management.ticker:Doing tick @ 0
DEBUG:tickit.core.components.component:source got Input(target='source', time=0, changes=immutables.Map({}))
DEBUG:tickit.devices.source:Sourced 10.0
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='source', time=0, changes=immutables.Map({'value': 10.0}), call_at=None)
DEBUG:tickit.core.components.component:amp got Input(target='amp', time=0, changes=immutables.Map({'initial_signal': 10.0}))
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='amp', time=0, changes=immutables.Map({'amplified_signal': 20.0}), call_at=None)
DEBUG:tickit.core.components.component:sink got Input(target='sink', time=0, changes=immutables.Map({'input': 20.0}))
DEBUG:tickit.devices.sink:Sunk {'input': 20.0}
DEBUG:tickit.core.management.schedulers.base:Scheduler got Output(source='sink', time=0, changes=immutables.Map({}), call_at=None)

We can see that we input a signal of 10, and it is amplified to a signal of 20.

See also

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

Interact with the device#

Now we have a device it is likely we want to interact with it. This can be achieved externally using adapters. A guide on how to do this can be found here.