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/__init__.py +19 -0
- epgx/axes.py +285 -0
- epgx/common.py +63 -0
- epgx/compiler.py +347 -0
- epgx/diff.py +242 -0
- epgx/examples.py +146 -0
- epgx/functions.py +602 -0
- epgx/kernels/__init__.py +9 -0
- epgx/kernels/affine.py +187 -0
- epgx/kernels/evolution.py +40 -0
- epgx/kernels/operator.py +32 -0
- epgx/kernels/probe.py +107 -0
- epgx/kernels/shift.py +106 -0
- epgx/kernels/state.py +447 -0
- epgx/kernels/transition.py +48 -0
- epgx/library/__init__.py +3 -0
- epgx/library/sequences.py +210 -0
- epgx/lowering.py +95 -0
- epgx/merging.py +137 -0
- epgx/methods.py +329 -0
- epgx/ops/__init__.py +30 -0
- epgx/ops/dephasing.py +71 -0
- epgx/ops/evolution.py +135 -0
- epgx/ops/functions.py +158 -0
- epgx/ops/operator.py +224 -0
- epgx/ops/probe.py +116 -0
- epgx/ops/shift.py +118 -0
- epgx/ops/transition.py +58 -0
- epgx/plotting.py +226 -0
- epgx/progress.py +81 -0
- epgx/sample.py +142 -0
- epgx/sequences.py +850 -0
- epgx/stats.py +117 -0
- epgx-0.1.dev0.dist-info/METADATA +64 -0
- epgx-0.1.dev0.dist-info/RECORD +38 -0
- epgx-0.1.dev0.dist-info/WHEEL +5 -0
- epgx-0.1.dev0.dist-info/licenses/LICENSE +28 -0
- epgx-0.1.dev0.dist-info/top_level.txt +1 -0
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)
|
epgx/kernels/__init__.py
ADDED
|
@@ -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
|