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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Reference sequences
|
|
2
|
+
|
|
3
|
+
Commonly used sequences, defined once with `@sequence`, for tests, examples
|
|
4
|
+
and benchmarks where the sequence is not the focus. Conventions:
|
|
5
|
+
|
|
6
|
+
- angles in degrees, times in ms; durations are set on the operators, so
|
|
7
|
+
the ADC times are meaningful (`output="times"`);
|
|
8
|
+
- relaxation and frequency offsets from the `Sample` (T1, T2, df), B1 and
|
|
9
|
+
B0 from the `Scanner`;
|
|
10
|
+
- numeric parameters (angles, timings, trains) are declared variables,
|
|
11
|
+
structural sizes (numbers of echoes, dummy repetitions) are static;
|
|
12
|
+
- the ADC phase follows the RF phase (RF-spoiled and phase-cycled
|
|
13
|
+
sequences are demodulated).
|
|
14
|
+
|
|
15
|
+
Each sequence gives the signal when called (`mse(esp=10.0, sample=...)`),
|
|
16
|
+
derivatives and statistics by name (`mse.jacobian(["T2"], esp=10.0, ...)`),
|
|
17
|
+
and the layer-2 list with `.build(...)`. A multi-point sequence (e.g. pSPGR)
|
|
18
|
+
is a batch axis of the variables: `spgr(alpha=Axis("point", alphas),
|
|
19
|
+
phi=Axis("point", phis), ...)`.
|
|
20
|
+
|
|
21
|
+
Preparation modules (`inversion`, `t2prep`) are plain functions returning
|
|
22
|
+
lists of operators, to compose into sequences.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
|
|
27
|
+
from .. import ops
|
|
28
|
+
from ..sequences import sequence
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# spin echoes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@sequence(variables=["esp", "alpha"])
|
|
35
|
+
def mse(esp, alpha=180.0, necho=10):
|
|
36
|
+
"""multi spin echo (CPMG): 90y excitation, `necho` refocusing pulses (x)
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
esp: echo spacing (ms)
|
|
40
|
+
alpha: refocusing flip angle (degrees)
|
|
41
|
+
necho: (static) number of echoes
|
|
42
|
+
"""
|
|
43
|
+
shift, relax = ops.S(1), ops.E(esp / 2, duration=True)
|
|
44
|
+
block = [shift, relax, ops.T(alpha, 0), shift, relax, ops.ADC]
|
|
45
|
+
return [ops.T(90, 90)] + [block] * necho
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@sequence(variables=["esp", "alpha[n]"])
|
|
49
|
+
def tse(esp, alpha):
|
|
50
|
+
"""turbo spin echo with a train of refocusing angles (one echo per angle)
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
esp: echo spacing (ms)
|
|
54
|
+
alpha: refocusing flip angles (degrees), one per echo
|
|
55
|
+
"""
|
|
56
|
+
shift, relax = ops.S(1), ops.E(esp / 2, duration=True)
|
|
57
|
+
return [ops.T(90, 90)] + [
|
|
58
|
+
[shift, relax, ops.T(a, 0), shift, relax, ops.ADC] for a in alpha
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@sequence(variables=["alpha", "esp"])
|
|
63
|
+
def hyperecho(alpha, esp=10.0, n=20):
|
|
64
|
+
"""hyperecho: 90y, n pulses (alpha, x), 180x, n pulses (-alpha, x)
|
|
65
|
+
|
|
66
|
+
The hyperecho forms at the last ADC (full refocusing without relaxation).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
alpha: flip angle of the trains (degrees)
|
|
70
|
+
esp: pulse spacing (ms)
|
|
71
|
+
n: (static) number of pulses of each train
|
|
72
|
+
"""
|
|
73
|
+
shift, relax = ops.S(1), ops.E(esp, duration=True)
|
|
74
|
+
first = [[ops.T(alpha, 0), shift, relax, ops.ADC]] * n
|
|
75
|
+
second = [[ops.T(-alpha, 0), shift, relax, ops.ADC]] * n
|
|
76
|
+
return (
|
|
77
|
+
[ops.T(90, 90), shift, relax] + first + [ops.T(180, 0), shift, relax] + second
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# gradient echoes
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@sequence(variables=["alpha", "TR", "TE", "phi"])
|
|
86
|
+
def spgr(alpha, TR, TE, phi=117.0, ndummy=200, nread=1):
|
|
87
|
+
"""RF-spoiled gradient echo (quadratic phase), acquired at steady state
|
|
88
|
+
|
|
89
|
+
Phase of pulse n: phi * n * (n + 1) / 2 (increment growing by phi); the
|
|
90
|
+
ADC phase follows the RF phase. Small `phi` gives partially spoiled
|
|
91
|
+
sequences (pSPGR).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
alpha: flip angle (degrees)
|
|
95
|
+
TR, TE: repetition and echo times (ms)
|
|
96
|
+
phi: phase increment step (degrees)
|
|
97
|
+
ndummy: (static) number of dummy repetitions
|
|
98
|
+
nread: (static) number of acquired repetitions (after the dummies)
|
|
99
|
+
"""
|
|
100
|
+
n = jnp.arange(ndummy + nread)
|
|
101
|
+
phases = phi * n * (n + 1) / 2
|
|
102
|
+
rlx1, rlx2 = ops.E(TE, duration=True), ops.E(TR - TE, duration=True)
|
|
103
|
+
dummies = [[ops.T(alpha, p), rlx1, rlx2, ops.S(1)] for p in phases[:ndummy]]
|
|
104
|
+
reads = [
|
|
105
|
+
[ops.T(alpha, p), rlx1, ops.Adc(phase=p), rlx2, ops.S(1)]
|
|
106
|
+
for p in phases[ndummy:]
|
|
107
|
+
]
|
|
108
|
+
return dummies + reads
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@sequence(variables=["alpha", "TR"])
|
|
112
|
+
def bssfp(alpha, TR, ndummy=200, nread=1):
|
|
113
|
+
"""balanced SSFP: phase alternation (0, 180), echo at TE = TR / 2
|
|
114
|
+
|
|
115
|
+
Off-resonance from the sample (`df`) or the scanner (`B0`); the ADC phase
|
|
116
|
+
follows the RF phase.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
alpha: flip angle (degrees)
|
|
120
|
+
TR: repetition time (ms)
|
|
121
|
+
ndummy: (static) number of dummy repetitions
|
|
122
|
+
nread: (static) number of acquired repetitions (after the dummies)
|
|
123
|
+
"""
|
|
124
|
+
half = ops.E(TR / 2, duration=True)
|
|
125
|
+
blocks = []
|
|
126
|
+
for n in range(ndummy + nread):
|
|
127
|
+
phase = 180.0 * (n % 2)
|
|
128
|
+
adc = [ops.Adc(phase=phase)] if n >= ndummy else []
|
|
129
|
+
blocks.append([ops.T(alpha, phase), half, *adc, half])
|
|
130
|
+
return blocks
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@sequence(variables=["alpha", "TR", "TE"])
|
|
134
|
+
def dess(alpha, TR, TE, ndummy=200, nread=1):
|
|
135
|
+
"""double echo steady state: FID and echo of an unbalanced gradient echo
|
|
136
|
+
|
|
137
|
+
The FID is acquired TE after the pulse (probe "fid"), the echo TE before
|
|
138
|
+
the next pulse (probe "echo"); `group="name"` separates them.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
alpha: flip angle (degrees)
|
|
142
|
+
TR, TE: repetition time and echo time of the FID (ms)
|
|
143
|
+
ndummy: (static) number of dummy repetitions
|
|
144
|
+
nread: (static) number of acquired repetitions (after the dummies)
|
|
145
|
+
"""
|
|
146
|
+
rf, shift = ops.T(alpha, 0), ops.S(1)
|
|
147
|
+
rlx1, rlx2 = ops.E(TE, duration=True), ops.E(TR - 2 * TE, duration=True)
|
|
148
|
+
fid, echo = ops.Adc(name="fid"), ops.Adc(name="echo")
|
|
149
|
+
dummies = [[rf, rlx1, shift, rlx2, rlx1]] * ndummy
|
|
150
|
+
reads = [[rf, rlx1, fid, shift, rlx2, echo, rlx1]] * nread
|
|
151
|
+
return dummies + reads
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@sequence(variables=["alpha[n]", "TR[n]", "TE", "TI"])
|
|
155
|
+
def mrf_fisp(alpha, TR, TE=3.0, TI=20.0, phi=90.0):
|
|
156
|
+
"""MR fingerprinting (FISP): inversion, then a train of (alpha_i, TR_i)
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
alpha: flip angles (degrees), one per repetition
|
|
160
|
+
TR: repetition times (ms), one per repetition
|
|
161
|
+
TE: echo time (ms)
|
|
162
|
+
TI: inversion delay (ms)
|
|
163
|
+
phi: (static) RF phase (degrees)
|
|
164
|
+
"""
|
|
165
|
+
rlx1 = ops.E(TE, duration=True)
|
|
166
|
+
return [ops.T(180, phi), ops.E(TI, duration=True)] + [
|
|
167
|
+
[ops.T(a, phi), rlx1, ops.ADC, ops.E(tr - TE, duration=True), ops.S(1)]
|
|
168
|
+
for a, tr in zip(alpha, TR)
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@sequence(variables=["TE", "esp", "alpha"])
|
|
173
|
+
def epi(TE, esp=0.5, alpha=90.0, nlines=64):
|
|
174
|
+
"""single-shot gradient echo EPI, at the signal level (no encoding)
|
|
175
|
+
|
|
176
|
+
One ADC per line, every `esp`, the center line at TE (requires TE >=
|
|
177
|
+
(nlines // 2) * esp). Intra-voxel dephasing (T2*) can be modelled by a
|
|
178
|
+
frequency distribution of the sample (`df` on an axis), summed with
|
|
179
|
+
`probe=Adc(reduce=...)`.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
TE: echo time of the center line (ms)
|
|
183
|
+
esp: line spacing (ms)
|
|
184
|
+
alpha: flip angle (degrees)
|
|
185
|
+
nlines: (static) number of lines
|
|
186
|
+
"""
|
|
187
|
+
first = TE - (nlines // 2) * esp
|
|
188
|
+
lines = [[ops.E(esp, duration=True), ops.ADC]] * (nlines - 1)
|
|
189
|
+
return [ops.T(alpha, 90), ops.E(first, duration=True), ops.ADC] + lines
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#
|
|
193
|
+
# preparation modules (lists of operators)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def inversion(TI, phi=0.0, spoil=True):
|
|
197
|
+
"""inversion module: 180 pulse, spoiler (optional), delay TI (ms)"""
|
|
198
|
+
spoiler = [ops.SPOILER] if spoil else []
|
|
199
|
+
return [ops.T(180, phi)] + spoiler + [ops.E(TI, duration=True)]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def t2prep(TE, nrefocus=4):
|
|
203
|
+
"""T2 preparation: 90x, `nrefocus` 180y pulses over TE (ms), -90x, spoiler
|
|
204
|
+
|
|
205
|
+
Stores exp(-TE / T2) of the longitudinal magnetization (on resonance,
|
|
206
|
+
without T1 recovery).
|
|
207
|
+
"""
|
|
208
|
+
half = ops.E(TE / nrefocus / 2, duration=True)
|
|
209
|
+
refocus = [[half, ops.T(180, 90), half]] * nrefocus
|
|
210
|
+
return [ops.T(90, 0)] + refocus + [ops.T(-90, 0), ops.SPOILER]
|
epgx/lowering.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""EPG front-end of the compiler: operators -> steps, compile_sequence
|
|
2
|
+
|
|
3
|
+
The compiler (cf. compiler) is generic; this module provides what is specific
|
|
4
|
+
to operators:
|
|
5
|
+
- `lower(op)`: Step applying the operator's kernel with its normalized
|
|
6
|
+
parameters, from the run context (sample, scanner); probes acquire;
|
|
7
|
+
- `compile_sequence`: compiler with this lowering and, with `merge=True`,
|
|
8
|
+
the merging pass over affine maps (cf. merging, kernels.affine).
|
|
9
|
+
|
|
10
|
+
Programs compiled here run with `program.run(state, Context(sample, scanner,
|
|
11
|
+
method))`; a tuple (sample, scanner) or None is also accepted. The method
|
|
12
|
+
(static) gives map builders access to the representation of the states (e.g.
|
|
13
|
+
the coordinates of a phase grid, for maps of shifts).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from . import compiler, merging
|
|
17
|
+
from .common import pytree, static
|
|
18
|
+
from .kernels.affine import AffineAlgebra
|
|
19
|
+
from .ops import Operator, Probe
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytree
|
|
23
|
+
class Context:
|
|
24
|
+
"""Run context of a compiled sequence
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
sample, scanner: Sample and Scanner (or None)
|
|
28
|
+
method: (static) representation of the states (e.g. `Lattice(...)`)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
sample: object = None
|
|
32
|
+
scanner: object = None
|
|
33
|
+
method: object = static()
|
|
34
|
+
|
|
35
|
+
def __init__(self, sample=None, scanner=None, method=None):
|
|
36
|
+
object.__setattr__(self, "sample", sample)
|
|
37
|
+
object.__setattr__(self, "scanner", scanner)
|
|
38
|
+
object.__setattr__(self, "method", method)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def lower(op):
|
|
42
|
+
"""Step of an operator (run context: Context, (sample, scanner) or None)"""
|
|
43
|
+
if not isinstance(op, Operator):
|
|
44
|
+
raise ValueError(f"Invalid operator: {op}")
|
|
45
|
+
|
|
46
|
+
def params(context):
|
|
47
|
+
return op.normalize(*_unpack(context))
|
|
48
|
+
|
|
49
|
+
acquire = None
|
|
50
|
+
if isinstance(op, Probe):
|
|
51
|
+
|
|
52
|
+
def acquire(state, context):
|
|
53
|
+
return op.acquire(state, *_unpack(context))
|
|
54
|
+
|
|
55
|
+
return compiler.Step(op.kernel, params, acquire, repr(op), source=op)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compile_sequence(
|
|
59
|
+
sequence,
|
|
60
|
+
*,
|
|
61
|
+
max_unroll=compiler.MAX_UNROLL,
|
|
62
|
+
checkpoint=False,
|
|
63
|
+
merge=False,
|
|
64
|
+
passes=(),
|
|
65
|
+
):
|
|
66
|
+
"""compile a (nested) list of operators (cf. compiler.build)
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
sequence: (nested) list of operators; repeated blocks are consecutive
|
|
70
|
+
sub-lists with the same structure (e.g. `[exc] + [block] * N`)
|
|
71
|
+
max_unroll, checkpoint: cf. compiler.build
|
|
72
|
+
merge: merge consecutive affine operators (RF pulses, relaxation,
|
|
73
|
+
precession, spoilers) into single maps, including repeated blocks
|
|
74
|
+
passes: other passes (Program -> Program), applied after merging
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Program, run with `program.run(state, Context(sample, scanner, method))`
|
|
78
|
+
"""
|
|
79
|
+
passes = ((merging.merge(AffineAlgebra),) if merge else ()) + tuple(passes)
|
|
80
|
+
return compiler.build(
|
|
81
|
+
sequence,
|
|
82
|
+
lower=lower,
|
|
83
|
+
passes=passes,
|
|
84
|
+
max_unroll=max_unroll,
|
|
85
|
+
checkpoint=checkpoint,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _unpack(context):
|
|
90
|
+
"""(sample, scanner) from the run context"""
|
|
91
|
+
if context is None:
|
|
92
|
+
return None, None
|
|
93
|
+
elif isinstance(context, Context):
|
|
94
|
+
return context.sample, context.scanner
|
|
95
|
+
return context
|
epgx/merging.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Merging pass: consecutive operators composed into single maps
|
|
2
|
+
|
|
3
|
+
A compiler pass (cf. compiler) parameterized by a map algebra, e.g.
|
|
4
|
+
`kernels.affine.AffineAlgebra` (affine maps on (F+, F-, Z)). An algebra provides:
|
|
5
|
+
|
|
6
|
+
mergeable(op): True if the operator has a map in this algebra
|
|
7
|
+
build(op, params, context): map of an operator from its kernel parameters
|
|
8
|
+
and the run context (e.g. the method, for maps depending on the
|
|
9
|
+
representation of the states)
|
|
10
|
+
apply(state, map): kernel applying a map to the states
|
|
11
|
+
compose(*maps): composition, first applied first
|
|
12
|
+
power(map, n): map applied n times (static n)
|
|
13
|
+
fold(stacked): composition of stacked maps (leading axis), in order
|
|
14
|
+
|
|
15
|
+
The pass, applied to each level of the program (top level and scan bodies):
|
|
16
|
+
- reduces repeated blocks made of a single mergeable step (after merging
|
|
17
|
+
of the body) to one step: `power` for identical blocks, maps of all
|
|
18
|
+
blocks then `fold` otherwise;
|
|
19
|
+
- composes runs of consecutive mergeable steps (no acquisition) into one
|
|
20
|
+
step; other steps (shifts, probes) are barriers.
|
|
21
|
+
|
|
22
|
+
A merged step is an ordinary step: kernel `algebra.apply`, parameters the
|
|
23
|
+
composed map (computed from the run context when the step runs).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import dataclasses
|
|
27
|
+
import typing
|
|
28
|
+
|
|
29
|
+
import jax
|
|
30
|
+
|
|
31
|
+
from .compiler import Program, Scan, Step, Unrolled, join_unrolled
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass(frozen=True)
|
|
35
|
+
class Merged:
|
|
36
|
+
"""source of a merged step: its map as a function of the run context"""
|
|
37
|
+
|
|
38
|
+
map: typing.Callable
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def merge(algebra):
|
|
42
|
+
"""merging pass (Program -> Program) for a map algebra"""
|
|
43
|
+
|
|
44
|
+
def merge_pass(program):
|
|
45
|
+
segments = [_reduce_scan(segment, algebra) for segment in program.segments]
|
|
46
|
+
segments = [
|
|
47
|
+
Unrolled(_merge_steps(segment.steps, algebra))
|
|
48
|
+
if isinstance(segment, Unrolled)
|
|
49
|
+
else segment
|
|
50
|
+
for segment in join_unrolled(segments)
|
|
51
|
+
]
|
|
52
|
+
return Program(segments)
|
|
53
|
+
|
|
54
|
+
return merge_pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def map_of(step, algebra):
|
|
58
|
+
"""map of a step as a function of the context, or None (not mergeable)"""
|
|
59
|
+
if isinstance(step.source, Merged):
|
|
60
|
+
return step.source.map
|
|
61
|
+
elif step.acquire is not None or not algebra.mergeable(step.source):
|
|
62
|
+
return None
|
|
63
|
+
return lambda context: algebra.build(step.source, step.params(context), context)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def merged_step(map_fn, algebra, label, count):
|
|
67
|
+
"""step applying the map map_fn(context)"""
|
|
68
|
+
|
|
69
|
+
def kernel(state, value):
|
|
70
|
+
return algebra.apply(state, value)
|
|
71
|
+
|
|
72
|
+
def params(context):
|
|
73
|
+
return {"value": map_fn(context)}
|
|
74
|
+
|
|
75
|
+
return Step(kernel, params, None, label, count, Merged(map_fn))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# private
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _merge_steps(steps, algebra):
|
|
83
|
+
"""compose runs of consecutive mergeable steps"""
|
|
84
|
+
merged, run = [], []
|
|
85
|
+
|
|
86
|
+
def flush():
|
|
87
|
+
if len(run) > 1:
|
|
88
|
+
maps = [map_of(step, algebra) for step in run]
|
|
89
|
+
|
|
90
|
+
def composed(context, maps=maps):
|
|
91
|
+
return algebra.compose(*(map_fn(context) for map_fn in maps))
|
|
92
|
+
|
|
93
|
+
count = sum(step.count for step in run)
|
|
94
|
+
merged.append(merged_step(composed, algebra, f"merged({len(run)})", count))
|
|
95
|
+
else:
|
|
96
|
+
merged.extend(run)
|
|
97
|
+
run.clear()
|
|
98
|
+
|
|
99
|
+
for step in steps:
|
|
100
|
+
if map_of(step, algebra) is not None:
|
|
101
|
+
run.append(step)
|
|
102
|
+
else:
|
|
103
|
+
flush()
|
|
104
|
+
merged.append(step)
|
|
105
|
+
flush()
|
|
106
|
+
return merged
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _single_step(program):
|
|
110
|
+
"""the only step of a program, or None"""
|
|
111
|
+
if len(program.segments) != 1 or not isinstance(program.segments[0], Unrolled):
|
|
112
|
+
return None
|
|
113
|
+
steps = program.segments[0].steps
|
|
114
|
+
return steps[0] if len(steps) == 1 else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _reduce_scan(segment, algebra):
|
|
118
|
+
"""Scan of blocks reduced to a single mergeable step -> one merged step"""
|
|
119
|
+
if not isinstance(segment, Scan):
|
|
120
|
+
return segment
|
|
121
|
+
step = _single_step(segment.body)
|
|
122
|
+
if step is None or map_of(step, algebra) is None:
|
|
123
|
+
return segment
|
|
124
|
+
|
|
125
|
+
def block_map(xs, context):
|
|
126
|
+
return map_of(_single_step(segment.body_at(xs)), algebra)(context)
|
|
127
|
+
|
|
128
|
+
def scan_map(context):
|
|
129
|
+
if segment.xs is None:
|
|
130
|
+
# same block repeated: power (log2(length) compositions)
|
|
131
|
+
return algebra.power(block_map(None, context), segment.length)
|
|
132
|
+
# maps of all blocks (vectorized), then composition in order
|
|
133
|
+
stacked = jax.vmap(lambda xs: block_map(xs, context))(segment.xs)
|
|
134
|
+
return algebra.fold(stacked)
|
|
135
|
+
|
|
136
|
+
step = merged_step(scan_map, algebra, f"merged({segment!r})", segment.count)
|
|
137
|
+
return Unrolled([step])
|