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.
@@ -0,0 +1,356 @@
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 logging
6
+
7
+ import asyncclick as click
8
+ from asyncclick import Choice
9
+
10
+ from pybrid.base.hybrid import EntityDoesNotExist
11
+ from pybrid.base.transport import TCPTransport
12
+ from pybrid.cli.base import cli
13
+ from pybrid.cli.base.commands import user_program
14
+ from pybrid.cli.base.ressources import ManagedAsyncResource
15
+ from pybrid.cli.base.shell import Shell
16
+
17
+ from pybrid.redac.blocks import SwitchingBlock
18
+ from pybrid.redac.cluster import Cluster
19
+ from pybrid.redac.controller import Controller
20
+ from pybrid.redac.data import DatExporter
21
+ from pybrid.redac.display import TreeDisplay
22
+ from pybrid.redac.entities import Path, Entity
23
+ from pybrid.redac.protocol.protocol import Protocol
24
+ from pybrid.redac.run import Run, RunState, RunError
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @cli.group()
30
+ @click.pass_context
31
+ @click.option('--host', '-h', type=str, required=False, help="Network name or address of the REDAC.")
32
+ @click.option('--port', '-p', type=int, default=5732, required=False, help="Network port of the REDAC.")
33
+ @click.option('--reset/--no-reset', is_flag=True, default=True, show_default=True,
34
+ help="Whether to reset the REDAC after connecting.")
35
+ async def redac(ctx: click.Context, host, port, reset):
36
+ """
37
+ Entrypoint for all REDAC commands.
38
+
39
+ Use :code:`pybrid redac --help` to list all available sub-commands.
40
+ """
41
+
42
+ # Generate a transport
43
+ if host is not None and port is not None:
44
+ transport_ = ctx.obj["transport"] = await TCPTransport.create(host, port)
45
+ else:
46
+ raise RuntimeError("No valid combination of transport options given.")
47
+
48
+ # Generate a protocol
49
+ protocol = await Protocol.create(transport_)
50
+
51
+ # Generate a controller, which will also start the protocol
52
+ controller = await Controller.create(protocol)
53
+ ctx.obj["controller"] = await ctx.with_async_resource(ManagedAsyncResource(controller, 'start', 'stop'))
54
+
55
+ # Unless chosen otherwise, reset the analog computer
56
+ if reset:
57
+ await controller.reset()
58
+
59
+ # Create a run which is potentially modified by other commands (e.g. set-readout-elements)
60
+ ctx.obj["run"] = await controller.create_run()
61
+ ctx.obj["previous_run"] = None
62
+
63
+
64
+ @redac.command()
65
+ @click.pass_obj
66
+ @click.argument('path', type=str)
67
+ @click.argument('alias', type=str)
68
+ async def set_alias(obj, path, alias):
69
+ """
70
+ Define an alias for a path in an interactive session or script.
71
+ You can use the alias in subsequent commands instead of a path argument.
72
+
73
+ PATH is the path the alias should resolve to.
74
+ ALIAS is the name of the alias.
75
+
76
+ If '*' is passed for the path as first argument, the alias is set to point
77
+ to the next carrier board which does not yet have an alias set for it.
78
+ """
79
+ controller: Controller = obj["controller"]
80
+ aliases: dict[str, Path] = obj.get("aliases", {})
81
+ # Set alias supports a special '*' path as first argument,
82
+ # in which case it selects the next carrier board which was not yet aliased.
83
+ # This is used to not have to hard-code carrier board identifiers for (simple) examples.
84
+ if path == '*':
85
+ aliased_carrier_paths = {path for path in aliases.values() if path.depth == 1}
86
+ for carrier in controller.computer.carriers:
87
+ if carrier.path not in aliased_carrier_paths:
88
+ path_ = carrier.path
89
+ break
90
+ else:
91
+ raise EntityDoesNotExist("No more carrier boards available.")
92
+ else:
93
+ path_ = Path.parse(path, aliases=aliases)
94
+ # Save alias
95
+ if "aliases" not in obj:
96
+ obj["aliases"] = dict()
97
+ obj["aliases"].update({alias: path_})
98
+
99
+
100
+ @redac.command()
101
+ @click.pass_obj
102
+ async def display(obj):
103
+ """
104
+ Display the hardware structure of the REDAC.
105
+ """
106
+ controller: Controller = obj["controller"]
107
+ click.echo(TreeDisplay().render(controller.computer))
108
+
109
+
110
+ @redac.command()
111
+ @click.pass_obj
112
+ @click.option('--keep-calibration', type=bool, default=True, help='Whether to keep calibration.')
113
+ @click.option('--sync/--no-sync', default=True, help='Whether to immediately sync configuration to hardware.')
114
+ async def reset(obj, keep_calibration, sync):
115
+ """
116
+ Reset the REDAC to initial configuration.
117
+ """
118
+ controller: Controller = obj["controller"]
119
+ await controller.reset(keep_calibration=keep_calibration, sync=sync)
120
+
121
+
122
+ @redac.command()
123
+ @click.pass_obj
124
+ @click.option('-r', '--recursive', type=bool, default=True, help='Whether to get config recursively for sub-entities.')
125
+ @click.argument('path', type=str)
126
+ async def get_entity_config(obj, recursive, path):
127
+ """
128
+ Get the configuration of an entity.
129
+
130
+ PATH is the unique path of the entity.
131
+ """
132
+ controller: Controller = obj["controller"]
133
+
134
+ path_ = Path.parse(path, aliases=obj.get("aliases", None))
135
+ config = await controller.protocol.get_config(path_, recursive)
136
+ click.echo(config)
137
+
138
+
139
+ @redac.command()
140
+ @click.pass_obj
141
+ @click.option('--sync/--no-sync', default=True, help='Whether to immediately send configuration to hybrid controller.')
142
+ @click.argument('path', type=str)
143
+ @click.argument('attribute', type=str)
144
+ @click.argument('value', type=str)
145
+ async def set_element_config(obj, sync, path, attribute, value):
146
+ """
147
+ Set one ATTRIBUTE to VALUE of the configuration of an entity at PATH.
148
+
149
+ PATH is the unique path of the entity.
150
+ ATTRIBUTE is the name of the attribute to change, e.g. 'factor'.
151
+ VALUE is the new value of the attribute, e.g. '0.42'.
152
+ """
153
+ controller: Controller = obj["controller"]
154
+
155
+ path_ = Path.parse(path, aliases=obj.get("aliases", None))
156
+
157
+ # Try to get the entity by its path
158
+ entity: Entity = controller.computer.get_entity(path_)
159
+
160
+ # Apply configuration to element
161
+ entity.apply_partial_configuration(attribute, value)
162
+
163
+ # Build a configuration message to the parent block
164
+ entity_config = entity.generate_partial_configuration(attribute)
165
+
166
+ if sync:
167
+ if path_.depth >= 4:
168
+ await controller.protocol.set_config_request(entity=path_.parent,
169
+ config={"elements": {path_.id_: entity_config}})
170
+ else:
171
+ await controller.protocol.set_config_request(entity=path_, config=entity_config)
172
+
173
+
174
+ @redac.command()
175
+ @click.pass_obj
176
+ @click.option('--sync/--no-sync', default=True, help='Whether to immediately send configuration to hybrid controller.')
177
+ @click.option('--force', is_flag=True, default=False, show_default=True,
178
+ help="Force connection, possibly disconnecting existing connections.")
179
+ @click.argument('path', type=str)
180
+ @click.argument('connections', type=int, nargs=-1)
181
+ async def set_connection(obj, sync, force, path, connections):
182
+ """
183
+ Set one or multiple connections in a U-Block or I-Block.
184
+
185
+ PATH is the unique path to either a U-Block or I-Block.
186
+ CONNECTIONS specifies which connections should be set.
187
+ For a U-Block, the syntax is <input> <output> [<output> ...].
188
+ For a I-Block, the syntax is <input> [<input> ...] <output>.
189
+ """
190
+ controller: Controller = obj["controller"]
191
+
192
+ # Sanity check connections, which must be at least two arguments
193
+ if len(connections) < 2:
194
+ raise ValueError("You must supply at least two arguments for connection specification.")
195
+
196
+ # Try to get the entity by its path
197
+ path_ = Path.parse(path, aliases=obj.get("aliases", None))
198
+ entity = controller.computer.get_entity(path_)
199
+ # It must be a SwitchingBlock
200
+ if not isinstance(entity, SwitchingBlock):
201
+ raise ValueError("Expected a path to a SwitchingBlock.")
202
+
203
+ # Set connection, data structure depends on block type
204
+ entity.connect(*connections, force=force)
205
+
206
+ # Send configuration
207
+ if sync:
208
+ carrier = controller.computer.get_entity(path_.to_carrier())
209
+ await controller.set_config(carrier)
210
+
211
+
212
+ @redac.command()
213
+ @click.pass_obj
214
+ @click.option('--sync/--no-sync', default=True, help='Whether to immediately send configuration to hybrid controller.')
215
+ @click.argument('path', type=str)
216
+ @click.argument('m_out', type=int)
217
+ @click.argument('u_out', type=int)
218
+ @click.argument('c_factor', type=float)
219
+ @click.argument('m_in', type=int)
220
+ async def route(obj, sync, path, m_out, u_out, c_factor, m_in):
221
+ """
222
+ Route a signal on one cluster from one output of one M-Block through the U-Block, a coefficient on the C-Block,
223
+ through the I-Block and back to one input of one M-Block.
224
+
225
+ PATH is the unique path of the entity.
226
+ M_OUT is the M-Block signal output index.
227
+ U_OUT is the U-Block signal output index (equals coefficient index).
228
+ C_FACTOR is the factor of the coefficient.
229
+ M_IN is the M-Block signal input index (equals I-Block signal output index).
230
+ """
231
+ controller: Controller = obj["controller"]
232
+
233
+ # Try to get the entity by its path
234
+ path_ = Path.parse(path, aliases=obj.get("aliases", None))
235
+ cluster = controller.computer.get_entity(path_)
236
+ # It must be a SwitchingBlock
237
+ if not isinstance(cluster, Cluster):
238
+ raise ValueError("Expected a path to a Cluster.")
239
+
240
+ cluster.route(m_out, u_out, c_factor, m_in)
241
+ if sync:
242
+ await controller.set_config(cluster)
243
+
244
+
245
+ @redac.command()
246
+ @click.pass_obj
247
+ @click.option('--sample-rate', '-r', type=Choice(
248
+ ['1', '2', '4', '5', '8', '10', '16', '20', '25', '32', '40', '50', '64', '80', '100', '125', '160', '200', '250',
249
+ '320', '400', '500', '625', '800', '1000', '1250', '1600', '2000', '2500', '3125', '4000', '5000', '6250', '8000',
250
+ '10000', '12500', '15625', '20000', '25000', '31250', '40000', '50000', '62500', '100000', '125000', '200000',
251
+ '250000', '500000', '1000000']), required=False, help="Sample rate in samples/second.")
252
+ @click.option('--num-channels', '-n', type=Choice(['0', '1', '2', '4', '8']), default='0', help="Number of channels.")
253
+ async def set_daq(obj, sample_rate, num_channels):
254
+ """
255
+ Configure data acquisition of subsequent run commands.
256
+ Only useful in interactive sessions or scripts.
257
+ Is lost once the session or script ends.
258
+ """
259
+ controller: Controller = obj["controller"]
260
+ run_: Run = obj["run"]
261
+
262
+ run_.daq.num_channels = num_channels
263
+ if sample_rate is not None:
264
+ run_.daq.sample_rate = int(sample_rate)
265
+
266
+
267
+ @redac.command()
268
+ @click.pass_obj
269
+ # Run options
270
+ @click.option('--op-time', type=int, default=None, help='OP time in nanoseconds.')
271
+ @click.option('--ic-time', type=int, default=None, help='IC time in nanoseconds.')
272
+ # Output options
273
+ @click.option('--output', '-o', type=click.File('wt'), default='-', help="File to write data to.")
274
+ @click.option('--output-format', '-f', type=click.Choice(choices=("none", "dat",)), default="dat",
275
+ help="Format to write data in.")
276
+ async def run(obj, op_time, ic_time, output, output_format):
277
+ """
278
+ Start a run (computation) and wait until it is complete.
279
+ """
280
+ controller: Controller = obj["controller"]
281
+ run_: Run = obj["run"]
282
+
283
+ # If the run in the context object is already done, we need a new one
284
+ if run_.state.is_done():
285
+ run_ = Run.make_from_other_run(run_)
286
+
287
+ # Set run config
288
+ if ic_time is not None:
289
+ run_.config.ic_time = ic_time
290
+ if op_time is not None:
291
+ run_.config.op_time = op_time
292
+
293
+ timeout = max(run_.config.op_time / 1_000_000_000 + 3, 3)
294
+ run_ = obj["run"] = await controller.start_and_await_run(run_, timeout=timeout)
295
+ if run_.state is RunState.ERROR:
296
+ raise RunError("Error while executing run.")
297
+
298
+ if output_format == "dat":
299
+ exporter = DatExporter(output)
300
+ exporter.export(run_)
301
+
302
+
303
+ @redac.command()
304
+ @click.pass_context
305
+ @click.option('--ignore-errors', is_flag=True, default=False, show_default=True,
306
+ help="Ignore errors while executing a script.")
307
+ @click.option('--exit-after-script', '-x', is_flag=True, default=False, show_default=True,
308
+ help="Exit after the scripts have been executed. Useful if output is piped into other programs.")
309
+ @click.argument('scripts', nargs=-1, type=click.File('r'))
310
+ async def shell(ctx: click.Context, ignore_errors, exit_after_script, scripts):
311
+ """
312
+ Start an interactive shell and/or execute a REDAC shell SCRIPT.
313
+
314
+ SCRIPTS is a list of REDAC shell script files to execute before starting the interactive session."
315
+ """
316
+ computer_name = ctx.obj["controller"].computer.name
317
+
318
+ # Create and start a shell
319
+ shell_ = Shell(base_group=redac, base_ctx=ctx.parent, slug=computer_name, prompt=f"{computer_name} >> ")
320
+ with shell_:
321
+ for script in scripts:
322
+ logger.debug("Executing %s.", script.name)
323
+ for line_no, line in enumerate(script):
324
+ line = line.strip()
325
+ if not line or line.startswith('#'):
326
+ continue
327
+ try:
328
+ await shell_.execute_cmdline(line)
329
+ except Exception as exc:
330
+ logger.exception("Error in script during '%s' (line %s): %s", line, line_no, exc)
331
+ if not ignore_errors:
332
+ raise
333
+ if not exit_after_script:
334
+ await shell_.repl_loop()
335
+
336
+
337
+ @redac.group()
338
+ async def hack():
339
+ """
340
+ Collects 'hack' commands, for development purposes only.
341
+ """
342
+ pass
343
+
344
+
345
+ @hack.command()
346
+ @click.pass_obj
347
+ async def make_slave(obj):
348
+ """
349
+ Set one hybrid controller into slave mode.
350
+ For development purposes only.
351
+ """
352
+ controller: Controller = obj["controller"]
353
+ await controller.hack("slave", True)
354
+
355
+
356
+ redac.command()(user_program)
@@ -0,0 +1,6 @@
1
+ from .computer import REDAC
2
+ from .controller import Controller
3
+ from .elements import ComputationElement
4
+ from .entities import Path
5
+ from .protocol.protocol import Protocol
6
+ from .run import Run, RunConfig, RunFlags, RunState, DAQConfig
@@ -0,0 +1,9 @@
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 .block import FunctionBlock, ElementBlock, SwitchingBlock
6
+ from .cblock import CBlock
7
+ from .iblock import IBlock
8
+ from .mblock import MBlock, MIntBlock
9
+ from .ublock import UBlock
@@ -0,0 +1,61 @@
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, EntityType
9
+ from ..elements import ComputationElement
10
+
11
+
12
+ class FunctionBlock(Entity):
13
+ @classmethod
14
+ def create_from_entity_type_tree(cls, sub_path, sub_tree):
15
+ # TODO: Refactor out common code
16
+ # Check information on self
17
+ this_entity_type = EntityType.pop_from_dict(sub_tree)
18
+
19
+ # Generate type-specific entity
20
+ entity_class = EntityType.lookup(this_entity_type, decay=True)
21
+ return entity_class(path=sub_path)
22
+
23
+
24
+ @dataclass(kw_only=True)
25
+ class ElementBlock(FunctionBlock):
26
+ """
27
+ Base class for function blocks in a REDAC.
28
+ """
29
+ ELEMENTS: typing.ClassVar[list[typing.Type[ComputationElement]]] = None
30
+ elements: typing.Optional[list[ComputationElement]] = None
31
+
32
+ @property
33
+ def children(self):
34
+ if not self.elements:
35
+ return
36
+ yield from self.elements
37
+
38
+ def __post_init__(self):
39
+ super().__post_init__()
40
+ if self.elements is None:
41
+ self.elements = self.initialize_elements(self.path)
42
+
43
+ @classmethod
44
+ def initialize_elements(cls, base_path) -> list[ComputationElement]:
45
+ if not cls.ELEMENTS:
46
+ return []
47
+ elements: list[ComputationElement] = list(
48
+ E(path=base_path / idx)
49
+ for idx, E in enumerate(cls.ELEMENTS)
50
+ )
51
+ return elements
52
+
53
+
54
+ class SignalConnectionError(Exception):
55
+ pass
56
+
57
+
58
+ @dataclass
59
+ class SwitchingBlock(FunctionBlock):
60
+ def connect(self, *connections, force=False):
61
+ raise NotImplementedError
@@ -0,0 +1,22 @@
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 .block import ElementBlock
6
+ from ..computations import ScalarMultiplication
7
+ from ..elements import ComputationElement
8
+ from ..entities import EntityClass, EntityType
9
+
10
+
11
+ @EntityType.register(EntityClass.CBLOCK, 0, 0, 0)
12
+ class CBlock(ElementBlock):
13
+ """
14
+ A coefficient block (C-Block) in a REDAC.
15
+ It can multiply each of the 32 input signal with an individual fixed scalar factor.
16
+ """
17
+ #: List of elements on the block. In case of the CBlock,
18
+ #: these are 32 scalar multiplication computation elements (so-called coefficients).
19
+ #: Each coefficient accepts configuration parameters according to
20
+ #: :class:`pybrid.redac.computations.ScalarMultiplication`.
21
+ elements: list[ComputationElement[ScalarMultiplication]]
22
+ ELEMENTS = (ComputationElement[ScalarMultiplication],) * 32
@@ -0,0 +1,38 @@
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 .block import SwitchingBlock, SignalConnectionError
8
+ from ..entities import EntityClass, EntityType
9
+
10
+
11
+ @EntityType.register(EntityClass.IBLOCK, None, None, None)
12
+ @dataclass
13
+ class IBlock(SwitchingBlock):
14
+ """
15
+ A current summation block (I-Block) in a REDAC.
16
+ It can connect and sum up a subset of the 32 inputs to each of the 16 outputs.
17
+ """
18
+
19
+ #: List of inputs connected to each output.
20
+ #: Each sub-list in the list corresponds to one output.
21
+ #: The outputs are set to the sum of the inputs specified by the sub-list in the respective array element.
22
+ #: Use an empty sub-list to disable an output.
23
+ #: The firmware may accept additional JSON structures (see JSON schema).
24
+ outputs: list[set[int]] = field(default_factory=lambda: [set()] * 16)
25
+
26
+ def connect(self, *connections, force=False):
27
+ *input_idxs, output_idx = connections
28
+ input_idxs = set(input_idxs)
29
+ # Check if input is already connected to another output (signal-splitting is usually wrong)
30
+ if not force:
31
+ for other_output_idx, other_output in enumerate(self.outputs):
32
+ if other_output_idx == output_idx:
33
+ continue
34
+ if other_output is not None and other_output.intersection(input_idxs):
35
+ raise SignalConnectionError(
36
+ "One of inputs %s is already connected to output %s. Use the force argument to ignore." % (
37
+ input_idxs, other_output_idx))
38
+ self.outputs[output_idx] = self.outputs[output_idx].union(input_idxs)
@@ -0,0 +1,37 @@
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 .block import ElementBlock
6
+ from ..computations import Integration, Multiplication
7
+ from ..elements import ComputationElement
8
+ from ..entities import EntityClass, EntityType
9
+
10
+
11
+ @EntityType.register(EntityClass.MBLOCK, None, None, None)
12
+ class MBlock(ElementBlock):
13
+ """
14
+ A math block (M-Block) in a REDAC.
15
+ """
16
+
17
+
18
+ @EntityType.register(EntityClass.MBLOCK, 0, 0, 0)
19
+ class MIntBlock(MBlock):
20
+ """
21
+ A math block consisting of eight integrators.
22
+ """
23
+ ELEMENTS = (ComputationElement[Integration],) * 8
24
+ elements: list[ComputationElement[Integration]]
25
+ """
26
+ List of elements on the block.
27
+ In case of the MIntBlock, these are eight integration computation elements.
28
+ Each integrator accepts configuration according to :class:`pybrid.redac.computations.Integration`.
29
+ """
30
+
31
+
32
+ @EntityType.register(EntityClass.MBLOCK, 1, 0, 0)
33
+ class MMulBlock(MBlock):
34
+ """
35
+ A math block consisting of multiplicative elements.
36
+ """
37
+ ELEMENTS = (ComputationElement[Multiplication],) * 4
@@ -0,0 +1,47 @@
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 field, dataclass
6
+ from itertools import chain
7
+
8
+ from .block import SwitchingBlock, SignalConnectionError
9
+ from ..entities import EntityClass, EntityType
10
+
11
+
12
+ @EntityType.register(EntityClass.UBLOCK, None, None, None)
13
+ @dataclass
14
+ class UBlock(SwitchingBlock):
15
+ """
16
+ A voltage fork block (U-Block) in a REDAC.
17
+ It can distribute each of the 16 input signals to one of the 32 output signals.
18
+ """
19
+ #: List of inputs forked to each of the outputs.
20
+ #: Each element in the list corresponds to one output.
21
+ #: The outputs are set to the input index specified by the respective array element.
22
+ #: Use None (null in JSON) to disable an output.
23
+ #: The firmware may accept additional JSON structures (see JSON schema).
24
+ outputs: list[int | None] = field(default_factory=lambda: [None] * 32)
25
+ #: List of alternate signals to activate.
26
+ #: The U-Block implements a set of alternate signals, e.g. the 1-reference and cluster input signals.
27
+ #: Each signal is identified by a unique number and if present in the list, is activated.
28
+ #: The signals are: 0-7 denote cluster input signals 0-7, 8 denotes the 1-reference.
29
+ #: Currently, there is no way to disable an alternate signal.
30
+ alt_signals: list[int] = field(default_factory=list)
31
+
32
+ def apply_partial_configuration(self, attribute, value):
33
+ if attribute == "alt_signals":
34
+ self.alt_signals = list(map(int, value.split(',')))
35
+ else:
36
+ raise AttributeError("Can not apply configuration to attribute %s like this." % attribute)
37
+
38
+ def connect(self, input, output, *outputs, force=False):
39
+ # Sanity check before actually doing anything
40
+ if not force:
41
+ for out in chain([output], outputs):
42
+ if self.outputs[out] is not None:
43
+ raise SignalConnectionError(
44
+ "Output %s is already in use. Use the force argument to overwrite." % out)
45
+ # Actually connect
46
+ for out in chain([output], outputs):
47
+ self.outputs[out] = input
@@ -0,0 +1,43 @@
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
+ from .entities import Entity, Path, EntityType, EntityClass
8
+ from .cluster import Cluster
9
+
10
+
11
+ @dataclass(kw_only=True)
12
+ class Carrier(Entity):
13
+ """
14
+ A REDAC carrier board.
15
+
16
+ This is the smallest independent hardware unit inside a REDAC.
17
+ It contains several :class:`.cluster.Cluster` objects.
18
+ """
19
+ #: List of clusters on the carrier board.
20
+ clusters: list[Cluster]
21
+
22
+ @property
23
+ def children(self):
24
+ """Generator iterating through child entities of type :class:`.cluster.Cluster`."""
25
+ yield from self.clusters
26
+
27
+ @classmethod
28
+ def create_from_entity_type_tree(cls, path, tree):
29
+ # TODO: Refactor out common code
30
+ # Check information on self
31
+ this_entity_type = EntityType.pop_from_dict(tree)
32
+ assert this_entity_type.class_ is EntityClass.CARRIER
33
+
34
+ # Generate child entities
35
+ clusters = []
36
+ for sub_path, sub_tree in tree.items():
37
+ if not sub_path.startswith('/'):
38
+ raise ValueError('Unexpected entities tree element. Expected only sub-paths to be left.')
39
+ path_ = path / Path((sub_path.removeprefix('/'),))
40
+ cluster = Cluster.create_from_entity_type_tree(path_, sub_tree)
41
+ clusters.append(cluster)
42
+
43
+ return cls(path=path, clusters=clusters)