pulserver 0.1.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.
Files changed (70) hide show
  1. pulserver/__init__.py +17 -0
  2. pulserver/_apps/SPGR2D.py +206 -0
  3. pulserver/_apps/SPGR3D.py +184 -0
  4. pulserver/_apps/bar.py +15 -0
  5. pulserver/_apps/foo.py +15 -0
  6. pulserver/_cli.py +70 -0
  7. pulserver/_core/__init__.py +12 -0
  8. pulserver/_core/_autoseg.py +128 -0
  9. pulserver/_core/_ceq.py +541 -0
  10. pulserver/_core/_sequence.py +313 -0
  11. pulserver/_server/__init__.py +6 -0
  12. pulserver/_server/_server.py +197 -0
  13. pulserver/_version.py +16 -0
  14. pulserver/blocks/__init__.py +16 -0
  15. pulserver/blocks/_grad/_make_spiral.py +201 -0
  16. pulserver/blocks/_misc/__init__.py +7 -0
  17. pulserver/blocks/_misc/_make_spoiler_gradient.py +64 -0
  18. pulserver/blocks/_phaseenc/__init__.py +7 -0
  19. pulserver/blocks/_phaseenc/_make_phase_blip.py +0 -0
  20. pulserver/blocks/_phaseenc/_make_phase_encoding.py +49 -0
  21. pulserver/blocks/_readout/__init__.py +9 -0
  22. pulserver/blocks/_readout/_make_line_readout.py +149 -0
  23. pulserver/blocks/_readout/_make_spiral_readout.py +105 -0
  24. pulserver/blocks/_readout/rosette_readout.py +6 -0
  25. pulserver/blocks/_rfpulse/__init__.py +221 -0
  26. pulserver/blocks/_rfpulse/_make_spsp_pulse.py +196 -0
  27. pulserver/blocks/_rfpulse/_plot.py +164 -0
  28. pulserver/blocks/_rfpulse/_spsp/__init__.py +11 -0
  29. pulserver/blocks/_rfpulse/_spsp/_slr.py +444 -0
  30. pulserver/blocks/_rfpulse/_spsp/_utils.py +99 -0
  31. pulserver/blocks/_rfpulse/_spsp/rf_ripple.py +75 -0
  32. pulserver/blocks/_rfpulse/_spsp/spec_interp.py +35 -0
  33. pulserver/blocks/_rfpulse/_spsp/ss_alias.py +316 -0
  34. pulserver/blocks/_rfpulse/_spsp/ss_design.py +493 -0
  35. pulserver/blocks/_rfpulse/_spsp/ss_ep.py +332 -0
  36. pulserver/blocks/_rfpulse/_spsp/ss_filters.py +2079 -0
  37. pulserver/blocks/_rfpulse/_spsp/ss_flyback.py +441 -0
  38. pulserver/blocks/_rfpulse/_spsp/ss_fourier.py +90 -0
  39. pulserver/blocks/_rfpulse/_spsp/ss_globals.py +43 -0
  40. pulserver/blocks/_rfpulse/_spsp/ss_grad.py +239 -0
  41. pulserver/blocks/_rfpulse/_spsp/ss_spect_correct.py +57 -0
  42. pulserver/blocks/_rfpulse/_spsp/ss_verse.py +166 -0
  43. pulserver/plan/__init__.py +32 -0
  44. pulserver/plan/_cartesian2D.py +151 -0
  45. pulserver/plan/_cartesian3D.py +157 -0
  46. pulserver/plan/_iterators/__init__.py +19 -0
  47. pulserver/plan/_iterators/_base.py +69 -0
  48. pulserver/plan/_iterators/_cartesian2D_iterator.py +173 -0
  49. pulserver/plan/_iterators/_cartesian3D_iterator.py +137 -0
  50. pulserver/plan/_iterators/_noncartesian2D_iterator.py +174 -0
  51. pulserver/plan/_iterators/_noncartesian3D_iterator.py +119 -0
  52. pulserver/plan/_iterators/_stack3D_iterator.py +173 -0
  53. pulserver/plan/_noncartesian2D.py +139 -0
  54. pulserver/plan/_noncartesian3D.py +94 -0
  55. pulserver/plan/_ordering.py +55 -0
  56. pulserver/plan/_phase_cycling.py +87 -0
  57. pulserver/plan/_sampling/__init__.py +14 -0
  58. pulserver/plan/_sampling/_grid_sampling.py +180 -0
  59. pulserver/plan/_sampling/_partial_fourier.py +52 -0
  60. pulserver/plan/_sampling/_poisson_sampling.py +275 -0
  61. pulserver/plan/_sampling/_tilt.py +48 -0
  62. pulserver/plan/_stack3D.py +136 -0
  63. pulserver/sequences/__init__.py +5 -0
  64. pulserver/sequences/_design_2D_spgr.py +211 -0
  65. pulserver/sequences/_design_3D_spgr.py +184 -0
  66. pulserver-0.1.0.dist-info/LICENSE.txt +21 -0
  67. pulserver-0.1.0.dist-info/METADATA +95 -0
  68. pulserver-0.1.0.dist-info/RECORD +70 -0
  69. pulserver-0.1.0.dist-info/WHEEL +5 -0
  70. pulserver-0.1.0.dist-info/top_level.txt +1 -0
