epgx 0.1.dev0__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.
epgx/functions.py ADDED
@@ -0,0 +1,602 @@
1
+ """Sequence functions
2
+
3
+ simulate = resolve (axes) -> method (representation) -> compile -> run
4
+ trace = simulate with a snapshot of the states after every operator
5
+
6
+ `simulate` and `trace` are pure and jit-able (except for the "numpy" and
7
+ "xarray" outputs). `simulate` runs a cached compiled program (`jit=True`):
8
+ the sequence is flattened preserving the identity of shared operators
9
+ (`ops.flatten_operators`), and its structure and the static options are the
10
+ cache key, so that new values (e.g. other T2 or flip angles) do not recompile.
11
+ Inside a user's `jax.jit`, build the sequence in the jitted function (or close
12
+ over it) rather than passing it as an argument: JAX's own flattening does not
13
+ preserve identity (correct results, shared operators stacked in scans).
14
+ Sequence helpers (flatten_sequence, getnshift, get_adc_times) are defined in
15
+ `ops.functions` and re-exported here.
16
+ """
17
+
18
+ import dataclasses
19
+ import functools
20
+ import operator as _operator
21
+ import warnings
22
+
23
+ import jax
24
+ import jax.numpy as jnp
25
+ import numpy as np
26
+
27
+ from . import axes, compiler, lowering, methods
28
+ from .common import pytree, static
29
+ from .kernels import probe as kprobe
30
+ from .ops import (
31
+ Adc,
32
+ Operator,
33
+ Probe,
34
+ Probes,
35
+ check_conflicts,
36
+ flatten_operators,
37
+ flatten_sequence,
38
+ get_adc_times,
39
+ get_dephasing_axes,
40
+ getnshift, # noqa: F401 (re-export)
41
+ unflatten_operators,
42
+ )
43
+ from .progress import Progress
44
+
45
+ OUTPUTS = ("jax", "numpy", "layout", "times", "xarray")
46
+ GROUPS = (None, "name")
47
+
48
+ # probe of the full states, inserted after every operator by `trace`
49
+ SNAPSHOT = Probes([Adc("F"), Adc("Z")], name="SNAPSHOT")
50
+
51
+
52
+ @dataclasses.dataclass(frozen=True)
53
+ class OutputLayout:
54
+ """Labels of a simulated output
55
+
56
+ Attributes:
57
+ dims: axis names ("adc" first, then batch and core axes)
58
+ shape: axis sizes
59
+ coords: coordinates of labelled axes (dict: name -> values)
60
+ names: name of the probe of each ADC (if "adc" is the first axis)
61
+ """
62
+
63
+ dims: tuple
64
+ shape: tuple
65
+ coords: dict
66
+ names: tuple = None
67
+
68
+ def __repr__(self):
69
+ axes = ", ".join(f"{d}={s}" for d, s in zip(self.dims, self.shape))
70
+ return f"OutputLayout({axes})"
71
+
72
+
73
+ def simulate(
74
+ sequence,
75
+ sample=None,
76
+ scanner=None,
77
+ *,
78
+ probe=None,
79
+ output="jax",
80
+ group=None,
81
+ adc_time=False,
82
+ method=None,
83
+ init=None,
84
+ dtype=None,
85
+ dims=None,
86
+ alignment=None,
87
+ max_unroll=compiler.MAX_UNROLL,
88
+ checkpoint=False,
89
+ merge=True,
90
+ jit=True,
91
+ disp=False,
92
+ ):
93
+ """simulate a sequence
94
+
95
+ Args:
96
+ sequence: (nested) list of operators (repeated blocks: sub-lists with
97
+ the same structure, cf. compiler)
98
+ sample: Sample (tissue properties)
99
+ scanner: Scanner (hardware properties)
100
+ probe: replaces the acquisition of all probes (ADC) of the sequence:
101
+ attribute name (e.g. "Z0", "F"), Probe operator, or tuple of these
102
+ (the acquired value is then a tuple). An `Adc` sets what is
103
+ measured (attribute, weights, reduction) and each ADC of the
104
+ sequence keeps its phase (a phase given by both: error)
105
+ output: name or tuple of names of the returned objects:
106
+ "jax": acquired values, jax array(s) of shape (nadc, *dims) (default)
107
+ "numpy": same, as numpy arrays (not under jit)
108
+ "layout": OutputLayout (dims, shape, coords) of the values (without
109
+ simulation if no values are requested)
110
+ "times": ADC times (cf. get_adc_times)
111
+ "xarray": xarray.DataArray (requires xarray, not under jit)
112
+ group: arrangement of every returned object:
113
+ None: in sequence order (default)
114
+ "name": dict by probe name, in order of first appearance (e.g.
115
+ DESS with `Adc(name="fid")` and `Adc(name="echo")`); each
116
+ group is arranged as a whole output
117
+ adc_time: if True, also return the ADC times first, i.e.
118
+ output=("times", output)
119
+ method: representation of the states (e.g. `Lattice(nmax=...)`),
120
+ default: inferred from the sequence
121
+ init: initial State (default: equilibrium with density sample.PD)
122
+ dtype: complex dtype of the states (default: JAX's default)
123
+ dims, alignment: order of the axes, alignment of unnamed arrays (cf.
124
+ axes.resolve)
125
+ max_unroll, checkpoint: cf. lowering.compile_sequence
126
+ merge: merge consecutive affine operators (RF pulses, relaxation...)
127
+ into single maps, cf. lowering.compile_sequence (results are
128
+ identical up to rounding)
129
+ jit: run a cached compiled program (compiled again only if the
130
+ structure of the sequence or the static options change); False:
131
+ eager execution (e.g. for debugging)
132
+ disp: display a progress bar (cf. epgx.progress; compilation not shown)
133
+
134
+ Returns:
135
+ object(s) selected by `output`. If the acquired values do not all have
136
+ the same shape (e.g. ADCs with different reductions), they are returned
137
+ as a list (one value per ADC); naming the probes and using
138
+ `group="name"` keeps values of the same shape together.
139
+ """
140
+ names = (output,) if isinstance(output, str) else tuple(output)
141
+ if adc_time:
142
+ names = ("times",) + names
143
+ if not set(names) <= set(OUTPUTS):
144
+ raise ValueError(f"Unknown output(s): {set(names) - set(OUTPUTS)}")
145
+ if group not in GROUPS:
146
+ raise ValueError(f"Unknown group: {group}, expected one of {GROUPS}")
147
+
148
+ # probes
149
+ sequence = replace_probes(sequence, probe)
150
+ probes = [op for op in flatten_sequence(sequence) if isinstance(op, Probe)]
151
+ if not probes:
152
+ raise ValueError("Cannot simulate a sequence without any probe (ADC)")
153
+ groups = _group_by_name(probes) if group == "name" else {None: None}
154
+
155
+ check_sample(sequence, sample)
156
+ get_dephasing_axes(sequence) # consistent axes (concrete scales, before jit)
157
+
158
+ # static options (cache key of the compiled program)
159
+ method = method or methods.infer_method(sequence)
160
+ options = (
161
+ ("method", method),
162
+ ("dtype", dtype),
163
+ ("dims", None if dims is None else tuple(dims)),
164
+ ("alignment", alignment),
165
+ ("max_unroll", max_unroll),
166
+ ("checkpoint", checkpoint),
167
+ ("merge", merge),
168
+ ("disp", disp),
169
+ )
170
+ leaves, skeleton = flatten_operators(sequence)
171
+ execute = functools.partial(
172
+ _execute_jit if jit else _execute, skeleton=skeleton, options=options
173
+ )
174
+ if set(names) & {"jax", "numpy", "xarray"}:
175
+ values, times = execute(leaves, sample, scanner, init)
176
+ else:
177
+ # labels and times only: shapes of the values, without simulation
178
+ values, _ = jax.eval_shape(execute, leaves, sample, scanner, init)
179
+ times = get_adc_times(sequence) if "times" in names else None
180
+
181
+ # labels (computed outside of the compiled program: static)
182
+ if set(names) & {"layout", "xarray"}:
183
+ coords = axes.get_coords(sequence, sample, scanner)
184
+ if getattr(sample, "compartments", None):
185
+ coords["compartment"] = np.array(sample.compartments)
186
+ coords.update(method.core_coords(sequence))
187
+ core = method.core_axes(sequence)
188
+ layout, (resolved, _, _) = axes.resolve(
189
+ sequence, sample, scanner, dims=dims, alignment=alignment
190
+ )
191
+ resolved = [op for op in flatten_sequence(resolved) if isinstance(op, Probe)]
192
+
193
+ # outputs, by group (positions of the ADCs: static)
194
+ results = [{} for _ in names]
195
+ for key, indices in groups.items():
196
+ values_, times_, probes_ = values, times, probes
197
+ if indices is not None:
198
+ values_ = _take(values, indices)
199
+ times_ = None if times is None else times[np.asarray(indices)]
200
+ if set(names) & {"layout", "xarray"}:
201
+ probes_ = resolved if indices is None else [resolved[i] for i in indices]
202
+ labels = _labels(values_, probes_, layout, core, coords)
203
+ for result, name in zip(results, names):
204
+ if name == "jax":
205
+ result[key] = values_
206
+ elif name == "numpy":
207
+ result[key] = _to_numpy(values_)
208
+ elif name == "times":
209
+ result[key] = times_
210
+ elif name == "layout":
211
+ result[key] = labels
212
+ elif name == "xarray":
213
+ result[key] = _to_xarray(values_, labels, times_)
214
+ results = [result[None] if group is None else result for result in results]
215
+ return results[0] if len(results) == 1 else tuple(results)
216
+
217
+
218
+ def _execute(leaves, sample, scanner, init, *, skeleton, options):
219
+ """run a flattened sequence (cf. `simulate`)
220
+
221
+ Returns:
222
+ acquired values, ADC times
223
+ """
224
+ options = dict(options)
225
+ sequence = unflatten_operators(skeleton, leaves)
226
+ layout, (sequence, sample, scanner) = axes.resolve(
227
+ sequence, sample, scanner, dims=options["dims"], alignment=options["alignment"]
228
+ )
229
+
230
+ # method and initial state
231
+ method = options["method"]
232
+ method.check(sequence)
233
+ if init is None:
234
+ density = 1.0 if sample is None else sample.PD
235
+ state = method.init_state(
236
+ sequence, layout.shape, density=density, dtype=options["dtype"]
237
+ )
238
+ else:
239
+ state = init.broadcast(layout.shape)
240
+
241
+ # compile and run
242
+ program = lowering.compile_sequence(
243
+ sequence,
244
+ max_unroll=options["max_unroll"],
245
+ checkpoint=options["checkpoint"],
246
+ merge=options["merge"],
247
+ )
248
+ monitor = Progress(program.count).monitor if options["disp"] else None
249
+ context = lowering.Context(sample, scanner, method)
250
+ _, values = program.run(state, context, monitor=monitor)
251
+ return values, get_adc_times(sequence)
252
+
253
+
254
+ # compiled once per structure (skeleton) and static options
255
+ _execute_jit = jax.jit(_execute, static_argnames=("skeleton", "options"))
256
+
257
+
258
+ @pytree
259
+ class Trace:
260
+ """States after every operator of a sequence (cf. `trace`)
261
+
262
+ Attributes:
263
+ F, Z: states before the first operator (row 0) and after each operator
264
+ (row i + 1), shape (nop + 1, *core)
265
+ times: time of each row (ms): 0, then end time of each operator
266
+ coords: coordinates of the core axis (phase states k or isochromat
267
+ positions); nd lattice: tuple of coordinates, one per axis
268
+ operators: flat tuple of the operators (parameters selected as the states)
269
+ axis: (static) name of the core axis; nd lattice: tuple of names
270
+ (e.g. ("kx", "ky", "kz"))
271
+ representation: (static) representation of the states
272
+ """
273
+
274
+ F: jax.Array = None
275
+ Z: jax.Array = None
276
+ times: jax.Array = None
277
+ coords: jax.Array = None
278
+ operators: tuple = ()
279
+ axis: str = static("k")
280
+ representation: str = static("lattice")
281
+
282
+ def __init__(self, F, Z, times, coords, operators, *, axis, representation):
283
+ for name, value in locals().items():
284
+ if name != "self":
285
+ object.__setattr__(self, name, value)
286
+
287
+ def __repr__(self):
288
+ names = (self.axis,) if isinstance(self.axis, str) else self.axis
289
+ sizes = self.F.shape[len(self.F.shape) - len(names) :]
290
+ core = ", ".join(f"{n}={s}" for n, s in zip(names, sizes))
291
+ return f"Trace({len(self.operators)} operators, {core}, {self.representation})"
292
+
293
+ @property
294
+ def nop(self):
295
+ """number of operators"""
296
+ return len(self.operators)
297
+
298
+
299
+ def trace(
300
+ sequence,
301
+ sample=None,
302
+ scanner=None,
303
+ *,
304
+ select=None,
305
+ method=None,
306
+ init=None,
307
+ dtype=None,
308
+ dims=None,
309
+ alignment=None,
310
+ max_unroll=compiler.MAX_UNROLL,
311
+ jit=True,
312
+ disp=False,
313
+ ):
314
+ """simulate a sequence, keeping the states after every operator
315
+
316
+ The states are acquired by a probe (`SNAPSHOT`) inserted after every
317
+ operator, in the same nested lists (repeated blocks are still scanned).
318
+ The probes of the sequence are replaced by snapshots.
319
+
320
+ Args:
321
+ sequence, sample, scanner: cf. `simulate`
322
+ select: element of the batch axes, as a dict {axis: index} (axis name,
323
+ or int i for the unnamed axis dim_i); other axes: index 0
324
+ method, init, dtype, dims, alignment, max_unroll, jit, disp: cf. `simulate`
325
+
326
+ Returns:
327
+ Trace (states and operators of the selected element)
328
+ """
329
+ method = method or methods.infer_method(sequence)
330
+ core = method.core_axes(sequence)
331
+ traced = [SNAPSHOT] + _insert_snapshots(sequence, {})
332
+ F, Z = simulate(
333
+ traced,
334
+ sample,
335
+ scanner,
336
+ method=method,
337
+ init=init,
338
+ dtype=dtype,
339
+ dims=dims,
340
+ alignment=alignment,
341
+ max_unroll=max_unroll,
342
+ merge=False,
343
+ jit=jit,
344
+ disp=disp,
345
+ )
346
+
347
+ # selected element of the batch axes
348
+ batch, (resolved, _, _) = axes.resolve(
349
+ sequence, sample, scanner, dims=dims, alignment=alignment
350
+ )
351
+ index = _select_index(select, batch)
352
+ F = F[(slice(None),) + index]
353
+ Z = Z[(slice(None),) + index]
354
+ operators = tuple(
355
+ jax.tree.map(lambda x: _select_leaf(x, index), op)
356
+ for op in flatten_sequence(resolved)
357
+ )
358
+ durations = jnp.stack([jnp.asarray(op.duration, dtype=float) for op in operators])
359
+ times = jnp.concatenate([jnp.zeros(1, durations.dtype), jnp.cumsum(durations)])
360
+
361
+ # coordinates of the core axes (nd lattice: one per axis)
362
+ if method.representation == "lattice":
363
+ sizes = F.shape[F.ndim - len(core) :]
364
+ coords = tuple(jnp.arange(-(n // 2), n // 2 + 1) for n in sizes)
365
+ else:
366
+ core_coords = method.core_coords(sequence)
367
+ coords = tuple(jnp.asarray(core_coords[axis]) for axis in core)
368
+ axis, coords = (core[0], coords[0]) if len(core) == 1 else (core, coords)
369
+ return Trace(
370
+ F,
371
+ Z,
372
+ times,
373
+ coords,
374
+ operators,
375
+ axis=axis,
376
+ representation=method.representation,
377
+ )
378
+
379
+
380
+ def check_sample(sequence, sample):
381
+ """check the sample against the operators of the sequence
382
+
383
+ Raises if a property is given both by an operator and by the sample (e.g.
384
+ `E(tau, T1, T2)` with a sample setting T2). Warns if properties set in the
385
+ sample are read by no operator (e.g. a sample with T1/T2 and a sequence of
386
+ `R` operators, which ignore the sample). PD is always used (equilibrium).
387
+ """
388
+ if sample is None:
389
+ return
390
+ operators = flatten_sequence(sequence)
391
+ for op in operators:
392
+ check_conflicts(op, sample)
393
+ fields = {name for name in ("T1", "T2", "df") if getattr(sample, name) is not None}
394
+ used = set()
395
+ for op in operators:
396
+ used.update(op.sample_fields())
397
+ unused = sorted(fields - used)
398
+ if unused:
399
+ warnings.warn(
400
+ f"Sample properties not used by any operator of the sequence: {unused} "
401
+ "(they are read by E and P)",
402
+ stacklevel=3,
403
+ )
404
+
405
+
406
+ def replace_probes(sequence, probe):
407
+ """replace the probes of a sequence (keeping their durations and names)
408
+
409
+ Args:
410
+ probe: attribute name (str), Probe, or tuple of these (-> Probes)
411
+ """
412
+ if probe is None:
413
+ return sequence
414
+ multiple = isinstance(probe, tuple)
415
+ probes = probe if multiple else (probe,)
416
+ for item in probes:
417
+ if isinstance(item, str) and item not in kprobe.ATTRIBUTES:
418
+ raise ValueError(
419
+ f"Invalid probe: {item}, expected one of {kprobe.ATTRIBUTES}"
420
+ )
421
+ elif not isinstance(item, (str, Probe)):
422
+ raise TypeError(f"Invalid probe: {item}")
423
+
424
+ cache = {} # keep the identity of shared probes (cf. compiler)
425
+
426
+ def new_probe(op, item):
427
+ if isinstance(item, Adc) and isinstance(op, Adc):
428
+ # the probe sets what is measured, the sequence ADC keeps its phase
429
+ if item.phase is not None and op.phase is not None:
430
+ raise ValueError(
431
+ f"ADC phase given both by the probe {item!r} and by the ADC "
432
+ f"{op!r} of the sequence: remove it from the probe"
433
+ )
434
+ phase = op.phase if item.phase is None else item.phase
435
+ return item.replace(phase=phase, duration=op.duration)
436
+ elif isinstance(item, Probe):
437
+ return item.replace(duration=op.duration)
438
+ elif isinstance(op, Adc):
439
+ # same ADC (phase, weights, reduction) with another attribute
440
+ return op.replace(attr=item)
441
+ return Adc(item, duration=op.duration)
442
+
443
+ def replace(op):
444
+ if not isinstance(op, Probe):
445
+ return op
446
+ if id(op) not in cache:
447
+ new = [new_probe(op, item) for item in probes]
448
+ if multiple:
449
+ new = Probes(new, duration=op.duration, name=op.name)
450
+ else:
451
+ # the name of the sequence probe is kept (cf. group="name")
452
+ new = new[0] if new[0].name else new[0].replace(name=op.name)
453
+ cache[id(op)] = new
454
+ return cache[id(op)]
455
+
456
+ return jax.tree.map(replace, sequence, is_leaf=lambda x: isinstance(x, Operator))
457
+
458
+
459
+ #
460
+ # private
461
+
462
+
463
+ def _insert_snapshots(sequence, cache):
464
+ """insert SNAPSHOT after every operator, replace probes (cf. `trace`)"""
465
+ if isinstance(sequence, Operator):
466
+ sequence = [sequence]
467
+ result = []
468
+ for item in sequence:
469
+ if isinstance(item, (list, tuple)):
470
+ result.append(_insert_snapshots(item, cache))
471
+ elif isinstance(item, Probe):
472
+ # keep the identity of shared probes (cf. compiler)
473
+ if id(item) not in cache:
474
+ cache[id(item)] = SNAPSHOT.replace(duration=item.duration)
475
+ result.append(cache[id(item)])
476
+ elif isinstance(item, Operator):
477
+ result.extend([item, SNAPSHOT])
478
+ else:
479
+ raise ValueError(f"Invalid operator: {item}")
480
+ return result
481
+
482
+
483
+ def _select_index(select, layout):
484
+ """index of the selected element of the batch axes (cf. `trace`)"""
485
+ select = dict(select or {})
486
+ index = [0] * len(layout.dims)
487
+ for axis, value in select.items():
488
+ name = f"dim_{axis}" if isinstance(axis, int) else axis
489
+ if name not in layout.dims:
490
+ raise ValueError(f"Unknown axis: {axis!r}, expected one of {layout.dims}")
491
+ pos = layout.dims.index(name)
492
+ size = layout.shape[pos]
493
+ value = _operator.index(value)
494
+ if not -size <= value < size:
495
+ raise IndexError(f"Index {value} out of range for axis {name!r} ({size})")
496
+ index[pos] = value % size
497
+ return tuple(index)
498
+
499
+
500
+ def _select_leaf(value, index):
501
+ """selected element of a resolved leaf (scalar, or broadcastable array)"""
502
+ if jnp.ndim(value) < len(index):
503
+ return value # scalar
504
+ shape = jnp.shape(value)
505
+ return value[tuple(i if s > 1 else 0 for i, s in zip(index, shape))]
506
+
507
+
508
+ def _probe_dims(probe, layout, core):
509
+ """axis names of the value acquired by a (resolved) probe"""
510
+ if isinstance(probe, Probes):
511
+ return tuple(_probe_dims(p, layout, core) for p in probe.probes)
512
+ elif not isinstance(probe, Adc):
513
+ return None # unknown probe
514
+ dims = layout.reduced_dims(probe.reduce)
515
+ if probe.attr in ("F", "Fminus", "Z"):
516
+ dims += core
517
+ elif probe.attr == "states":
518
+ dims += core + ("component",)
519
+ return dims
520
+
521
+
522
+ def _labels(values, probes, layout, core, coords):
523
+ """OutputLayout(s) of the values acquired by (resolved) probes"""
524
+ specs = [_probe_dims(probe, layout, core) for probe in probes]
525
+ names = tuple(probe.name for probe in probes)
526
+
527
+ def label(dims, value, adc=True):
528
+ if dims is None:
529
+ return None
530
+ dims = (("adc",) if adc else ()) + dims
531
+ coords_ = {d: coords[d] for d in dims if d in coords}
532
+ return OutputLayout(dims, tuple(value.shape), coords_, names if adc else None)
533
+
534
+ def label_value(spec, value, adc=True):
535
+ if isinstance(value, tuple):
536
+ return tuple(label_value(s, v, adc) for s, v in zip(spec, value))
537
+ return label(spec, value, adc)
538
+
539
+ if isinstance(values, list):
540
+ return [
541
+ label_value(spec, value, adc=False) for spec, value in zip(specs, values)
542
+ ]
543
+ return label_value(specs[0], values)
544
+
545
+
546
+ def _group_by_name(probes):
547
+ """positions of the probes by name, in order of first appearance"""
548
+ unnamed = [probe for probe in probes if not probe.name]
549
+ if unnamed:
550
+ raise ValueError(
551
+ f'group="name" requires named probes, got: {unnamed[0]!r} '
552
+ '(e.g. Adc(name="echo"), the ADC instance is named "ADC")'
553
+ )
554
+ groups = {}
555
+ for i, probe in enumerate(probes):
556
+ groups.setdefault(probe.name, []).append(i)
557
+ return groups
558
+
559
+
560
+ def _take(values, indices):
561
+ """acquired values of the ADCs at given positions (as a whole output)"""
562
+ if all(isinstance(leaf, jax.ShapeDtypeStruct) for leaf in jax.tree.leaves(values)):
563
+ # shapes only (labels without simulation)
564
+ return jax.eval_shape(functools.partial(_take, indices=indices), values)
565
+ if isinstance(values, list):
566
+ chunks = [jax.tree.map(lambda leaf: leaf[None], values[i]) for i in indices]
567
+ return compiler._assemble(chunks)
568
+ index = np.asarray(indices)
569
+ return jax.tree.map(lambda leaf: leaf[index], values)
570
+
571
+
572
+ def _to_numpy(values):
573
+ def convert(leaf):
574
+ if isinstance(leaf, jax.core.Tracer):
575
+ raise ValueError('output="numpy" is not available under jit/grad')
576
+ return np.asarray(leaf)
577
+
578
+ return jax.tree.map(convert, values)
579
+
580
+
581
+ def _to_xarray(values, labels, times):
582
+ try:
583
+ import xarray
584
+ except ImportError as exc:
585
+ raise ImportError('output="xarray" requires xarray') from exc
586
+ times = None if times is None else np.asarray(_to_numpy(times))
587
+
588
+ def convert(value, label):
589
+ if isinstance(value, tuple):
590
+ return tuple(convert(v, lab) for v, lab in zip(value, label))
591
+ elif label is None:
592
+ raise ValueError("Cannot label the output of a custom probe")
593
+ coords = {name: np.asarray(c) for name, c in label.coords.items()}
594
+ if label.dims and label.dims[0] == "adc" and times is not None:
595
+ coords["adc"] = times
596
+ return xarray.DataArray(
597
+ np.asarray(_to_numpy(value)), dims=label.dims, coords=coords
598
+ )
599
+
600
+ if isinstance(values, list):
601
+ return [convert(value, label) for value, label in zip(values, labels)]
602
+ return convert(values, labels)
@@ -0,0 +1,9 @@
1
+ """Layer 1: numerical kernels (pure functions on dimensionless arrays)"""
2
+
3
+ from .affine import apply_affine
4
+ from .evolution import evolve
5
+ from .operator import reset, spoil, spoil_axis
6
+ from .probe import adc, demodulate, readout, reduce
7
+ from .shift import shift_lattice, shift_samples
8
+ from .state import State, mirror
9
+ from .transition import phase, rotate