pybrid-computing 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pybrid/cli/redac/__init__.py +356 -0
- pybrid/redac/__init__.py +6 -0
- pybrid/redac/blocks/__init__.py +9 -0
- pybrid/redac/blocks/block.py +61 -0
- pybrid/redac/blocks/cblock.py +22 -0
- pybrid/redac/blocks/iblock.py +38 -0
- pybrid/redac/blocks/mblock.py +37 -0
- pybrid/redac/blocks/ublock.py +47 -0
- pybrid/redac/carrier.py +43 -0
- pybrid/redac/cluster.py +83 -0
- pybrid/redac/computations.py +30 -0
- pybrid/redac/computer.py +42 -0
- pybrid/redac/controller.py +141 -0
- pybrid/redac/data.py +64 -0
- pybrid/redac/display.py +25 -0
- pybrid/redac/elements.py +28 -0
- pybrid/redac/entities.py +200 -0
- pybrid/redac/modules.py +15 -0
- pybrid/redac/protocol/__init__.py +20 -0
- pybrid/redac/protocol/envelope.py +62 -0
- pybrid/redac/protocol/messages.py +726 -0
- pybrid/redac/protocol/protocol.py +180 -0
- pybrid/redac/protocol/serializer.py +52 -0
- pybrid/redac/protocol/types.py +17 -0
- pybrid/redac/run.py +131 -0
- pybrid_computing-0.10.0.dist-info/METADATA +136 -0
- pybrid_computing-0.10.0.dist-info/RECORD +28 -0
- pybrid_computing-0.10.0.dist-info/WHEEL +4 -0
pybrid/redac/cluster.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .entities import Entity, Path, EntityType, EntityClass
|
|
9
|
+
from .blocks import FunctionBlock, MBlock, UBlock, CBlock, IBlock
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(kw_only=True)
|
|
13
|
+
class Cluster(Entity):
|
|
14
|
+
"""
|
|
15
|
+
A REDAC computation cluster.
|
|
16
|
+
|
|
17
|
+
The cluster is the smallest unit capable of an analog computation.
|
|
18
|
+
It always consists of two optional :class:`.blocks.MBlock` objects
|
|
19
|
+
and one mandatory :class:`.blocks.UBlock`, :class:`.blocks.CBlock` and :class:`.blocks.IBlock` each.
|
|
20
|
+
"""
|
|
21
|
+
#: The first :class:`.blocks.MBlock` in this cluster. May be ``None`` if the slot is not filled.
|
|
22
|
+
m0block: typing.Optional[MBlock]
|
|
23
|
+
#: The second :class:`.blocks.MBlock` in this cluster. May be ``None`` if the slot is not filled.
|
|
24
|
+
m1block: typing.Optional[MBlock]
|
|
25
|
+
#: The :class:`.blocks.UBlock` in this cluster.
|
|
26
|
+
ublock: UBlock
|
|
27
|
+
#: The :class:`.blocks.CBlock` in this cluster.
|
|
28
|
+
cblock: CBlock
|
|
29
|
+
#: The :class:`.blocks.IBlock` in this cluster.
|
|
30
|
+
iblock: IBlock
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def children(self):
|
|
34
|
+
"""
|
|
35
|
+
Generator iterating through child entities of type :class:`.blocks.FunctionBlock`.
|
|
36
|
+
Only returns blocks that are actually present (i.e. not ``None``).
|
|
37
|
+
"""
|
|
38
|
+
yield from (block for block in self.blocks if block is not None)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def blocks(self) -> tuple[typing.Optional[MBlock], typing.Optional[MBlock], UBlock, CBlock, IBlock]:
|
|
42
|
+
"""
|
|
43
|
+
List of :class:`.blocks.FunctionBlock` objects in this cluster.
|
|
44
|
+
Returns ``None`` elements for blocks that are not present.
|
|
45
|
+
"""
|
|
46
|
+
return self.m0block, self.m1block, self.ublock, self.cblock, self.iblock
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def create_from_entity_type_tree(cls, path, tree):
|
|
50
|
+
# TODO: Refactor out common code
|
|
51
|
+
# Check information on self
|
|
52
|
+
this_entity_type = EntityType.pop_from_dict(tree)
|
|
53
|
+
assert this_entity_type.class_ is EntityClass.CLUSTER
|
|
54
|
+
|
|
55
|
+
# Generate child entities
|
|
56
|
+
blocks = []
|
|
57
|
+
for sub_path, sub_tree in tree.items():
|
|
58
|
+
if not sub_path.startswith('/'):
|
|
59
|
+
raise ValueError('Unexpected entities tree element. Expected only sub-paths to be left.')
|
|
60
|
+
path_ = path / Path((sub_path.removeprefix('/'),))
|
|
61
|
+
block = FunctionBlock.create_from_entity_type_tree(path_, sub_tree)
|
|
62
|
+
blocks.append(block)
|
|
63
|
+
|
|
64
|
+
# TODO: Less hard-coding :)
|
|
65
|
+
return cls(path=path, m0block=blocks[0], m1block=blocks[1], ublock=blocks[2], cblock=blocks[3],
|
|
66
|
+
iblock=blocks[4])
|
|
67
|
+
|
|
68
|
+
def route(self, m_out: int, u_out: int, c_factor: float, m_in: int):
|
|
69
|
+
"""
|
|
70
|
+
Convenience function to connect a signal from before the :class:`.blocks.UBlock` through a coefficient
|
|
71
|
+
on the :class:`.blocks.CBlock` and through the :class:`.blocks.IBlock` to an input on one of the
|
|
72
|
+
:class:`.blocks.MBlock` slots.
|
|
73
|
+
|
|
74
|
+
:param int m_out: Output index from one of the MBlocks, respectively input index of the UBlock.
|
|
75
|
+
:param int u_out: Output index of the UBlock, respectively index of the coefficient.
|
|
76
|
+
:param float c_factor: Factor of the coefficient.
|
|
77
|
+
:param int m_in: Input index of one of the MBlocks, respectively output index of the IBlock.
|
|
78
|
+
:return: ``None``
|
|
79
|
+
"""
|
|
80
|
+
# TODO: Sanity checks and error handling
|
|
81
|
+
self.ublock.connect(m_out, u_out)
|
|
82
|
+
self.cblock.elements[u_out].factor = c_factor
|
|
83
|
+
self.iblock.connect(u_out, m_in)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from pybrid.base.analog import Integration as BaseIntegration
|
|
8
|
+
from pybrid.base.analog import ScalarMultiplication as BaseScalarMultiplication, \
|
|
9
|
+
ScalarMultiplicationFactor
|
|
10
|
+
from pybrid.base.analog import Multiplication # noqa
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(kw_only=True)
|
|
14
|
+
class Integration(BaseIntegration):
|
|
15
|
+
#: Initial value. Must be in range [-1.0, 1.0].
|
|
16
|
+
ic: float = 0.0
|
|
17
|
+
#: Time constant in :math:`\frac{1}{\mathrm{s}}`. Must be one of {100, 10000}.
|
|
18
|
+
k: int = 10_000
|
|
19
|
+
|
|
20
|
+
# Inherit __doc__
|
|
21
|
+
__doc__ = BaseIntegration.__doc__
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(kw_only=True)
|
|
25
|
+
class ScalarMultiplication(BaseScalarMultiplication):
|
|
26
|
+
#: Scalar factor α. Must be in range [-20.0, 20.0].
|
|
27
|
+
factor: float = field(default=ScalarMultiplicationFactor(min=-20.0, max=+20.0, default=1.0))
|
|
28
|
+
|
|
29
|
+
# Inherit __doc__
|
|
30
|
+
__doc__ = BaseScalarMultiplication.__doc__
|
pybrid/redac/computer.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
from pybrid.base.hybrid import AnalogComputer
|
|
6
|
+
|
|
7
|
+
from .blocks import FunctionBlock
|
|
8
|
+
from .carrier import Carrier
|
|
9
|
+
from .cluster import Cluster
|
|
10
|
+
from .elements import ComputationElement
|
|
11
|
+
from .entities import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class REDAC(AnalogComputer):
|
|
15
|
+
"""
|
|
16
|
+
Representation of the REDAC analog computer and its structure.
|
|
17
|
+
"""
|
|
18
|
+
hierarchy = (Carrier, Cluster, FunctionBlock, ComputationElement)
|
|
19
|
+
entities: list[Carrier]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "REDAC"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def carriers(self) -> list[Carrier]:
|
|
27
|
+
"""The list of :class:`.Carrier` boards in this REDAC."""
|
|
28
|
+
return self.entities
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def create_from_entity_type_tree(cls, type_tree):
|
|
32
|
+
carriers = []
|
|
33
|
+
for sub_path, sub_tree in type_tree.items():
|
|
34
|
+
carrier = Carrier.create_from_entity_type_tree(Path((sub_path,)), sub_tree)
|
|
35
|
+
carriers.append(carrier)
|
|
36
|
+
return cls(entities=carriers)
|
|
37
|
+
|
|
38
|
+
def __repr__(self):
|
|
39
|
+
return repr(self.entities)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import typing
|
|
8
|
+
from asyncio import Future
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from pybrid.base.hybrid import BaseController
|
|
12
|
+
|
|
13
|
+
from .computer import REDAC
|
|
14
|
+
from .entities import Entity, Path
|
|
15
|
+
from .protocol.messages import RunStateChangeMessage, RunDataMessage
|
|
16
|
+
from .protocol.protocol import Protocol
|
|
17
|
+
from .run import Run, RunState
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Controller(BaseController):
|
|
23
|
+
"""
|
|
24
|
+
Abstraction of the REDAC hybrid controller.
|
|
25
|
+
|
|
26
|
+
The hybrid controller is an interface to all relevant functions to configure and control the REDAC.
|
|
27
|
+
It also collects all :class:`Run` instances started with it.
|
|
28
|
+
|
|
29
|
+
The controller object also holds references to the underlying protocol and transport objects and manages them.
|
|
30
|
+
"""
|
|
31
|
+
computer: REDAC
|
|
32
|
+
protocol: Protocol
|
|
33
|
+
#: List of all runs started by this controller.
|
|
34
|
+
runs: dict[UUID, Run] = dict()
|
|
35
|
+
_ongoing_runs: dict[UUID, Future] = dict()
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_run_implementation(cls) -> typing.Type[Run]:
|
|
39
|
+
"""Returns the specific :class:`.Run` implementation used by the REDAC."""
|
|
40
|
+
return Run
|
|
41
|
+
|
|
42
|
+
async def start(self) -> None:
|
|
43
|
+
await super().start()
|
|
44
|
+
self.protocol.register_callback(RunStateChangeMessage, self.handle_run_state_change)
|
|
45
|
+
self.protocol.register_callback(RunDataMessage, self.handle_run_data)
|
|
46
|
+
|
|
47
|
+
def handle_run_state_change(self, msg: RunStateChangeMessage):
|
|
48
|
+
"""A handler for incoming :class:`.RunStateChangeMessage` messages."""
|
|
49
|
+
logger.debug("Received run state change: %s.", msg)
|
|
50
|
+
if run := self.runs.get(msg.id, None):
|
|
51
|
+
run.state = RunState(msg.new)
|
|
52
|
+
if run.state.is_done():
|
|
53
|
+
self._ongoing_runs.pop(run.id_).set_result(run)
|
|
54
|
+
else:
|
|
55
|
+
logger.warning("Received run state change with unknown id %s.", msg.id)
|
|
56
|
+
|
|
57
|
+
def handle_run_data(self, msg: RunDataMessage):
|
|
58
|
+
"""A handler for incoming :class:`.RunDataMessage` messages."""
|
|
59
|
+
if run := self.runs.get(msg.id, None):
|
|
60
|
+
adc_paths = [Path(msg.entity).join(f"ADC{idx}") for idx in range(run.daq.num_channels)]
|
|
61
|
+
for data_pkg in msg.data:
|
|
62
|
+
for channel, data_point in zip(adc_paths, data_pkg):
|
|
63
|
+
run.data[channel].append(data_point)
|
|
64
|
+
last_t = len(run.data["t"])
|
|
65
|
+
run.data["t"].extend(range(last_t, last_t + len(msg.data)))
|
|
66
|
+
|
|
67
|
+
# ██████ ██████ ███ ███ ███ ███ █████ ███ ██ ██████ ███████
|
|
68
|
+
# ██ ██ ██ ████ ████ ████ ████ ██ ██ ████ ██ ██ ██ ██
|
|
69
|
+
# ██ ██ ██ ██ ████ ██ ██ ████ ██ ███████ ██ ██ ██ ██ ██ ███████
|
|
70
|
+
# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
71
|
+
# ██████ ██████ ██ ██ ██ ██ ██ ██ ██ ████ ██████ ███████
|
|
72
|
+
|
|
73
|
+
async def hack(self, cmd: str, data: typing.Any) -> typing.Any:
|
|
74
|
+
"""
|
|
75
|
+
Send the passed data as a 'hack' request, only used during development.
|
|
76
|
+
It allows to pass and receive arbitrary data to and from the hybrid controller.
|
|
77
|
+
"""
|
|
78
|
+
return await self.protocol.hack_request(cmd, data)
|
|
79
|
+
|
|
80
|
+
async def get_computer(self) -> REDAC:
|
|
81
|
+
"""
|
|
82
|
+
Retrieve the current hardware configuration of the REDAC.
|
|
83
|
+
"""
|
|
84
|
+
entities = await self.protocol.get_entities()
|
|
85
|
+
computer = REDAC.create_from_entity_type_tree(entities)
|
|
86
|
+
return computer
|
|
87
|
+
|
|
88
|
+
async def set_computer(self, computer: REDAC):
|
|
89
|
+
"""
|
|
90
|
+
Change the configuration of all carrier boards and sub-entities on the REDAC.
|
|
91
|
+
|
|
92
|
+
:param computer: The :class:`.REDAC` object containing the configuration to be set.
|
|
93
|
+
:return: None
|
|
94
|
+
"""
|
|
95
|
+
for carrier in computer.carriers:
|
|
96
|
+
await self.set_config(carrier)
|
|
97
|
+
|
|
98
|
+
async def start_run(self, run: typing.Optional[Run] = None) -> Future:
|
|
99
|
+
"""
|
|
100
|
+
Start a run (computation) on the REDAC.
|
|
101
|
+
|
|
102
|
+
:param run: The :class:`.Run` to be started, including its configuration. If None, a new run is created.
|
|
103
|
+
:return: An :class:`asyncio.Future` which can be awaited and will return the run object once it is done.
|
|
104
|
+
"""
|
|
105
|
+
if run is None:
|
|
106
|
+
run = await self.create_run()
|
|
107
|
+
self.runs[run.id_] = run
|
|
108
|
+
self._ongoing_runs[run.id_] = run_future = asyncio.get_event_loop().create_future()
|
|
109
|
+
await self.protocol.start_run_request(run.id_, run.config, run.daq)
|
|
110
|
+
return run_future
|
|
111
|
+
|
|
112
|
+
async def start_and_await_run(self, run: typing.Optional[Run] = None, timeout=5) -> Run:
|
|
113
|
+
"""
|
|
114
|
+
A convenience function which starts a run, blocks until it is completed and returns it.
|
|
115
|
+
|
|
116
|
+
:param run: The :class:`.Run` to be started, including its configuration. If None, a new run is created.
|
|
117
|
+
:param timeout: Timeout
|
|
118
|
+
:return: The completed :class:`.Run`.
|
|
119
|
+
"""
|
|
120
|
+
run_future = await self.start_run(run)
|
|
121
|
+
await asyncio.wait_for(run_future, timeout=timeout)
|
|
122
|
+
return run_future.result()
|
|
123
|
+
|
|
124
|
+
async def set_config(self, entity: Entity):
|
|
125
|
+
"""
|
|
126
|
+
Change the configuration of a singe entity.
|
|
127
|
+
|
|
128
|
+
:param entity: The entity to change.
|
|
129
|
+
:return: None
|
|
130
|
+
"""
|
|
131
|
+
await self.protocol.set_config(entity)
|
|
132
|
+
|
|
133
|
+
async def reset(self, keep_calibration: bool = True, sync: bool = True):
|
|
134
|
+
"""
|
|
135
|
+
Reset the hybrid controller and the analog computer to its initial configuration.
|
|
136
|
+
|
|
137
|
+
:param keep_calibration: Whether to keep the calibration.
|
|
138
|
+
:param sync: Whether to write the reset values to the hardware.
|
|
139
|
+
:return: None
|
|
140
|
+
"""
|
|
141
|
+
await self.protocol.reset(keep_calibration=keep_calibration, sync=sync)
|
pybrid/redac/data.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
# TODO: Refactor out common code with Model-1 data exporters and make abstraction for command line
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
from pybrid.redac.run import Run
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# TODO: Implement a lookup for different data types
|
|
14
|
+
# _REGISTRY = {}
|
|
15
|
+
# def get_exporter(format):
|
|
16
|
+
# return _REGISTRY[format]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseExporter:
|
|
20
|
+
FORMAT = None
|
|
21
|
+
|
|
22
|
+
def export(self, run, **kwargs):
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DatExporter(BaseExporter):
|
|
27
|
+
FORMAT = "dat"
|
|
28
|
+
|
|
29
|
+
def __init__(self, file: typing.IO):
|
|
30
|
+
self._file = file
|
|
31
|
+
|
|
32
|
+
def _write_line(self, line=""):
|
|
33
|
+
self._file.write(line)
|
|
34
|
+
self._file.write("\n")
|
|
35
|
+
|
|
36
|
+
def _write_header_line(self, key, value):
|
|
37
|
+
self._write_line(f"# {key} = {value}")
|
|
38
|
+
|
|
39
|
+
def _write_header(self, run):
|
|
40
|
+
self._write_line("# Run result")
|
|
41
|
+
for key in ("id_", "created"):
|
|
42
|
+
self._write_header_line(key, getattr(run, key))
|
|
43
|
+
for flag in dataclasses.fields(run.flags):
|
|
44
|
+
flag_set = "Yes" if getattr(run.flags, flag.name) else "No"
|
|
45
|
+
self._write_header_line("flag " + flag.name, flag_set)
|
|
46
|
+
|
|
47
|
+
def _write_data_line(self, idx, data_pkg):
|
|
48
|
+
self._write_line(str(idx) + "\t" + "\t".join(map(str, data_pkg)))
|
|
49
|
+
|
|
50
|
+
def _write_data(self, run: Run):
|
|
51
|
+
if not run.data:
|
|
52
|
+
self._write_line("# No data.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
data_header = "# idx\t" + "\t".join(map(str, run.data.keys()))
|
|
56
|
+
self._write_line(data_header)
|
|
57
|
+
|
|
58
|
+
for idx, data_pkg in enumerate(zip(*run.data.values())):
|
|
59
|
+
self._write_data_line(idx, data_pkg)
|
|
60
|
+
|
|
61
|
+
def export(self, run):
|
|
62
|
+
self._write_header(run)
|
|
63
|
+
self._write_line("#")
|
|
64
|
+
self._write_data(run)
|
pybrid/redac/display.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
from .computer import REDAC
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TreeDisplay:
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def render(redac: REDAC):
|
|
12
|
+
buffer = ""
|
|
13
|
+
buffer += "REDAC Analag Computer\n"
|
|
14
|
+
|
|
15
|
+
# TODO: Do it better :)
|
|
16
|
+
for carrier in redac.carriers:
|
|
17
|
+
buffer += "├── " + carrier.__class__.__name__ + " @ " + str(carrier.path) + "\n"
|
|
18
|
+
for cluster in carrier.children:
|
|
19
|
+
buffer += "│ ├── " + cluster.__class__.__name__ + " @ " + str(cluster.path) + "\n"
|
|
20
|
+
for block in cluster.children:
|
|
21
|
+
buffer += "│ │ ├── " + block.__class__.__name__ + " @ " + str(block.path) + "\n"
|
|
22
|
+
for element in block.children:
|
|
23
|
+
buffer += "│ │ │ ├── " + element.__class__.__name__ + " @ " + str(element.path) + "\n"
|
|
24
|
+
|
|
25
|
+
return buffer
|
pybrid/redac/elements.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
from pybrid.base.hybrid import ComputationElement as BaseComputationElement
|
|
6
|
+
|
|
7
|
+
from .entities import Entity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComputationElement(BaseComputationElement, Entity):
|
|
11
|
+
"""
|
|
12
|
+
A REDAC computation element (a function).
|
|
13
|
+
|
|
14
|
+
Each computation element implements one of the available
|
|
15
|
+
analog computations in :doc:`configurations`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def generate_partial_configuration(self, attribute):
|
|
19
|
+
if self.computation_class.__dataclass_fields__.get(attribute, None):
|
|
20
|
+
return {attribute: getattr(self.computation, attribute)}
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError("Unknown attribute %s for %s." % (attribute, self.__class__))
|
|
23
|
+
|
|
24
|
+
def apply_partial_configuration(self, attribute, value):
|
|
25
|
+
if field := self.computation_class.__dataclass_fields__.get(attribute, None):
|
|
26
|
+
setattr(self.computation, attribute, field.type(value))
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError("Unknown attribute %s for %s." % (attribute, self.__class__))
|
pybrid/redac/entities.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
The REDAC analog computer consists of a hierarchical structure of hardware modules.
|
|
7
|
+
|
|
8
|
+
This structure is represented by a tree-like structure of :class:`Entity` objects and their sub-classes.
|
|
9
|
+
Each entity has a unique :class:`Path` defining its position in this hierarchy.
|
|
10
|
+
You can iterate over the children of an entity via its :attr:`Entity.children` property.
|
|
11
|
+
As described in :class:`Path`, the hierarchy represented is as follows.
|
|
12
|
+
|
|
13
|
+
#. Carrier boards implemented by :class:`pybrid.redac.carrier.Carrier`
|
|
14
|
+
#. Clusters implemented by :class:`pybrid.redac.cluster.Cluster`
|
|
15
|
+
#. Function blocks implemented by :class:`pybrid.redac.blocks.FunctionBlock`, see :doc:`configurations`
|
|
16
|
+
#. Functions (Elements) implemented by :class:`pybrid.redac.elements.ComputationElement`
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, fields, replace
|
|
20
|
+
from enum import Enum
|
|
21
|
+
import typing
|
|
22
|
+
|
|
23
|
+
from pybrid.base.hybrid import Path as BasePath
|
|
24
|
+
from pybrid.base.hybrid import Entity as BaseEntity
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnknownEntityTypeError(ValueError):
|
|
28
|
+
"""Exception thrown when trying to get an unknown :class:`EntityType` instance."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EntityTypeRegistryError(ValueError):
|
|
33
|
+
"""Exception for errors inside the :class:`EntityType` registry."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EntityClass(Enum):
|
|
38
|
+
"""Entity class differentiates between carrier boards, different function blocks and so on. Max 5bit = 31."""
|
|
39
|
+
CARRIER = 0
|
|
40
|
+
CLUSTER = 1 # mostly unused
|
|
41
|
+
MBLOCK = 2
|
|
42
|
+
UBLOCK = 3
|
|
43
|
+
CBLOCK = 4
|
|
44
|
+
IBLOCK = 5
|
|
45
|
+
OTHER = 31
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_ENTITY_TYPE_REGISTRY: dict["EntityType", object] = dict()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(kw_only=True, eq=True, frozen=True)
|
|
52
|
+
class EntityType:
|
|
53
|
+
"""
|
|
54
|
+
A unique identifier for an entity type.
|
|
55
|
+
|
|
56
|
+
Each hardware module in a REDAC analog computer contains hardware version information
|
|
57
|
+
in their EEPROM. When the client library connects to an analog computer,
|
|
58
|
+
it generally requests the tree of hardware modules present.
|
|
59
|
+
This is given as a tree-like list of :class:`EntityType` objects.
|
|
60
|
+
Since it is also automatically converted to the respective python objects,
|
|
61
|
+
you typically do not need to handle :class:`EntityType` objects directly.
|
|
62
|
+
|
|
63
|
+
One notable exception is registering a python class for the auto-conversion
|
|
64
|
+
from an :class:`EntityType` to its respective :class:`Entity` class as follows.
|
|
65
|
+
|
|
66
|
+
.. code-block::
|
|
67
|
+
|
|
68
|
+
@EntityType.register(EntityClass.MBLOCK, 17, 3, 1)
|
|
69
|
+
class ACustomMBlock(ElementBlock):
|
|
70
|
+
...
|
|
71
|
+
"""
|
|
72
|
+
#: The class of the entity, see :class:`EntityClass`.
|
|
73
|
+
#: Different classes of entities can only be placed at their expected slots and can not be interchanged.
|
|
74
|
+
class_: EntityClass
|
|
75
|
+
#: The type of the entity, mostly relevant for :class:`pybrid.redac.blocks.MBlock`.
|
|
76
|
+
#: Different types of an entity have significantly different functionality, but may be placed in the same slots.
|
|
77
|
+
type_: typing.Optional[int] = None
|
|
78
|
+
#: The variant of an entity.
|
|
79
|
+
#: Different variants of an entity have similar functionality, but may differ in certain implementation details.
|
|
80
|
+
variant: typing.Optional[int] = None
|
|
81
|
+
#: The version on an entity.
|
|
82
|
+
#: Different versions of an entity are basically identical, but may contain different hardware elements or bugfixes.
|
|
83
|
+
version: typing.Optional[int] = None
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def pop_from_dict(cls, d):
|
|
87
|
+
return cls(class_=EntityClass(d.pop("class")), type_=d.pop("type"), variant=d.pop("variant"),
|
|
88
|
+
version=d.pop("version"))
|
|
89
|
+
|
|
90
|
+
def fallback_type(self):
|
|
91
|
+
"""Return a copy of this :class:`EntityType` with one more field set to None."""
|
|
92
|
+
for field in reversed(fields(self)[1:]):
|
|
93
|
+
if getattr(self, field.name) is not None:
|
|
94
|
+
return replace(self, **{field.name: None})
|
|
95
|
+
raise ValueError("Can not further decay.")
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def register(cls, class_: EntityClass, type_, variant, version):
|
|
99
|
+
"""Register a class as an implementation of an :class:`EntityType`."""
|
|
100
|
+
entity_type = cls(class_=class_, type_=type_, variant=variant, version=version)
|
|
101
|
+
|
|
102
|
+
def register_(obj):
|
|
103
|
+
if entity_type in _ENTITY_TYPE_REGISTRY:
|
|
104
|
+
raise EntityTypeRegistryError("Entity type is already registered.")
|
|
105
|
+
_ENTITY_TYPE_REGISTRY[entity_type] = obj
|
|
106
|
+
return obj
|
|
107
|
+
|
|
108
|
+
return register_
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def lookup(cls, type_, decay=False):
|
|
112
|
+
"""
|
|
113
|
+
Lookup the implementation of an :class:`EntityType`.
|
|
114
|
+
Use the ``decay`` parameter if you want to allow finding a more generic implementation.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
return _ENTITY_TYPE_REGISTRY[type_]
|
|
118
|
+
except KeyError:
|
|
119
|
+
if not decay:
|
|
120
|
+
raise UnknownEntityTypeError("Entity type %s not registered." % type_)
|
|
121
|
+
else:
|
|
122
|
+
try:
|
|
123
|
+
return cls.lookup(type_.fallback_type(), True)
|
|
124
|
+
except ValueError:
|
|
125
|
+
raise UnknownEntityTypeError("Neither entity type %s nor any fallbacks are registered." % type_)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Path(BasePath):
|
|
129
|
+
"""
|
|
130
|
+
A tuple uniquely identifying an entity in the REDAC.
|
|
131
|
+
|
|
132
|
+
The path to an entity is a hierarchical combination of paths to its parent entities.
|
|
133
|
+
Its structure in the REDAC is :code:`(<carrier board>, <cluster>, <block>, <function>)`.
|
|
134
|
+
Carrier boards are defined by their MAC address, e.g. "04-E9-E5-14-74-BF".
|
|
135
|
+
Clusters are defined by their index sent as a string, e.g. "0".
|
|
136
|
+
Function blocks on them are identified by their abbreviation, one of "M0", "M1", "U", "C", "I".
|
|
137
|
+
Functions on blocks are defined by their index as integer, e.g. 7.
|
|
138
|
+
The blocks' functions are usually not directly accessed, but instead configured via their block.
|
|
139
|
+
|
|
140
|
+
:Usage: Combine the identifiers to the required depth
|
|
141
|
+
|
|
142
|
+
.. code-block::
|
|
143
|
+
|
|
144
|
+
path_to_a_carrier_board = Path("00:00:5e:00:53:af")
|
|
145
|
+
path_to_second_cluster_on_it = Path("00:00:5e:00:53:af", "1")
|
|
146
|
+
path_to_m0_block_in_cluster0 = Path("00:00:5e:00:53:af", "0", "M0")
|
|
147
|
+
path_to_first_func_on_block = Path("00:00:5e:00:53:af", "0", "M0", 0)
|
|
148
|
+
"""
|
|
149
|
+
#: The schema defining the data types for the path's subcomponents.
|
|
150
|
+
SCHEMA = (str, str, str, int)
|
|
151
|
+
|
|
152
|
+
def to_carrier(self):
|
|
153
|
+
"""Returns the path until the carrier board level."""
|
|
154
|
+
return Path(self[:1])
|
|
155
|
+
|
|
156
|
+
def to_cluster(self):
|
|
157
|
+
"""
|
|
158
|
+
Returns the path until the cluster level.
|
|
159
|
+
|
|
160
|
+
Raises IndexError if path is not of sufficient depth.
|
|
161
|
+
"""
|
|
162
|
+
return Path(self[:2])
|
|
163
|
+
|
|
164
|
+
def to_block(self):
|
|
165
|
+
"""
|
|
166
|
+
Returns the path until the block level.
|
|
167
|
+
|
|
168
|
+
Raises IndexError if path is not of sufficient depth.
|
|
169
|
+
"""
|
|
170
|
+
return Path(self[:3])
|
|
171
|
+
|
|
172
|
+
def to_function(self):
|
|
173
|
+
"""
|
|
174
|
+
Returns the path until the function level.
|
|
175
|
+
|
|
176
|
+
Raises IndexError if path is not of sufficient depth.
|
|
177
|
+
"""
|
|
178
|
+
return Path(self[:4])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class Entity(BaseEntity):
|
|
183
|
+
"""
|
|
184
|
+
Base class for all entities inside a REDAC.
|
|
185
|
+
"""
|
|
186
|
+
#: Unique path to this entity.
|
|
187
|
+
path: Path
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def create_from_entity_type_tree(cls, sub_path, sub_tree):
|
|
191
|
+
raise NotImplementedError
|
|
192
|
+
|
|
193
|
+
def generate_partial_configuration(self, attribute):
|
|
194
|
+
if self.__dataclass_fields__.get(attribute, None):
|
|
195
|
+
return {attribute: getattr(self, attribute)}
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError("Unknown attribute %s for %s." % (attribute, self.__class__))
|
|
198
|
+
|
|
199
|
+
def apply_partial_configuration(self, attribute, value):
|
|
200
|
+
raise NotImplementedError
|
pybrid/redac/modules.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) 2022-2024 anabrid GmbH
|
|
2
|
+
# Contact: https://www.anabrid.com/licensing/
|
|
3
|
+
# SPDX-License-Identifier: MIT OR GPL-2.0-or-later
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModuleType(str):
|
|
9
|
+
"""Should probably be roughly equal to what we save in metadata storage"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Module:
|
|
15
|
+
pass
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The REDAC network protocol is a request-response message based protocol.
|
|
3
|
+
Messages are encoded as JSON strings and delimited by newline characters.
|
|
4
|
+
The message definition can be found in the :mod:`.messages` namespace.
|
|
5
|
+
Please refer to their documentations and the therein contained sequence diagrams
|
|
6
|
+
for details about the messaging process.
|
|
7
|
+
|
|
8
|
+
A usual sequence of messages for running one or multiple analog computations is:
|
|
9
|
+
|
|
10
|
+
#. Get the current hardware configuration via a :class:`~.messages.GetEntitiesRequest`.
|
|
11
|
+
#. Start a new user session (if required) via a :class:`~.messages.StartSessionRequest`.
|
|
12
|
+
#. Configure the analog computation via a series of :class:`~.messages.SetConfigRequest`.
|
|
13
|
+
#. Start the analog computation via a :class:`~.messages.StartRunRequest`.
|
|
14
|
+
#. Monitor the analog computation by listening to incoming :class:`~.messages.RunStateChangeMessage`
|
|
15
|
+
and :class:`~.messages.RunDataMessage` notifications.
|
|
16
|
+
#. The analog computation is done once you receive a :class:`~.messages.RunStateChangeMessage`
|
|
17
|
+
with :attr:`~.messages.RunStateChangeMessage.new` set to :attr:`~..run.RunState.DONE`.
|
|
18
|
+
#. Evaluate the data received and repeat from step 3 if necessary.
|
|
19
|
+
#. End your session with a :class:`~.messages.EndSessionRequest`.
|
|
20
|
+
"""
|