pulserver/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """Pulseforge public API."""
2
+
3
+ __all__ = []
4
+
5
+ from pypulseq import Opts # noqa
6
+
7
+ from . import blocks # noqa
8
+ from . import plan # noqa
9
+ from . import sequences # noqa
10
+
11
+ from . import _server # noqa
12
+
13
+ from ._core import Sequence # noqa
14
+ from ._core import SequenceParams # noqa
15
+
16
+ __all__.append("Opts")
17
+ __all__.extend(["Sequence", "SequenceParams"])
@@ -0,0 +1,206 @@
1
+ """2D Spoiled Gradient Echo sequence."""
2
+
3
+ __all__ = ["SPGR2D"]
4
+
5
+ from collections.abc import Iterable
6
+
7
+
8
+ import numpy as np
9
+ import pypulseq as pp
10
+
11
+
12
+ from pulserver import Sequence
13
+ from pulserver.plan import RfPhaseCycle
14
+
15
+
16
+ def SPGR2D(
17
+ fov: Iterable[float],
18
+ slice_thickness: float,
19
+ npix: Iterable[int],
20
+ nslices: int,
21
+ alpha: float,
22
+ rectime: float,
23
+ max_grad: float,
24
+ max_slew: float,
25
+ raster_time: float,
26
+ slice_spacing: float = 0.0,
27
+ seqformat: str | bool = "bytes",
28
+ ):
29
+ """
30
+ Generate a 2D Spoiled Gradient Recalled Echo (SPGR) pulse sequence.
31
+
32
+ This function designs a 2D SPGR sequence based on the provided field of view (FOV), matrix size,
33
+ number of slices, slice thickness and spacing, flip angle, recovery time,
34
+ and hardware constraints such as maximum gradient amplitude and slew rate.
35
+ The output can be formatted in different sequence file formats if specified.
36
+
37
+ Parameters
38
+ ----------
39
+ fov : Iterable[float]
40
+ Field of view along each spatial dimension [fov_x, fov_y] in mm.
41
+ If scalar, assume squared fov.
42
+ slice_thickness : float)
43
+ Slice thickness in mm.
44
+ npix : Iterable[int]
45
+ Number of voxels along each spatial dimension [nx, ny] (matrix size).
46
+ If scalar, assume squared matrix size.
47
+ nslices : int
48
+ Number of slices.
49
+ alpha : float
50
+ Flip angle in degrees.
51
+ rectime : float
52
+ Recovery time after spoiling in ms.
53
+ max_grad : float
54
+ Maximum gradient amplitude in mT/m.
55
+ max_slew : float
56
+ Maximum gradient slew rate in T/m/s.
57
+ raster_time : float
58
+ Waveform raster time in seconds (the time between successive gradient samples).
59
+ slice_spacing : float, optional
60
+ Additional slice spacing in mm. The default is 0.0 (contiguous slices).
61
+ seqformat : str or bool, optional
62
+ Output sequence format. If a string is provided, it specifies the desired output format (e.g., 'pulseq', 'bytes').
63
+ If False, the sequence is returned as an internal object. Default is False.
64
+
65
+ Returns
66
+ -------
67
+ seq : object or dict
68
+ The generated SPGR sequence. If `seqformat` is a string, the sequence is returned in the specified format.
69
+ If `seqformat` is False, the sequence is returned as an internal representation.
70
+
71
+ Notes
72
+ -----
73
+ - This function is designed to work within the constraints of MRI scanners, taking into account the physical limits
74
+ on gradient amplitude and slew rates.
75
+ - The flip angle (`alpha`) controls the excitation of spins and directly impacts the signal-to-noise ratio (SNR) and contrast.
76
+
77
+ Examples
78
+ --------
79
+ Generate a 2D SPGR sequence for a single 5 mm thick slice and 240x240 mm FOV, 256x256 matrix size,
80
+ 15-degree flip angle, 5s recovery time and hardware limits 4mT/m, 150T/m/s, 4e-6 s raster time as:
81
+
82
+ >>> from pulseforge import SPGR2D
83
+ >>> SPGR2D(240.0, 5.0, 256, 1, 15.0, 5.0, 40, 150, 4e-6)
84
+
85
+ Generate the same sequence and export it in bytes format:
86
+
87
+ >>> SPGR2D(240.0, 5.0, 256, 1, 15.0, 5.0, 40, 150, 4e-6, format='bytes')
88
+
89
+ """
90
+ # RF specs
91
+ rf_spoiling_inc = 117.0 # RF spoiling increment
92
+
93
+ # initialize system limits
94
+ system = pp.Opts(
95
+ max_grad=max_grad,
96
+ grad_unit="mT/m",
97
+ max_slew=max_slew,
98
+ slew_unit="T/m/s",
99
+ grad_raster_time=raster_time,
100
+ rf_raster_time=raster_time,
101
+ )
102
+
103
+ # initialize sequence
104
+ seq = Sequence(system=system, format=seqformat)
105
+
106
+ # initialize prescription
107
+ fov, slice_thickness, slice_spacing = (
108
+ fov * 1e-3,
109
+ slice_thickness * 1e-3,
110
+ slice_spacing * 1e-3,
111
+ )
112
+ slice_spacing += slice_thickness
113
+
114
+ if np.isscalar(npix):
115
+ Nx, Ny, Nz = npix, npix, nslices # in-plane resolution, slice thickness
116
+ else:
117
+ Nx, Ny, Nz = npix[0], npix[1], nslices # in-plane resolution, slice thickness
118
+
119
+ # initialize event events
120
+ # RF pulse
121
+ rf, gss, _ = pp.make_sinc_pulse(
122
+ flip_angle=np.deg2rad(alpha),
123
+ duration=3e-3,
124
+ slice_thickness=slice_thickness,
125
+ apodization=0.42,
126
+ time_bw_product=4,
127
+ system=system,
128
+ return_gz=True,
129
+ )
130
+ gss_reph = pp.make_trapezoid(
131
+ channel="z", area=-gss.area / 2, duration=1e-3, system=system
132
+ )
133
+ seq.register_block(name="excitation", rf=rf, gz=gss)
134
+ seq.register_block(name="slab_rephasing", gz=gss_reph)
135
+
136
+ # readout
137
+ delta_kx, delta_ky = 1 / fov, 1 / fov
138
+ g_read = pp.make_trapezoid(
139
+ channel="x", flat_area=Nx * delta_kx, flat_time=3.2e-3, system=system
140
+ )
141
+ adc = pp.make_adc(
142
+ num_samples=Nx, duration=g_read.flat_time, delay=g_read.rise_time, system=system
143
+ )
144
+ seq.register_block("dummy_readout", gx=g_read)
145
+ seq.register_block("readout", gx=g_read, adc=adc)
146
+
147
+ # phase encoding
148
+ gx_phase = pp.make_trapezoid(
149
+ channel="x", area=-g_read.area / 2, duration=1e-3, system=system
150
+ )
151
+ gy_phase = pp.make_trapezoid(channel="y", area=delta_ky * Ny, system=system)
152
+ seq.register_block("g_phase", gx=gx_phase, gy=gy_phase)
153
+
154
+ # crusher gradient
155
+ gz_spoil = pp.make_trapezoid(channel="z", area=32 / slice_thickness, system=system)
156
+ delay = pp.make_delay(rectime)
157
+ seq.register_block("g_spoil", gz=gz_spoil, delay=delay)
158
+
159
+ # phase encoding plan TODO: helper routine
160
+ pey_steps = ((np.arange(Ny)) - (Ny / 2)) / Ny
161
+ FOVz = Nz * slice_spacing
162
+ foff_steps = np.linspace(-FOVz / 2, FOVz / 2, Nz) * gss.amplitude
163
+ encoding_plan = np.meshgrid(pey_steps, foff_steps, indexing="xy")
164
+ encoding_plan = [enc.ravel() for enc in encoding_plan]
165
+
166
+ # scan duration
167
+ dummy_scans = 10
168
+ calib_scans = 10
169
+ imaging_scans = Ny * nslices
170
+
171
+ # generate rf phases
172
+ rf_phases = RfPhaseCycle(
173
+ num_pulses=dummy_scans + imaging_scans + calib_scans,
174
+ phase_increment=rf_spoiling_inc,
175
+ )
176
+
177
+ # construct sequence
178
+ seq.section(name="ss_prep")
179
+ for n in range(dummy_scans):
180
+ rf_phase = rf_phases()
181
+ seq.add_block("excitation")
182
+ seq.add_block("slab_rephasing")
183
+ seq.add_block("g_phase", gy_amp=0.0)
184
+ seq.add_block("dummy_readout", rf_phase=rf_phase)
185
+ seq.add_block("g_phase", gy_amp=0.0)
186
+ seq.add_block("g_spoil")
187
+
188
+ seq.section(name="scan_loop")
189
+ for n in range(imaging_scans + calib_scans):
190
+ rf_phase = rf_phases()
191
+
192
+ if n < calib_scans:
193
+ rf_freq = 0.0
194
+ y_amp = 0.0
195
+ else:
196
+ rf_freq = encoding_plan[1][n - calib_scans]
197
+ y_amp = encoding_plan[0][n - calib_scans]
198
+
199
+ seq.add_block("excitation", rf_freq=rf_freq)
200
+ seq.add_block("slab_rephasing")
201
+ seq.add_block("g_phase", gy_amp=y_amp)
202
+ seq.add_block("readout", rf_phase=rf_phase, adc_phase=rf_phase)
203
+ seq.add_block("g_phase", gy_amp=-y_amp)
204
+ seq.add_block("g_spoil")
205
+
206
+ return seq.export()
@@ -0,0 +1,184 @@
1
+ """3D Spoiled Gradient Echo sequence."""
2
+
3
+ __all__ = ["SPGR3D"]
4
+
5
+ from collections.abc import Iterable
6
+
7
+
8
+ import numpy as np
9
+ import pypulseq as pp
10
+
11
+
12
+ from pulserver import Sequence
13
+ from pulserver.plan import RfPhaseCycle
14
+
15
+
16
+ def SPGR3D(
17
+ fov: Iterable[float],
18
+ npix: Iterable[int],
19
+ alpha: float,
20
+ max_grad: float,
21
+ max_slew: float,
22
+ raster_time: float,
23
+ seqformat: str | bool = "bytes",
24
+ ):
25
+ """
26
+ Generate a 3D Spoiled Gradient Recalled Echo (SPGR) pulse sequence.
27
+
28
+ This function designs a 3D SPGR sequence based on the provided field of view (FOV), matrix size,
29
+ flip angle, and hardware constraints such as maximum gradient amplitude and slew rate. The output
30
+ can be formatted in different sequence file formats if specified.
31
+
32
+ Parameters
33
+ ----------
34
+ fov : Iterable[float]
35
+ Field of view along each spatial dimension [fov_plane, fov_z] in mm.
36
+ If scalar, assume cubic fov.
37
+ npix : Iterable[int]
38
+ Number of voxels along each spatial dimension [plane_mtx, nz] (matrix size).
39
+ If scalar, assume cubic matrix size.
40
+ alpha : float
41
+ Flip angle in degrees.
42
+ max_grad : float
43
+ Maximum gradient amplitude in mT/m.
44
+ max_slew : float
45
+ Maximum gradient slew rate in T/m/s.
46
+ raster_time : float
47
+ Waveform raster time in seconds (the time between successive gradient samples).
48
+ seqformat : str or bool, optional
49
+ Output sequence format. If a string is provided, it specifies the desired output format (e.g., 'pulseq', 'bytes').
50
+ If False, the sequence is returned as an internal object. Default is False.
51
+
52
+ Returns
53
+ -------
54
+ seq : object or dict
55
+ The generated SPGR sequence. If `seqformat` is a string, the sequence is returned in the specified format.
56
+ If `seqformat` is False, the sequence is returned as an internal representation.
57
+
58
+ Notes
59
+ -----
60
+ - This function is designed to work within the constraints of MRI scanners, taking into account the physical limits
61
+ on gradient amplitude and slew rates.
62
+ - The flip angle (`alpha`) controls the excitation of spins and directly impacts the signal-to-noise ratio (SNR) and contrast.
63
+
64
+ Examples
65
+ --------
66
+ Generate a 3D SPGR sequence for a 256x256x128 matrix with a 240x240x120 mm FOV, a 15-degree flip angle, and hardware limits:
67
+
68
+ >>> from pulseforge import SPGR3D
69
+ >>> SPGR3D([240, 120], [256, 128], 15, 0.04, 150, 4e-6)
70
+
71
+ Generate the same sequence and export it in bytes format:
72
+
73
+ >>> SPGR3D([240, 120], [256, 128], 15, 0.04, 150, 4e-6, seqformat='bytes')
74
+
75
+ """
76
+ # RF specs
77
+ rf_spoiling_inc = 117.0 # RF spoiling increment
78
+
79
+ # initialize system limits
80
+ system = pp.Opts(
81
+ max_grad=max_grad,
82
+ grad_unit="mT/m",
83
+ max_slew=max_slew,
84
+ slew_unit="T/m/s",
85
+ grad_raster_time=raster_time,
86
+ rf_raster_time=raster_time,
87
+ )
88
+
89
+ # initialize sequence
90
+ seq = Sequence(system=system, format=seqformat)
91
+
92
+ # initialize prescription
93
+ if np.isscalar(fov):
94
+ fov, slab_thickness = fov * 1e-3, fov * 1e-3 # isotropic
95
+ else:
96
+ fov, slab_thickness = (
97
+ fov[0] * 1e-3,
98
+ fov[1] * 1e-3,
99
+ ) # in-plane FOV, slab thickness
100
+
101
+ if np.isscalar(npix):
102
+ Nx, Ny, Nz = npix, npix, npix # in-plane resolution, slice thickness
103
+ else:
104
+ Nx, Ny, Nz = npix[0], npix[0], npix[1] # in-plane resolution, slice thickness
105
+
106
+ # initialize event events
107
+ # RF pulse
108
+ rf, gss, _ = pp.make_sinc_pulse(
109
+ flip_angle=np.deg2rad(alpha),
110
+ duration=3e-3,
111
+ slice_thickness=slab_thickness,
112
+ apodization=0.42,
113
+ time_bw_product=4,
114
+ system=system,
115
+ return_gz=True,
116
+ )
117
+ gss_reph = pp.make_trapezoid(
118
+ channel="z", area=-gss.area / 2, duration=1e-3, system=system
119
+ )
120
+ seq.register_block(name="excitation", rf=rf, gz=gss)
121
+ seq.register_block(name="slab_rephasing", gz=gss_reph)
122
+
123
+ # readout
124
+ delta_kx, delta_ky, delta_kz = 1 / fov, 1 / fov, 1 / slab_thickness
125
+ g_read = pp.make_trapezoid(
126
+ channel="x", flat_area=Nx * delta_kx, flat_time=3.2e-3, system=system
127
+ )
128
+ adc = pp.make_adc(
129
+ num_samples=Nx, duration=g_read.flat_time, delay=g_read.rise_time, system=system
130
+ )
131
+ seq.register_block("dummy_readout", gx=g_read)
132
+ seq.register_block("readout", gx=g_read, adc=adc)
133
+
134
+ # phase encoding
135
+ gx_phase = pp.make_trapezoid(
136
+ channel="x", area=-g_read.area / 2, duration=1e-3, system=system
137
+ )
138
+ gy_phase = pp.make_trapezoid(channel="y", area=delta_ky * Ny, system=system)
139
+ gz_phase = pp.make_trapezoid(channel="z", area=delta_kz * Nz, system=system)
140
+ seq.register_block("g_phase", gx=gx_phase, gy=gy_phase, gz=gz_phase)
141
+
142
+ # crusher gradient
143
+ gz_spoil = pp.make_trapezoid(channel="z", area=32 / slab_thickness, system=system)
144
+ seq.register_block("g_spoil", gz=gz_spoil)
145
+
146
+ # phase encoding plan TODO: helper routine
147
+ pey_steps = ((np.arange(Ny)) - (Ny / 2)) / Ny
148
+ pez_steps = ((np.arange(Nz)) - (Nz / 2)) / Nz
149
+ encoding_plan = np.meshgrid(pey_steps, pez_steps, indexing="xy")
150
+ encoding_plan = [enc.ravel() for enc in encoding_plan]
151
+
152
+ # scan duration
153
+ dummy_scans = Ny
154
+ imaging_scans = Ny * Nz
155
+
156
+ # generate rf phases
157
+ rf_phases = RfPhaseCycle(
158
+ num_pulses=dummy_scans + imaging_scans, phase_increment=rf_spoiling_inc
159
+ )
160
+
161
+ # construct sequence
162
+ seq.section(name="ss_prep")
163
+ for n in range(dummy_scans):
164
+ rf_phase = rf_phases()
165
+ seq.add_block("excitation")
166
+ seq.add_block("slab_rephasing")
167
+ seq.add_block("g_phase", gy_amp=0.0, gz_amp=0.0)
168
+ seq.add_block("dummy_readout", rf_phase=rf_phase)
169
+ seq.add_block("g_phase", gy_amp=0.0, gz_amp=0.0)
170
+ seq.add_block("g_spoil")
171
+
172
+ seq.section(name="scan_loop")
173
+ for n in range(imaging_scans):
174
+ rf_phase = rf_phases()
175
+ seq.add_block("excitation")
176
+ seq.add_block("slab_rephasing")
177
+ seq.add_block("g_phase", gy_amp=encoding_plan[0][n], gz_amp=encoding_plan[1][n])
178
+ seq.add_block("readout", rf_phase=rf_phase, adc_phase=rf_phase)
179
+ seq.add_block(
180
+ "g_phase", gy_amp=-encoding_plan[0][n], gz_amp=-encoding_plan[1][n]
181
+ )
182
+ seq.add_block("g_spoil")
183
+
184
+ return seq.export()
pulserver/_apps/bar.py ADDED
@@ -0,0 +1,15 @@
1
+ """Another demo function."""
2
+
3
+
4
+ def bar(arg1, arg2):
5
+ """
6
+ Another function.
7
+
8
+ Parameters
9
+ ----------
10
+ arg1 : int
11
+ A parameter.
12
+ arg2 : float
13
+ Another parameter.
14
+
15
+ """
pulserver/_apps/foo.py ADDED
@@ -0,0 +1,15 @@
1
+ """A demo function."""
2
+
3
+
4
+ def foo(arg1, arg2):
5
+ """
6
+ A function.
7
+
8
+ Parameters
9
+ ----------
10
+ arg1 : int
11
+ A parameter.
12
+ arg2 : float
13
+ Another parameter.
14
+
15
+ """
pulserver/_cli.py ADDED
@@ -0,0 +1,70 @@
1
+ """Command Line Interface for server."""
2
+
3
+ import os
4
+
5
+ import click
6
+
7
+ from . import _server
8
+
9
+
10
+ @click.group()
11
+ def cli():
12
+ pass
13
+
14
+
15
+ @click.command()
16
+ @click.option("--config-path", default=None, help="Location of config.yml file.")
17
+ @click.option("--scanner-address", default=None, help="IP address of MR scanner.")
18
+ @click.option("--scanner-port", default=None, help="MR scanner port.")
19
+ @click.option(
20
+ "--recon-server-address", default=None, help="IP address of reconstruction servr."
21
+ )
22
+ @click.option("--recon-server-port", default=None, help="Reconstruction server port.")
23
+ def start(
24
+ config_path, scanner_address, scanner_port, recon_server_address, recon_server_port
25
+ ):
26
+ """
27
+ Start sequence design server.
28
+ """
29
+ if config_path is not None:
30
+ os.environ["PULSEFORGE_CONFIG"] = config_path
31
+ else:
32
+ if scanner_address is not None:
33
+ os.environ["PULSEFORGE_SCANNER_ADDRESS"] = scanner_address
34
+ if scanner_port is not None:
35
+ os.environ["PULSEFORGE_SCANNER_PORT"] = scanner_port
36
+ if recon_server_address is not None:
37
+ os.environ["PULSEFORGE_RECON_SERVER_ADDRESS"] = recon_server_address
38
+ if recon_server_port is not None:
39
+ os.environ["PULSEFORGE_RECON_SERVER_PORT"] = recon_server_port
40
+
41
+ # start the server
42
+ _server.start_server()
43
+
44
+
45
+ @click.command()
46
+ @click.option(
47
+ "--name",
48
+ default=None,
49
+ help="Function whose docstring we want to see. If not provided, print list of available commands.",
50
+ )
51
+ def apps(name):
52
+ """
53
+ Design function documentations
54
+ """
55
+ # Load plugin list
56
+ plugins = _server.load_plugins()
57
+ if name is None:
58
+ for fun in plugins.values():
59
+ name = fun.__name__
60
+ summary = fun.__doc__.split("\n")[1]
61
+ click.echo(name + "\t" + summary)
62
+ else:
63
+ click.echo(plugins[name].__doc__)
64
+
65
+
66
+ cli.add_command(start)
67
+ cli.add_command(apps)
68
+
69
+ if __name__ == "__main__":
70
+ cli()
@@ -0,0 +1,12 @@
1
+ """Core Sequence representations.
2
+
3
+ This is an intermediate representation to handle both
4
+ native pypulseq Sequence and Ceq structure.
5
+ """
6
+
7
+ __all__ = []
8
+
9
+ from ._ceq import SequenceParams # noqa
10
+ from ._sequence import Sequence # noqa
11
+
12
+ __all__.extend(["Sequence", "SequenceParams"])
@@ -0,0 +1,128 @@
1
+ """Automatic segment definition subroutines."""
2
+
3
+ __all__ = ["find_segment_definitions", "find_segments", "split_rotated_segments"]
4
+
5
+ import numpy as np
6
+ import numba as nb
7
+
8
+
9
+ def find_segment_definitions(arr):
10
+ # here, we make the (reasonable?) assumption
11
+ # that all the "prescan", dummy / syncronization / calibration
12
+ # scans are performed at the beginning of the sequence,
13
+ # then we have a long periodic main loop. We want to find the beginning
14
+ # of the main loop
15
+ main_loop, dummy_loop = _find_periodic_pattern(arr.astype(int))
16
+
17
+ if len(dummy_loop) > 0: # TODO: make this recursive?
18
+ # here, we make the (reasonable?) assumption
19
+ # that all the "prescan", dummy / syncronization / calibration
20
+ # scans are also periodic
21
+ dummy_loop, rem = _find_periodic_pattern(dummy_loop.astype(int))
22
+
23
+ # hopefully rem is empty; if it is not, append at the beginning
24
+ if len(rem) > 0 and len(dummy_loop) == 0:
25
+ dummy_loop = [rem.tolist()]
26
+ elif len(rem) > 0:
27
+ dummy_loop = [rem.tolist() + dummy_loop.tolist()]
28
+ else:
29
+ dummy_loop = [dummy_loop.tolist()]
30
+ if len(main_loop) > 0:
31
+ return dummy_loop + [main_loop.tolist()]
32
+ return dummy_loop
33
+
34
+ return [main_loop.tolist()]
35
+
36
+
37
+ def find_segments(array, subarray):
38
+ arr = nb.typed.List(array)
39
+ subarr = nb.typed.List(subarray)
40
+ return list(_find_segments(arr, subarr))
41
+
42
+
43
+ @nb.njit(cache=True, fastmath=True)
44
+ def _find_segments(array, subarray):
45
+ n = len(array)
46
+ m = len(subarray)
47
+ result = np.zeros(n, dtype=np.bool_)
48
+
49
+ for i in range(n - m + 1):
50
+ match = True
51
+ for j in range(m):
52
+ if array[i + j] != subarray[j]:
53
+ match = False
54
+ break
55
+ if match:
56
+ for k in range(m):
57
+ result[i + k] = True
58
+
59
+ return result
60
+
61
+
62
+ def split_rotated_segments(input):
63
+ output = []
64
+ for segment in input:
65
+ tmp = _split_signed_blocks(segment)
66
+ for item in tmp:
67
+ output.append(item)
68
+ return output
69
+
70
+
71
+ # %% local utils
72
+ def _principal_period(s):
73
+ i = (s + s).find(s, 1, -1)
74
+ return None if i == -1 else s[:i]
75
+
76
+
77
+ def _find_periodic_pattern(arr):
78
+ numel = len(arr)
79
+ loop = _principal_period(arr.tobytes())
80
+
81
+ for start in range(numel):
82
+ loop = _principal_period(arr[start:].tobytes())
83
+ if loop is not None:
84
+ break
85
+
86
+ if loop is not None:
87
+ loop = np.frombuffer(loop, dtype=int)
88
+ remainder = np.frombuffer(arr[:start], dtype=int)
89
+ else:
90
+ loop = np.frombuffer(arr, dtype=int)
91
+ remainder = np.asarray([], dtype=int)
92
+ return loop, remainder
93
+
94
+
95
+ # Here we make the (I think) reasonable assumption that rotated blocks with different angles
96
+ # (e.g., two consecutive (spiral arm, spiral rewinder) pairs with different angles)
97
+ # are separated by non rotated blocks (e.g., RF pulses or z-spoilers)
98
+ def _split_signed_blocks(arr):
99
+ if not arr:
100
+ return []
101
+
102
+ # Convert the input list to a numpy array
103
+ arr = np.array(arr)
104
+
105
+ # Create a sign array where positive numbers are marked as 1, negative as -1
106
+ sign = np.sign(arr)
107
+ sign[sign == 0] = 1
108
+
109
+ # Find the indices where the sign changes
110
+ change_indices = np.where(np.diff(sign) != 0)[0] + 1
111
+
112
+ # Split the array at the change indices
113
+ split_blocks = np.split(arr, change_indices)
114
+
115
+ # Convert the numpy arrays in the list to python lists
116
+ result = [list(abs(block)) for block in split_blocks]
117
+
118
+ # Get sizes of each block
119
+ osize = np.asarray([len(block) for block in result])
120
+ max_size = max(osize)
121
+ tmp = [np.pad(result[n], (0, max_size - osize[n])) for n in range(len(result))]
122
+ tmp = np.stack(tmp, axis=0)
123
+ tmp, idx = np.unique(tmp, return_index=True, axis=0)
124
+ tmp = tmp[np.argsort(idx)]
125
+ osize = osize[np.sort(idx)]
126
+ result = [tmp[n][: osize[n]].tolist() for n in range(len(osize))]
127
+
128
+ return result