System Simulation Adapters#

Adapters may also be used with system simulations (SystemComponent), as well as with devices (DeviceComponent). This would allow you, for example, to query what device components are inside the system simulation component, and how they are wired together.

Currently system adapters need both wiring and components. There is a helper base class for implementing this.

class BaseSystemSimulationAdapter:
    """A base for a SystemComponent adapter."""

    _components: Dict[ComponentID, Component]
    _wiring: Union[Wiring, InverseWiring]

    def setup_adapter(
        self,
        components: Dict[ComponentID, Component],
        wiring: Union[Wiring, InverseWiring],
    ) -> None:
        """Provides the components and wiring of a SystemComponent."""
        self._components = components
        self._wiring = wiring

Nested Amplifier Example#

The following system adapter is a simple CommandAdapter. This Can be used to query the SystemComponent for a list of ID’s of the components it contains; and to return the wiring map of the components.

class SystemSimulationAdapter(BaseSystemSimulationAdapter, CommandAdapter):

    _byte_format: ByteFormat = ByteFormat(b"%b\r\n")

    @RegexCommand(r"ids", False, "utf-8")
    async def get_component_ids(self) -> bytes:
        """Returns a list of ids for all the components in the system simulation."""
        return str(self._components.keys()).encode("utf-8")

    @RegexCommand(r"wiring", False, "utf-8")
    async def get_wiring(self) -> bytes:
        """Returns the wiring object used by the nested scheduler."""
        return str(self._wiring).encode("utf-8")

To use this adapter we would need to put it in an adapter container with TcpIo and assign it as an argument in the SystemComponent.

@pydantic.v1.dataclasses.dataclass
class NestedAmplifierWithAdapter(ComponentConfig):
    """Simulation of a nested amplifier with a CommandAdapter."""

    name: ComponentID
    inputs: Dict[PortID, ComponentPort]
    components: List[ComponentConfig]
    expose: Dict[PortID, ComponentPort]

    def __call__(self) -> Component:  # noqa: D102
        return SystemComponent(
            name=self.name,
            components=self.components,
            expose=self.expose,
            adapter=AdapterContainer(
                SystemSimulationAdapter(),
                TcpIo(host="localhost", port=25560),
            ),
        )

This ComponentConfig NestedAmplifierWithAdapter can be used as any other component.

Below is a very simple simulation with a system simulation component. It is a source, which is connected to a SystemComponent which only contains a single amplifier. The output of this amplfier is fed to the output of the system simulation component, which is wired to a sink.

- type: tickit.devices.source.Source
  name: source
  inputs: {}
  value: 10.0
- type: examples.adapters.system_simulation_adapter_config.NestedAmplifierWithAdapter
  name: nested-amp
  inputs:
      input_1:
      component: source
      port: value
  components:
      - type: examples.devices.amplifier.Amplifier
      name: amp
      inputs:
          initial_signal:
          component: external
          port: input_1
      initial_amplification: 2
  expose:
      output_1:
      component: amp
      port: amplified_signal
- type: tickit.devices.sink.Sink
  name: external_sink
  inputs:
      sink_1:
      component: nested-amp
      port: output_1

Interacting with devices using a system simulation adapter#

When using a system adapter you must be careful to achieve the behaviour you desire.

If you wish to write to and change the devices within the system simulation then any change you make must be followed by raising an interrupt in that specific device component. If you do not, the changes will not propagate correctly.

This is done below in the raise_component_interrupt method which takes a given component ID and does await component.raise_interrupt() for the specific component.

class SystemSimulationAdapter(BaseSystemSimulationAdapter, CommandAdapter):

    _byte_format: ByteFormat = ByteFormat(b"%b\r\n")

    @RegexCommand(r"interrupt=(\w+)", False, "utf-8")
        async def raise_component_interrupt(self, id: str) -> bytes:
            """Raises an interrupt in the component of the given id."""
            component = self._components.get(ComponentID(id), None)

            if isinstance(component, BaseComponent):
                await component.raise_interrupt()
                return str(f"Raised Interupt in {component.name}").encode("utf-8")
            else:
                return str("ComponentID not recognised, No interupt raised.").encode(
                    "utf-8"
                )