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
|
@@ -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)
|
pybrid/redac/__init__.py
ADDED
|
@@ -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
|
pybrid/redac/carrier.py
ADDED
|
@@ -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)
|