diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..3f2340c --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,19 @@ +name: pre-commit + +on: + pull_request: + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + + # Cancel in-progress runs when a new workflow with the same group name is triggered + cancel-in-progress: true diff --git a/.gitignore b/.gitignore index 1bfa44c..38e55cb 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ venv.bak/ # PyCharm config folder \.idea/ + +# VS Code config folder +\.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d055eb5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +default_language_version: + python: python3 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + # - id: check-docstring-first + - id: check-merge-conflict + - id: check-yaml + - id: check-toml + - id: check-json + exclude: ^.vscode/ + - id: mixed-line-ending + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff # linter + args: [--fix] + - id: ruff-format # formatter + + - repo: https://github.com/crate-ci/typos + rev: v1.25.0 + hooks: + - id: typos + exclude: ^pypulseq/seq_examples/|paper.md + +ci: + autofix_commit_msg: | + [pre-commit] auto fixes from pre-commit hooks + autofix_prs: true + autoupdate_branch: "" + autoupdate_commit_msg: "[pre-commit] pre-commit autoupdate" + autoupdate_schedule: monthly + skip: [] + submodules: false diff --git a/doc/source/conf.py b/doc/source/conf.py index 6fc2d72..680ef12 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,6 +12,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath('../../')) @@ -30,9 +31,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ -'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'recommonmark' -] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'recommonmark'] source_suffix = { '.rst': 'restructuredtext', diff --git a/doc/walkthrough/gre_walkthrough.ipynb b/doc/walkthrough/gre_walkthrough.ipynb index 4a591ae..b1c0b75 100644 --- a/doc/walkthrough/gre_walkthrough.ipynb +++ b/doc/walkthrough/gre_walkthrough.ipynb @@ -48,18 +48,25 @@ "outputs": [], "source": [ "seq = Sequence()\n", - "fov = 256e-3 # field of view\n", - "Nx = 256 # number of frequency encodes\n", - "Ny = 256 # number of phase encodes\n", - "alpha = 10 # RF flip\n", + "fov = 256e-3 # field of view\n", + "Nx = 256 # number of frequency encodes\n", + "Ny = 256 # number of phase encodes\n", + "alpha = 10 # RF flip\n", "slice_thickness = 3e-3\n", - "TE = 7.38e-3 # echo time\n", - "TR = 100e-3 # repetition time\n", + "TE = 7.38e-3 # echo time\n", + "TR = 100e-3 # repetition time\n", "\n", "rf_spoiling_inc = 117\n", "\n", - "sys = Opts(max_grad=28, grad_unit='mT/m', max_slew=150, slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6,\n", - " adc_dead_time=10e-6)" + "sys = Opts(\n", + " max_grad=28,\n", + " grad_unit='mT/m',\n", + " max_slew=150,\n", + " slew_unit='T/m/s',\n", + " rf_ringdown_time=20e-6,\n", + " rf_dead_time=100e-6,\n", + " adc_dead_time=10e-6,\n", + ")" ] }, { @@ -88,23 +95,36 @@ } ], "source": [ - "rf, gz, gzr = make_sinc_pulse(flip_angle=alpha * math.pi / 180, duration=4e-3, slice_thickness=slice_thickness,\n", - " apodization=0.5, time_bw_product=4, system=sys, return_gz=True)\n", + "rf, gz, gzr = make_sinc_pulse(\n", + " flip_angle=alpha * math.pi / 180,\n", + " duration=4e-3,\n", + " slice_thickness=slice_thickness,\n", + " apodization=0.5,\n", + " time_bw_product=4,\n", + " system=sys,\n", + " return_gz=True,\n", + ")\n", "\n", "delta_k = 1 / fov\n", "gx = make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=6.4e-3, system=sys)\n", "adc = make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=sys)\n", "gx_pre = make_trapezoid(channel='x', area=-gx.area / 2, duration=2e-3, system=sys)\n", "gz_reph = make_trapezoid(channel='z', area=-gz.area / 2, duration=2e-3, system=sys)\n", - "phase_areas = (np.arange(Ny) - Ny / 2) * delta_k\n", + "phase_areas = (np.arrange(Ny) - Ny / 2) * delta_k\n", "\n", "gx_spoil = make_trapezoid(channel='x', area=2 * Nx * delta_k, system=sys)\n", "gz_spoil = make_trapezoid(channel='z', area=4 / slice_thickness, system=sys)\n", "\n", - "delay_TE = math.ceil((TE - calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - calc_duration(\n", - " gx) / 2) / seq.grad_raster_time) * seq.grad_raster_time\n", - "delay_TR = math.ceil((TR - calc_duration(gx_pre) - calc_duration(gz) - calc_duration(\n", - " gx) - delay_TE) / seq.grad_raster_time) * seq.grad_raster_time\n", + "delay_TE = (\n", + " math.ceil(\n", + " (TE - calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - calc_duration(gx) / 2) / seq.grad_raster_time\n", + " )\n", + " * seq.grad_raster_time\n", + ")\n", + "delay_TR = (\n", + " math.ceil((TR - calc_duration(gx_pre) - calc_duration(gz) - calc_duration(gx) - delay_TE) / seq.grad_raster_time)\n", + " * seq.grad_raster_time\n", + ")\n", "\n", "assert np.all(delay_TR >= calc_duration(gx_spoil, gz_spoil))" ] @@ -128,7 +148,7 @@ "rf_phase = 0\n", "rf_inc = 0\n", "\n", - "for i in range(Ny): # We have Ny phase encodes\n", + "for i in range(Ny): # We have Ny phase encodes\n", " rf.phase_offset = rf_phase / 180 * np.pi\n", " adc.phase_offset = rf_phase / 180 * np.pi\n", " rf_inc = divmod(rf_inc + rf_spoiling_inc, 360.0)[1]\n", @@ -148,7 +168,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "5. Visualise the constructed pulse sequence by calling the `plot()` command. The `plot()` command visualises the ADC and RF events in one window, and the gradient events in another window. For GRE, the `plot()` command should display two plots that look like this:\n", + "5. Visualize the constructed pulse sequence by calling the `plot()` command. The `plot()` command visualizes the ADC and RF events in one window, and the gradient events in another window. For GRE, the `plot()` command should display two plots that look like this:\n", "\n", "![GRE Plot 1](./gre_1.png)\n", "![GRE Plot 2](./gre_2.png)" diff --git a/pyproject.toml b/pyproject.toml index e91d312..0c1483a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ [project.optional-dependencies] sigpy = ["sigpy>=0.1.26"] -test = ["pytest"] +test = ["pytest", "pre-commit"] [project.urls] Homepage = "https://github.com/imr-framework/pypulseq" @@ -41,6 +41,79 @@ SAR = ["QGlobal.mat"] [tool.setuptools.dynamic] version = { attr = "version.__version__" } +[tool.ruff] +line-length = 120 +extend-exclude = ["__init__.py"] +exclude = ["doc/**", "pypulseq/seq_examples/**"] + +# RUFF section +[tool.ruff.lint] +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "COM", # flake8-commas + # "D", # pydocstyle + # "E", # pycodestyle errors + "F", # Pyflakes + # "FA", # flake8-future-annotations + "I", # isort + # "N", # pep8-naming + "NPY", # NumPy-specific rules + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + # "UP", # pyupgrade + "PIE", # flake8-pie + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "W", # pycodestyle warnings + "YTT", # flake8-2020 + # "ERA", # flake8-eradicate +] + +extend-select = [ + # "ANN001", # type annotation for function argument + # # "ANN201", # return type annonation public function + # # "ANN205", # return type annonation static method + # # "ANN401", # any type annotation + # # "BLE001", # blind exception + # # "D107", # missing docstring in __init__ + # # "D417", # undocumented-parameter +] + +ignore = [ + "B028", # explicit "stacklevel" arg in warnings + "COM812", # missing-trailing-comma (conflict with formatter) + "PTH123", # use of Path.open + "S101", # use of assert + "S307", # use of possibly insecure eval function + "S311", # standard pseudo-random generators + "S324", # insecure hash function + "SIM108", # if-else-block-instead-of-if-exp + "SIM115", # use of context manager +] + +[tool.ruff.lint.isort] +force-single-line = false +split-on-trailing-comma = false + +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "single" + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.format] +quote-style = "single" +skip-magic-trailing-comma = false + +[tool.typos.default] +locale = "en-us" +exclude = ["pypulseq/seq_examples/**"] + # PyTest section [tool.pytest.ini_options] testpaths = ["pypulseq/tests"] diff --git a/pypulseq/SAR/SAR_calc.py b/pypulseq/SAR/SAR_calc.py index 071b44f..d0276c9 100644 --- a/pypulseq/SAR/SAR_calc.py +++ b/pypulseq/SAR/SAR_calc.py @@ -1,15 +1,14 @@ # Copyright of the Board of Trustees of Columbia University in the City of New York from pathlib import Path -from typing import Tuple -from typing import Union +from typing import Tuple, Union import matplotlib.pyplot as plt import numpy as np import scipy.io as sio from scipy import interpolate -from pypulseq.Sequence.sequence import Sequence from pypulseq.calc_duration import calc_duration +from pypulseq.Sequence.sequence import Sequence def _calc_SAR(Q: np.ndarray, I: np.ndarray) -> np.ndarray: @@ -31,7 +30,6 @@ def _calc_SAR(Q: np.ndarray, I: np.ndarray) -> np.ndarray: SAR : numpy.ndarray Contains the SAR value for a particular Q matrix """ - if len(I.shape) == 1: # Just to fit the multi-transmit case for now, TODO I = np.tile(I, (Q.shape[0], 1)) # Nc x Nt @@ -54,19 +52,17 @@ def _load_Q() -> Tuple[np.ndarray, np.ndarray]: Contains the Q-matrix of global SAR values for body-mass and head-mass respectively. """ # Load relevant Q matrices computed from the model - this code will be integrated later - starting from E fields - path_Q = str(Path(__file__).parent / "QGlobal.mat") + path_Q = str(Path(__file__).parent / 'QGlobal.mat') Q = sio.loadmat(path_Q) - Q = Q["Q"] + Q = Q['Q'] val = Q[0, 0] - Qtmf = val["Qtmf"] - Qhmf = val["Qhmf"] + Qtmf = val['Qtmf'] + Qhmf = val['Qhmf'] return Qtmf, Qhmf -def _SAR_from_seq( - seq: Sequence, Qtmf: np.ndarray, Qhmf: np.ndarray -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +def _SAR_from_seq(seq: Sequence, Qtmf: np.ndarray, Qhmf: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Compute global whole body and head only SAR values for the given `seq` object. @@ -102,7 +98,7 @@ def _SAR_from_seq( block_dur = calc_duration(block) t[block_counter - 1] = t_prev + block_dur t_prev = t[block_counter - 1] - if hasattr(block, "rf"): # has rf + if hasattr(block, 'rf'): # has rf rf = block.rf signal = rf.signal # This rf could be parallel transmit as well @@ -176,9 +172,7 @@ def _SAR_lims_check( six_min_threshold_hg = 3.2 ten_sec_threshold_hg = 6.4 - SAR_wbg_lim_app = np.concatenate( - (np.zeros(5), SARwbg_lim_s, np.zeros(5)), axis=0 - ) + SAR_wbg_lim_app = np.concatenate((np.zeros(5), SARwbg_lim_s, np.zeros(5)), axis=0) SAR_hg_lim_app = np.concatenate((np.zeros(5), SARhg_lim_s, np.zeros(5)), axis=0) SAR_wbg_tensec = _do_sw_sar(SAR_wbg_lim_app, tsec, 10) # < 2 SARmax @@ -186,42 +180,34 @@ def _SAR_lims_check( SAR_wbg_tensec_peak = np.round(np.max(SAR_wbg_tensec), 2) SAR_hg_tensec_peak = np.round(np.max(SAR_hg_tensec), 2) - if (np.max(SAR_wbg_tensec) > ten_sec_threshold_wbg) or ( - np.max(SAR_hg_tensec) > ten_sec_threshold_hg - ): - print("Pulse exceeding 10 second Global SAR limits, increase TR") - SAR_wbg_sixmin = "NA" - SAR_hg_sixmin = "NA" - SAR_wbg_sixmin_peak = "NA" - SAR_hg_sixmin_peak = "NA" + if (np.max(SAR_wbg_tensec) > ten_sec_threshold_wbg) or (np.max(SAR_hg_tensec) > ten_sec_threshold_hg): + print('Pulse exceeding 10 second Global SAR limits, increase TR') + SAR_wbg_sixmin = 'NA' + SAR_hg_sixmin = 'NA' + SAR_wbg_sixmin_peak = 'NA' + SAR_hg_sixmin_peak = 'NA' if tsec[-1] > 600: - SAR_wbg_lim_app = np.concatenate( - (np.zeros(300), SARwbg_lim_s, np.zeros(300)), axis=0 - ) - SAR_hg_lim_app = np.concatenate( - (np.zeros(300), SARhg_lim_s, np.zeros(300)), axis=0 - ) + SAR_wbg_lim_app = np.concatenate((np.zeros(300), SARwbg_lim_s, np.zeros(300)), axis=0) + SAR_hg_lim_app = np.concatenate((np.zeros(300), SARhg_lim_s, np.zeros(300)), axis=0) SAR_hg_sixmin = _do_sw_sar(SAR_hg_lim_app, tsec, 600) SAR_wbg_sixmin = _do_sw_sar(SAR_wbg_lim_app, tsec, 600) SAR_wbg_sixmin_peak = np.round(np.max(SAR_wbg_sixmin), 2) SAR_hg_sixmin_peak = np.round(np.max(SAR_hg_sixmin), 2) - if (np.max(SAR_hg_sixmin) > six_min_threshold_wbg) or ( - np.max(SAR_hg_sixmin) > six_min_threshold_hg - ): - print("Pulse exceeding 10 second Global SAR limits, increase TR") + if (np.max(SAR_hg_sixmin) > six_min_threshold_wbg) or (np.max(SAR_hg_sixmin) > six_min_threshold_hg): + print('Pulse exceeding 10 second Global SAR limits, increase TR') else: - print("Need at least 10 seconds worth of sequence to calculate SAR") - SAR_wbg_tensec = "NA" - SAR_wbg_sixmin = "NA" - SAR_hg_tensec = "NA" - SAR_hg_sixmin = "NA" - SAR_wbg_sixmin_peak = "NA" - SAR_hg_sixmin_peak = "NA" - SAR_wbg_tensec_peak = "NA" - SAR_hg_tensec_peak = "NA" + print('Need at least 10 seconds worth of sequence to calculate SAR') + SAR_wbg_tensec = 'NA' + SAR_wbg_sixmin = 'NA' + SAR_hg_tensec = 'NA' + SAR_hg_sixmin = 'NA' + SAR_wbg_sixmin_peak = 'NA' + SAR_hg_sixmin_peak = 'NA' + SAR_wbg_tensec_peak = 'NA' + SAR_hg_tensec_peak = 'NA' return ( SAR_wbg_tensec, @@ -254,12 +240,8 @@ def _do_sw_sar(SAR: np.ndarray, tsec: np.ndarray, t: np.ndarray) -> np.ndarray: Sliding window time average of SAR values. """ SAR_time_avg = np.zeros(len(tsec) + int(t)) - for instant in range( - int(t / 2), int(t / 2) + (int(tsec[-1])) - ): # better to go from -sw / 2: sw / 2 - SAR_time_avg[instant] = ( - sum(SAR[range(instant - int(t / 2), instant + int(t / 2) - 1)]) / t - ) + for instant in range(int(t / 2), int(t / 2) + (int(tsec[-1]))): # better to go from -sw / 2: sw / 2 + SAR_time_avg[instant] = sum(SAR[range(instant - int(t / 2), instant + int(t / 2) - 1)]) / t SAR_time_avg = SAR_time_avg[int(t / 2) : int(t / 2) + (int(tsec[-1]))] return SAR_time_avg @@ -270,7 +252,7 @@ def calc_SAR(file: Union[str, Path, Sequence]) -> None: Parameters ---------- - file : str, Path or Seuqence + file : str, Path or Sequence `.seq` file for which global SAR values will be computed. Can be path to `.seq` file as `str` or `Path`, or the `Sequence` object itself. @@ -288,7 +270,7 @@ def calc_SAR(file: Union[str, Path, Sequence]) -> None: seq_obj.read(str(file)) seq_obj = seq_obj else: - raise ValueError("Seq file does not exist.") + raise ValueError('Seq file does not exist.') else: seq_obj = file @@ -309,15 +291,15 @@ def calc_SAR(file: Union[str, Path, Sequence]) -> None: # Plot 10 sec average SAR if tsec[-1] > 10: - plt.plot(tsec, SAR_wbg_tensec, "x-", label="Whole Body: 10sec") - plt.plot(tsec, SAR_hg_tensec, ".-", label="Head only: 10sec") + plt.plot(tsec, SAR_wbg_tensec, 'x-', label='Whole Body: 10sec') + plt.plot(tsec, SAR_hg_tensec, '.-', label='Head only: 10sec') # plt.plot(t, SARwbg, label='Whole Body - instant') # plt.plot(t, SARhg, label='Whole Body - instant') - plt.xlabel("Time (s)") - plt.ylabel("SAR (W/kg)") - plt.title("Global SAR - Mass Normalized - Whole body and head only") + plt.xlabel('Time (s)') + plt.ylabel('SAR (W/kg)') + plt.title('Global SAR - Mass Normalized - Whole body and head only') plt.legend() plt.grid(True) diff --git a/pypulseq/SAR/ruff.toml b/pypulseq/SAR/ruff.toml new file mode 100644 index 0000000..0a3c926 --- /dev/null +++ b/pypulseq/SAR/ruff.toml @@ -0,0 +1,5 @@ +extend = "../../pyproject.toml" + +lint.extend-ignore = [ + "E741", #ambiguous-variable-name +] diff --git a/pypulseq/Sequence/block.py b/pypulseq/Sequence/block.py index 3ac7997..76fe730 100755 --- a/pypulseq/Sequence/block.py +++ b/pypulseq/Sequence/block.py @@ -1,6 +1,6 @@ import math from types import SimpleNamespace -from typing import Tuple, List, Union +from typing import List, Tuple, Union import numpy as np @@ -19,7 +19,8 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: from events and store at position specified by index. The block or events are provided in uncompressed form and will be stored in the compressed, non-redundant internal libraries. - See also: + See Also + -------- - `pypulseq.Sequence.sequence.Sequence.get_block()` - `pypulseq.Sequence.sequence.Sequence.add_block()` @@ -45,47 +46,40 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: duration = 0 check_g = { - 0: SimpleNamespace(idx=2, start=(0,0), stop=(0,0)), - 1: SimpleNamespace(idx=3, start=(0,0), stop=(0,0)), - 2: SimpleNamespace(idx=4, start=(0,0), stop=(0,0)) - } # Key-value mapping of index and pairs of gradients/times + 0: SimpleNamespace(idx=2, start=(0, 0), stop=(0, 0)), + 1: SimpleNamespace(idx=3, start=(0, 0), stop=(0, 0)), + 2: SimpleNamespace(idx=4, start=(0, 0), stop=(0, 0)), + } # Key-value mapping of index and pairs of gradients/times extensions = [] for event in events: if not isinstance(event, float): # If event is not a block duration - if event.type == "rf": + if event.type == 'rf': if new_block[1] != 0: raise ValueError('Multiple RF events were specified in set_block') - if hasattr(event, "id"): + if hasattr(event, 'id'): rf_id = event.id else: rf_id, _ = register_rf_event(self, event) new_block[1] = rf_id - duration = max( - duration, event.shape_dur + event.delay + event.ringdown_time - ) + duration = max(duration, event.shape_dur + event.delay + event.ringdown_time) - if trace_enabled(): - if hasattr(event, 'trace'): - setattr(self.block_trace[block_index], 'rf', event.trace) - elif event.type == "grad": - channel_num = ["x", "y", "z"].index(event.channel) + if trace_enabled() and hasattr(event, 'trace'): + self.block_trace[block_index].rf = event.trace + elif event.type == 'grad': + channel_num = ['x', 'y', 'z'].index(event.channel) idx = 2 + channel_num if new_block[idx] != 0: raise ValueError(f'Multiple {event.channel.upper()} gradient events were specified in set_block') grad_start = ( - event.delay - + math.floor(event.tt[0] / self.grad_raster_time + 1e-10) - * self.grad_raster_time + event.delay + math.floor(event.tt[0] / self.grad_raster_time + 1e-10) * self.grad_raster_time ) grad_duration = ( - event.delay - + math.ceil(event.tt[-1] / self.grad_raster_time - 1e-10) - * self.grad_raster_time + event.delay + math.ceil(event.tt[-1] / self.grad_raster_time - 1e-10) * self.grad_raster_time ) check_g[channel_num] = SimpleNamespace() @@ -93,7 +87,7 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: check_g[channel_num].start = (grad_start, event.first) check_g[channel_num].stop = (grad_duration, event.last) - if hasattr(event, "id"): + if hasattr(event, 'id'): grad_id = event.id else: grad_id, _ = register_grad_event(self, event) @@ -101,71 +95,59 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: new_block[idx] = grad_id duration = max(duration, grad_duration) - if trace_enabled(): - if hasattr(event, 'trace'): - setattr(self.block_trace[block_index], 'g' + event.channel, event.trace) - elif event.type == "trap": - channel_num = ["x", "y", "z"].index(event.channel) + if trace_enabled() and hasattr(event, 'trace'): + setattr(self.block_trace[block_index], 'g' + event.channel, event.trace) + elif event.type == 'trap': + channel_num = ['x', 'y', 'z'].index(event.channel) idx = 2 + channel_num if new_block[idx] != 0: raise ValueError(f'Multiple {event.channel.upper()} gradient events were specified in set_block') - if hasattr(event, "id"): + if hasattr(event, 'id'): trap_id = event.id else: trap_id = register_grad_event(self, event) new_block[idx] = trap_id - duration = max( - duration, - event.delay - + event.rise_time - + event.flat_time - + event.fall_time - ) + duration = max(duration, event.delay + event.rise_time + event.flat_time + event.fall_time) - if trace_enabled(): - if hasattr(event, 'trace'): - setattr(self.block_trace[block_index], 'g' + event.channel, event.trace) - elif event.type == "adc": + if trace_enabled() and hasattr(event, 'trace'): + setattr(self.block_trace[block_index], 'g' + event.channel, event.trace) + elif event.type == 'adc': if new_block[5] != 0: raise ValueError('Multiple ADC events were specified in set_block') - if hasattr(event, "id"): + if hasattr(event, 'id'): adc_id = event.id else: adc_id = register_adc_event(self, event) new_block[5] = adc_id - duration = max( - duration, - event.delay + event.num_samples * event.dwell + event.dead_time - ) + duration = max(duration, event.delay + event.num_samples * event.dwell + event.dead_time) - if trace_enabled(): - if hasattr(event, 'trace'): - setattr(self.block_trace[block_index], 'adc', event.trace) - elif event.type == "delay": + if trace_enabled() and hasattr(event, 'trace'): + self.block_trace[block_index].adc = event.trace + elif event.type == 'delay': duration = max(duration, event.delay) - elif event.type in ["output", "trigger"]: - if hasattr(event, "id"): + elif event.type in ['output', 'trigger']: + if hasattr(event, 'id'): event_id = event.id else: event_id = register_control_event(self, event) - ext = {"type": self.get_extension_type_ID("TRIGGERS"), "ref": event_id} + ext = {'type': self.get_extension_type_ID('TRIGGERS'), 'ref': event_id} extensions.append(ext) duration = max(duration, event.delay + event.duration) - elif event.type in ["labelset", "labelinc"]: - if hasattr(event, "id"): + elif event.type in ['labelset', 'labelinc']: + if hasattr(event, 'id'): label_id = event.id else: label_id = register_label_event(self, event) ext = { - "type": self.get_extension_type_ID(event.type.upper()), - "ref": label_id, + 'type': self.get_extension_type_ID(event.type.upper()), + 'ref': label_id, } extensions.append(ext) else: @@ -182,12 +164,12 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: mapping then... The trick is that we rely on the sorting of the extension IDs and then we can always find the last one in the list by setting the reference to the next to 0 and then proceed with the other elements. """ - sort_idx = np.argsort([e["ref"] for e in extensions]) + sort_idx = np.argsort([e['ref'] for e in extensions]) extensions = np.take(extensions, sort_idx) all_found = True extension_id = 0 for i in range(len(extensions)): - data = (extensions[i]["type"], extensions[i]["ref"], extension_id) + data = (extensions[i]['type'], extensions[i]['ref'], extension_id) extension_id, found = self.extensions_library.find(data) all_found = all_found and found if not found: @@ -197,7 +179,7 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: # Add the list extension_id = 0 for i in range(len(extensions)): - data = (extensions[i]["type"], extensions[i]["ref"], extension_id) + data = (extensions[i]['type'], extensions[i]['ref'], extension_id) extension_id, found = self.extensions_library.find(data) if not found: self.extensions_library.insert(extension_id, data) @@ -209,12 +191,9 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: # PERFORM GRADIENT CHECKS # ========= for grad_to_check in check_g.values(): - - if (abs(grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time): + if abs(grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time: # noqa: SIM102 if grad_to_check.start[0] > eps: - raise RuntimeError( - "No delay allowed for gradients which start with a non-zero amplitude" - ) + raise RuntimeError('No delay allowed for gradients which start with a non-zero amplitude') # Check whether any blocks exist in the sequence if self.next_free_block_ID > 1: @@ -229,7 +208,7 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: # Existing block overwritten idx = blocks.index(block_index) prev_block_index = blocks[idx - 1] if idx > 0 else None - next_block_index = blocks[idx + 1] if idx < len(blocks)-1 else None + next_block_index = blocks[idx + 1] if idx < len(blocks) - 1 else None except ValueError: # Inserting a new block with non-contiguous numbering prev_block_index = next(reversed(self.block_events)) @@ -251,10 +230,8 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: # Check whether the difference between the last gradient value and # the first value of the new gradient is achievable with the # specified slew rate. - if (abs(last - grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time): - raise RuntimeError( - "Two consecutive gradients need to have the same amplitude at the connection point" - ) + if abs(last - grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time: + raise RuntimeError('Two consecutive gradients need to have the same amplitude at the connection point') # Look up the first gradient value in the next block # (this only happens when using set_block to patch a block) @@ -274,22 +251,18 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: # Check whether the difference between the first gradient value # in the next block and the last value of the new gradient is # achievable with the specified slew rate. - if (abs(first - grad_to_check.stop[1]) > self.system.max_slew * self.system.grad_raster_time): + if abs(first - grad_to_check.stop[1]) > self.system.max_slew * self.system.grad_raster_time: raise RuntimeError( - "Two consecutive gradients need to have the same amplitude at the connection point" + 'Two consecutive gradients need to have the same amplitude at the connection point' ) elif abs(grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time: - raise RuntimeError( - "First gradient in the the first block has to start at 0." - ) + raise RuntimeError('First gradient in the the first block has to start at 0.') if ( grad_to_check.stop[1] > self.system.max_slew * self.system.grad_raster_time and abs(grad_to_check.stop[0] - duration) > 1e-7 ): - raise RuntimeError( - "A gradient that doesn't end at zero needs to be aligned to the block boundary." - ) + raise RuntimeError("A gradient that doesn't end at zero needs to be aligned to the block boundary.") self.block_events[block_index] = new_block self.block_durations[block_index] = float(duration) @@ -317,13 +290,12 @@ def get_block(self, block_index: int) -> SimpleNamespace: If a trigger event of an unsupported control type is encountered. If a label object of an unknown extension ID is encountered. """ - # Check if block exists in the block cache. If so, return that if self.use_block_cache and block_index in self.block_cache: return self.block_cache[block_index] block = SimpleNamespace() - attrs = ["block_duration", "rf", "gx", "gy", "gz", "adc", "label"] + attrs = ['block_duration', 'rf', 'gx', 'gy', 'gz', 'adc', 'label'] values = [None] * len(attrs) for att, val in zip(attrs, values): setattr(block, att, val) @@ -331,30 +303,26 @@ def get_block(self, block_index: int) -> SimpleNamespace: if event_ind[0] > 0: # Delay delay = SimpleNamespace() - delay.type = "delay" + delay.type = 'delay' delay.delay = self.delay_library.data[event_ind[0]][0] block.delay = delay if event_ind[1] > 0: # RF if event_ind[1] in self.rf_library.type: - block.rf = self.rf_from_lib_data( - self.rf_library.data[event_ind[1]], self.rf_library.type[event_ind[1]] - ) + block.rf = self.rf_from_lib_data(self.rf_library.data[event_ind[1]], self.rf_library.type[event_ind[1]]) else: - block.rf = self.rf_from_lib_data( - self.rf_library.data[event_ind[1]] , 'u' - ) # Undefined type/use + block.rf = self.rf_from_lib_data(self.rf_library.data[event_ind[1]], 'u') # Undefined type/use # Gradients - grad_channels = ["gx", "gy", "gz"] + grad_channels = ['gx', 'gy', 'gz'] for i in range(len(grad_channels)): if event_ind[2 + i] > 0: grad, compressed = SimpleNamespace(), SimpleNamespace() grad_type = self.grad_library.type[event_ind[2 + i]] lib_data = self.grad_library.data[event_ind[2 + i]] - grad.type = "trap" if grad_type == "t" else "grad" + grad.type = 'trap' if grad_type == 't' else 'grad' grad.channel = grad_channels[i][1] - if grad.type == "grad": + if grad.type == 'grad': amplitude = lib_data[0] shape_id = lib_data[1] time_id = lib_data[2] @@ -390,9 +358,7 @@ def get_block(self, block_index: int) -> SimpleNamespace: grad.flat_time = lib_data[2] grad.fall_time = lib_data[3] grad.delay = lib_data[4] - grad.area = grad.amplitude * ( - grad.flat_time + grad.rise_time / 2 + grad.fall_time / 2 - ) + grad.area = grad.amplitude * (grad.flat_time + grad.rise_time / 2 + grad.fall_time / 2) grad.flat_area = grad.amplitude * grad.flat_time setattr(block, grad_channels[i], grad) @@ -411,7 +377,7 @@ def get_block(self, block_index: int) -> SimpleNamespace: adc.dead_time, ) = [lib_data[x] for x in range(6)] adc.num_samples = int(adc.num_samples) - adc.type = "adc" + adc.type = 'adc' block.adc = adc # Triggers @@ -423,32 +389,32 @@ def get_block(self, block_index: int) -> SimpleNamespace: # Format: ext_type, ext_id, next_ext_id ext_type = self.get_extension_type_string(ext_data[0]) - if ext_type == "TRIGGERS": - trigger_types = ["output", "trigger"] + if ext_type == 'TRIGGERS': + trigger_types = ['output', 'trigger'] data = self.trigger_library.data[ext_data[1]] trigger = SimpleNamespace() trigger.type = trigger_types[int(data[0]) - 1] if data[0] == 1: - trigger_channels = ["osc0", "osc1", "ext1"] + trigger_channels = ['osc0', 'osc1', 'ext1'] trigger.channel = trigger_channels[int(data[1]) - 1] elif data[0] == 2: - trigger_channels = ["physio1", "physio2"] + trigger_channels = ['physio1', 'physio2'] trigger.channel = trigger_channels[int(data[1]) - 1] else: - raise ValueError("Unsupported trigger event type") + raise ValueError('Unsupported trigger event type') trigger.delay = data[2] trigger.duration = data[3] # Allow for multiple triggers per block - if hasattr(block, "trigger"): + if hasattr(block, 'trigger'): block.trigger[len(block.trigger)] = trigger else: block.trigger = {0: trigger} - elif ext_type in ["LABELSET", "LABELINC"]: + elif ext_type in ['LABELSET', 'LABELINC']: label = SimpleNamespace() label.type = ext_type.lower() supported_labels = get_supported_labels() - if ext_type == "LABELSET": + if ext_type == 'LABELSET': data = self.label_set_library.data[ext_data[1]] else: data = self.label_inc_library.data[ext_data[1]] @@ -461,7 +427,7 @@ def get_block(self, block_index: int) -> SimpleNamespace: else: block.label = {0: label} else: - raise RuntimeError(f"Unknown extension ID {ext_data[0]}") + raise RuntimeError(f'Unknown extension ID {ext_data[0]}') next_ext_id = ext_data[2] @@ -488,13 +454,13 @@ def register_adc_event(self, event: EventLibrary) -> int: ID of registered ADC event. """ data = ( - event.num_samples, - event.dwell, - event.delay, - event.freq_offset, - event.phase_offset, - event.dead_time, - ) + event.num_samples, + event.dwell, + event.delay, + event.freq_offset, + event.phase_offset, + event.dead_time, + ) adc_id, found = self.adc_library.find_or_insert(new_data=data) # Clear block cache because ADC was overwritten @@ -518,15 +484,15 @@ def register_control_event(self, event: SimpleNamespace) -> int: int ID of registered control event. """ - event_type = ["output", "trigger"].index(event.type) + event_type = ['output', 'trigger'].index(event.type) if event_type == 0: # Trigger codes supported by the Siemens interpreter as of May 2019 - event_channel = ["osc0", "osc1", "ext1"].index(event.channel) + event_channel = ['osc0', 'osc1', 'ext1'].index(event.channel) elif event_type == 1: # Trigger codes supported by the Siemens interpreter as of June 2019 - event_channel = ["physio1", "physio2"].index(event.channel) + event_channel = ['physio1', 'physio2'].index(event.channel) else: - raise ValueError("Unsupported control event type") + raise ValueError('Unsupported control event type') data = (event_type + 1, event_channel + 1, event.delay, event.duration) control_id, found = self.trigger_library.find_or_insert(new_data=data) @@ -539,9 +505,7 @@ def register_control_event(self, event: SimpleNamespace) -> int: return control_id -def register_grad_event( - self, event: SimpleNamespace -) -> Union[int, Tuple[int, List[int]]]: +def register_grad_event(self, event: SimpleNamespace) -> Union[int, Tuple[int, List[int]]]: """ Parameters ---------- @@ -557,15 +521,13 @@ def register_grad_event( """ may_exist = True any_changed = False - if event.type == "grad": + if event.type == 'grad': amplitude = np.abs(event.waveform).max() if amplitude > 0: fnz = event.waveform[np.nonzero(event.waveform)[0][0]] - amplitude *= ( - np.sign(fnz) if fnz != 0 else 1 - ) # Workaround for np.sign(0) = 0 + amplitude *= np.sign(fnz) if fnz != 0 else 1 # Workaround for np.sign(0) = 0 - if hasattr(event, "shape_IDs"): + if hasattr(event, 'shape_IDs'): shape_IDs = event.shape_IDs else: shape_IDs = [0, 0] @@ -580,7 +542,7 @@ def register_grad_event( any_changed = any_changed or found # Check whether tt == np.arange(len(event.tt)) * self.grad_raster_time + 0.5 - tt_regular = (np.floor(event.tt/self.grad_raster_time) == np.arange(len(event.tt))).all() + tt_regular = (np.floor(event.tt / self.grad_raster_time) == np.arange(len(event.tt))).all() if not tt_regular: c_time = compress_shape(event.tt / self.grad_raster_time) @@ -590,21 +552,19 @@ def register_grad_event( any_changed = any_changed or found data = (amplitude, *shape_IDs, event.delay, event.first, event.last) - elif event.type == "trap": + elif event.type == 'trap': data = ( - event.amplitude, - event.rise_time, - event.flat_time, - event.fall_time, - event.delay, - ) + event.amplitude, + event.rise_time, + event.flat_time, + event.fall_time, + event.delay, + ) else: - raise ValueError("Unknown gradient type passed to register_grad_event()") + raise ValueError('Unknown gradient type passed to register_grad_event()') if may_exist: - grad_id, found = self.grad_library.find_or_insert( - new_data=data, data_type=event.type[0] - ) + grad_id, found = self.grad_library.find_or_insert(new_data=data, data_type=event.type[0]) any_changed = any_changed or found else: grad_id = self.grad_library.insert(0, data, event.type[0]) @@ -614,9 +574,9 @@ def register_grad_event( if self.use_block_cache and any_changed: self.block_cache.clear() - if event.type == "grad": + if event.type == 'grad': return grad_id, shape_IDs - elif event.type == "trap": + elif event.type == 'trap': return grad_id @@ -632,15 +592,14 @@ def register_label_event(self, event: SimpleNamespace) -> int: int ID of registered label event. """ - label_id = get_supported_labels().index(event.label) + 1 data = (event.value, label_id) - if event.type == "labelset": + if event.type == 'labelset': label_id, found = self.label_set_library.find_or_insert(new_data=data) - elif event.type == "labelinc": + elif event.type == 'labelinc': label_id, found = self.label_inc_library.find_or_insert(new_data=data) else: - raise ValueError("Unsupported label type passed to register_label_event()") + raise ValueError('Unsupported label type passed to register_label_event()') # Clear block cache because label event was overwritten # TODO: Could find only the blocks that are affected by the changes @@ -673,7 +632,7 @@ def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]: phase /= 2 * np.pi may_exist = True - if hasattr(event, "shape_IDs"): + if hasattr(event, 'shape_IDs'): shape_IDs = event.shape_IDs else: shape_IDs = [0, 0, 0] @@ -688,31 +647,28 @@ def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]: shape_IDs[1], found = self.shape_library.find_or_insert(data) may_exist = may_exist & found - - t_regular = (np.floor(event.t/self.rf_raster_time) == np.arange(len(event.t))).all() + t_regular = (np.floor(event.t / self.rf_raster_time) == np.arange(len(event.t))).all() if t_regular: shape_IDs[2] = 0 else: - time_shape = compress_shape( - event.t / self.rf_raster_time - ) + time_shape = compress_shape(event.t / self.rf_raster_time) data = [time_shape.num_samples, *time_shape.data] shape_IDs[2], found = self.shape_library.find_or_insert(data) may_exist = may_exist & found - use = "u" # Undefined - if hasattr(event, "use"): + use = 'u' # Undefined + if hasattr(event, 'use'): if event.use in [ - "excitation", - "refocusing", - "inversion", - "saturation", - "preparation", + 'excitation', + 'refocusing', + 'inversion', + 'saturation', + 'preparation', ]: use = event.use[0] else: - use = "u" + use = 'u' data = (amplitude, *shape_IDs, event.delay, event.freq_offset, event.phase_offset) @@ -726,6 +682,4 @@ def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]: else: rf_id = self.rf_library.insert(key_id=0, new_data=data, data_type=use) - - return rf_id, shape_IDs diff --git a/pypulseq/Sequence/calc_grad_spectrum.py b/pypulseq/Sequence/calc_grad_spectrum.py index 7812989..dbc7330 100644 --- a/pypulseq/Sequence/calc_grad_spectrum.py +++ b/pypulseq/Sequence/calc_grad_spectrum.py @@ -1,8 +1,8 @@ -from typing import Tuple, List, Union +from typing import List, Tuple, Union import numpy as np -from scipy.signal import spectrogram from matplotlib import pyplot as plt +from scipy.signal import spectrogram def calculate_gradient_spectrum( @@ -12,9 +12,9 @@ def calculate_gradient_spectrum( frequency_oversampling: float = 3, time_range: Union[List[float], None] = None, plot: bool = True, - combine_mode: str = "max", + combine_mode: str = 'max', use_derivative: bool = False, - acoustic_resonances: List[dict] = [], + acoustic_resonances: Union[List[dict], None] = None, ) -> Tuple[List[np.ndarray], np.ndarray, np.ndarray, np.ndarray]: """ Calculates the gradient spectrum of the sequence. Returns a spectrogram @@ -65,6 +65,9 @@ def calculate_gradient_spectrum( Time axis of the spectrograms (only relevant when combine_mode == 'none'). """ + if acoustic_resonances is None: + acoustic_resonances = [] + dt = obj.system.grad_raster_time # time raster nwin = round(window_width / dt) nfft = round(frequency_oversampling * nwin) @@ -101,12 +104,12 @@ def calculate_gradient_spectrum( freq, times, sxx = spectrogram( gw[i], fs=1 / dt, - mode="magnitude", + mode='magnitude', nperseg=nwin, noverlap=nwin // 2, nfft=nfft, - detrend="constant", - window=("tukey", 1), + detrend='constant', + window=('tukey', 1), ) mask = freq < max_frequency @@ -114,50 +117,50 @@ def calculate_gradient_spectrum( spectrogram_rss += sxx[mask] ** 2 # Combine spectrogram over time axis - if combine_mode == "max": + if combine_mode == 'max': s = sxx[mask].max(axis=1) - elif combine_mode == "mean": + elif combine_mode == 'mean': s = sxx[mask].mean(axis=1) - elif combine_mode == "rss": + elif combine_mode == 'rss': s = np.sqrt((sxx[mask] ** 2).sum(axis=1)) - elif combine_mode == "none": + elif combine_mode == 'none': s = sxx[mask] else: - raise ValueError(f"Unknown value for combine_mode: {combine_mode}, must be one of [max, mean, rss, none]") + raise ValueError(f'Unknown value for combine_mode: {combine_mode}, must be one of [max, mean, rss, none]') frequencies = freq[mask] spectrograms.append(s) # Root-sum-of-squares combined spectrogram for all gradient channels spectrogram_rss = np.sqrt(spectrogram_rss) - if combine_mode == "max": + if combine_mode == 'max': spectrogram_rss = spectrogram_rss.max(axis=1) - elif combine_mode == "mean": + elif combine_mode == 'mean': spectrogram_rss = spectrogram_rss.mean(axis=1) - elif combine_mode == "rss": + elif combine_mode == 'rss': spectrogram_rss = np.sqrt((spectrogram_rss**2).sum(axis=1)) # Plot spectrograms and acoustic resonances if specified if plot: - if combine_mode != "none": + if combine_mode != 'none': plt.figure() - plt.xlabel("Frequency (Hz)") + plt.xlabel('Frequency (Hz)') # According to spectrogram documentation y unit is (Hz/m)^2 / Hz = Hz/m^2, is this meaningful? for s in spectrograms: plt.plot(frequencies, s) plt.plot(frequencies, spectrogram_rss) - plt.legend(["x", "y", "z", "rss"]) + plt.legend(['x', 'y', 'z', 'rss']) for res in acoustic_resonances: - plt.axvline(res["frequency"], color="k", linestyle="-") - plt.axvline(res["frequency"] - res["bandwidth"] / 2, color="k", linestyle="--") - plt.axvline(res["frequency"] + res["bandwidth"] / 2, color="k", linestyle="--") + plt.axvline(res['frequency'], color='k', linestyle='-') + plt.axvline(res['frequency'] - res['bandwidth'] / 2, color='k', linestyle='--') + plt.axvline(res['frequency'] + res['bandwidth'] / 2, color='k', linestyle='--') else: - for s, c in zip(spectrograms, ["X", "Y", "Z"]): + for s, c in zip(spectrograms, ['X', 'Y', 'Z']): plt.figure() - plt.title(f"Spectrum {c}") - plt.xlabel("Time (s)") - plt.ylabel("Frequency (Hz)") + plt.title(f'Spectrum {c}') + plt.xlabel('Time (s)') + plt.ylabel('Frequency (Hz)') plt.imshow( abs(s[::-1]), extent=(times[0], times[-1], frequencies[0], frequencies[-1]), @@ -165,14 +168,14 @@ def calculate_gradient_spectrum( ) for res in acoustic_resonances: - plt.axhline(res["frequency"], color="r", linestyle="-") - plt.axhline(res["frequency"] - res["bandwidth"] / 2, color="r", linestyle="--") - plt.axhline(res["frequency"] + res["bandwidth"] / 2, color="r", linestyle="--") + plt.axhline(res['frequency'], color='r', linestyle='-') + plt.axhline(res['frequency'] - res['bandwidth'] / 2, color='r', linestyle='--') + plt.axhline(res['frequency'] + res['bandwidth'] / 2, color='r', linestyle='--') plt.figure() - plt.title("Total spectrum") - plt.xlabel("Time (s)") - plt.ylabel("Frequency (Hz)") + plt.title('Total spectrum') + plt.xlabel('Time (s)') + plt.ylabel('Frequency (Hz)') plt.imshow( abs(spectrogram_rss[::-1]), extent=(times[0], times[-1], frequencies[0], frequencies[-1]), @@ -180,8 +183,8 @@ def calculate_gradient_spectrum( ) for res in acoustic_resonances: - plt.axhline(res["frequency"], color="r", linestyle="-") - plt.axhline(res["frequency"] - res["bandwidth"] / 2, color="r", linestyle="--") - plt.axhline(res["frequency"] + res["bandwidth"] / 2, color="r", linestyle="--") + plt.axhline(res['frequency'], color='r', linestyle='-') + plt.axhline(res['frequency'] - res['bandwidth'] / 2, color='r', linestyle='--') + plt.axhline(res['frequency'] + res['bandwidth'] / 2, color='r', linestyle='--') return spectrograms, spectrogram_rss, frequencies, times diff --git a/pypulseq/Sequence/calc_pns.py b/pypulseq/Sequence/calc_pns.py index 96d1a51..23d2f58 100644 --- a/pypulseq/Sequence/calc_pns.py +++ b/pypulseq/Sequence/calc_pns.py @@ -1,14 +1,13 @@ from types import SimpleNamespace -from typing import Tuple, Union, List +from typing import List, Tuple, Union import matplotlib.pyplot as plt import numpy as np from pypulseq import Sequence from pypulseq.utils.safe_pns_prediction import safe_gwf_to_pns, safe_plot - -from pypulseq.utils.siemens.readasc import readasc from pypulseq.utils.siemens.asc_to_hw import asc_to_hw +from pypulseq.utils.siemens.readasc import readasc def calc_pns( @@ -43,7 +42,6 @@ def calc_pns( t_pns : np.array [N] Time axis for the pns_norm and pns_components arrays """ - dt = obj.grad_raster_time # Get gradients as piecewise-polynomials gw_pp = obj.get_gradients(time_range=time_range) @@ -70,9 +68,9 @@ def calc_pns( for i in range(ng): if gw_pp[i] is not None: plt.plot(gw_pp[i].x[1:-1], gw_pp[i].c[1, :-1]) - plt.title("gradient wave form, in Hz/m") + plt.title('gradient wave form, in Hz/m') - if type(hardware) == str: + if isinstance(hardware, str): # this loads the parameters from the provided text file asc, _ = readasc(hardware) hardware = asc_to_hw(asc) diff --git a/pypulseq/Sequence/ext_test_report.py b/pypulseq/Sequence/ext_test_report.py index 1211c20..d114613 100644 --- a/pypulseq/Sequence/ext_test_report.py +++ b/pypulseq/Sequence/ext_test_report.py @@ -84,7 +84,7 @@ def ext_test_report(self) -> str: k_repeat = np.zeros(k_len) k_storage = np.zeros(k_len) k_storage_next = 0 - k_map = dict() + k_map = {} keys = np.round(k_traj_adc / k_threshold).astype(np.int32) for i in range(k_len): key_string = tuple(keys[:, i]) @@ -111,7 +111,7 @@ def ext_test_report(self) -> str: keys = keys[:, k_repeat == 1] for j in range(dims): - k_map = dict() + k_map = {} k_storage = np.zeros(k_len) k_storage_next = 0 @@ -170,66 +170,66 @@ def ext_test_report(self) -> str: timing_ok, timing_error_report = self.check_timing() report = ( - f"Number of blocks: {num_blocks}\n" - f"Number of events:\n" - f"RF: {event_count[1]:6.0f}\n" - f"Gx: {event_count[2]:6.0f}\n" - f"Gy: {event_count[3]:6.0f}\n" - f"Gz: {event_count[4]:6.0f}\n" - f"ADC: {event_count[5]:6.0f}\n" - f"Delay: {event_count[0]:6.0f}\n" - f"Sequence duration: {duration:.6f} s\n" - f"TE: {TE:.6f} s\n" - f"TR: {TR:.6f} s\n" + f'Number of blocks: {num_blocks}\n' + f'Number of events:\n' + f'RF: {event_count[1]:6.0f}\n' + f'Gx: {event_count[2]:6.0f}\n' + f'Gy: {event_count[3]:6.0f}\n' + f'Gz: {event_count[4]:6.0f}\n' + f'ADC: {event_count[5]:6.0f}\n' + f'Delay: {event_count[0]:6.0f}\n' + f'Sequence duration: {duration:.6f} s\n' + f'TE: {TE:.6f} s\n' + f'TR: {TR:.6f} s\n' ) - report += "Flip angle: " + ("{:.02f} " * len(flip_angles_deg)).format(*flip_angles_deg) + "deg\n" + report += 'Flip angle: ' + ('{:.02f} ' * len(flip_angles_deg)).format(*flip_angles_deg) + 'deg\n' report += ( - "Unique k-space positions (aka cols, rows, etc.): " - + ("{:.0f} " * len(unique_k_positions)).format(*unique_k_positions) - + "\n" + 'Unique k-space positions (aka cols, rows, etc.): ' + + ('{:.0f} ' * len(unique_k_positions)).format(*unique_k_positions) + + '\n' ) if np.any(unique_k_positions > 1): - report += f"Dimensions: {len(k_extent)}\n" - report += ("Spatial resolution: {:.02f} mm\n" * len(k_extent)).format(*(0.5 / k_extent * 1e3)) - report += f"Repetitions/slices/contrasts: {repeats_median}; range: [{repeats_min, repeats_max}]\n" + report += f'Dimensions: {len(k_extent)}\n' + report += ('Spatial resolution: {:.02f} mm\n' * len(k_extent)).format(*(0.5 / k_extent * 1e3)) + report += f'Repetitions/slices/contrasts: {repeats_median}; range: [{repeats_min, repeats_max}]\n' if is_cartesian: - report += "Cartesian encoding trajectory detected\n" + report += 'Cartesian encoding trajectory detected\n' else: - report += "Non-cartesian/irregular encoding trajectory detected (eg: EPI, spiral, radial, etc.)\n" + report += 'Non-cartesian/irregular encoding trajectory detected (eg: EPI, spiral, radial, etc.)\n' - ga_converted = convert(from_value=ga, from_unit="Hz/m", to_unit="mT/m") - gs_converted = convert(from_value=gs, from_unit="Hz/m/s", to_unit="T/m/s") + ga_converted = convert(from_value=ga, from_unit='Hz/m', to_unit='mT/m') + gs_converted = convert(from_value=gs, from_unit='Hz/m/s', to_unit='T/m/s') report += ( - "Max gradient: " - + ("{:.0f} " * len(ga)).format(*ga) - + "Hz/m == " - + ("{:.02f} " * len(ga_converted)).format(*ga_converted) - + "mT/m\n" + 'Max gradient: ' + + ('{:.0f} ' * len(ga)).format(*ga) + + 'Hz/m == ' + + ('{:.02f} ' * len(ga_converted)).format(*ga_converted) + + 'mT/m\n' ) report += ( - "Max slew rate: " - + ("{:.0f} " * len(gs)).format(*gs) - + "Hz/m/s == " - + ("{:.02f} " * len(ga_converted)).format(*gs_converted) - + "T/m/s\n" + 'Max slew rate: ' + + ('{:.0f} ' * len(gs)).format(*gs) + + 'Hz/m/s == ' + + ('{:.02f} ' * len(ga_converted)).format(*gs_converted) + + 'T/m/s\n' ) - ga_abs_converted = convert(from_value=ga_abs, from_unit="Hz/m", to_unit="mT/m") - gs_abs_converted = convert(from_value=gs_abs, from_unit="Hz/m/s", to_unit="T/m/s") - report += f"Max absolute gradient: {ga_abs:.0f} Hz/m == {ga_abs_converted:.2f} mT/m\n" - report += f"Max absolute slew rate: {gs_abs:g} Hz/m/s == {gs_abs_converted:.2f} T/m/s" + ga_abs_converted = convert(from_value=ga_abs, from_unit='Hz/m', to_unit='mT/m') + gs_abs_converted = convert(from_value=gs_abs, from_unit='Hz/m/s', to_unit='T/m/s') + report += f'Max absolute gradient: {ga_abs:.0f} Hz/m == {ga_abs_converted:.2f} mT/m\n' + report += f'Max absolute slew rate: {gs_abs:g} Hz/m/s == {gs_abs_converted:.2f} T/m/s' if timing_ok: - report += "\nEvent timing check passed successfully\n" + report += '\nEvent timing check passed successfully\n' else: - report += f"\nEvent timing check failed with {len(timing_error_report)} errors in total. \n" - report += "Details of the first up to 20 timing errors:" + report += f'\nEvent timing check failed with {len(timing_error_report)} errors in total. \n' + report += 'Details of the first up to 20 timing errors:' max_errors = min(20, len(timing_error_report)) for line in timing_error_report[:max_errors]: - report += f"\n{line}" + report += f'\n{line}' if len(timing_error_report) > max_errors: - report += "\n..." + report += '\n...' return report diff --git a/pypulseq/Sequence/parula.py b/pypulseq/Sequence/parula.py index be2d0fd..ccae80e 100644 --- a/pypulseq/Sequence/parula.py +++ b/pypulseq/Sequence/parula.py @@ -83,4 +83,4 @@ def main(N: int) -> LinearSegmentedColormap: [0.9769, 0.9839, 0.0805], ] - return LinearSegmentedColormap.from_list(name="parula", colors=cm_data, N=N) + return LinearSegmentedColormap.from_list(name='parula', colors=cm_data, N=N) diff --git a/pypulseq/Sequence/read_seq.py b/pypulseq/Sequence/read_seq.py index 22bb411..b6eee27 100755 --- a/pypulseq/Sequence/read_seq.py +++ b/pypulseq/Sequence/read_seq.py @@ -1,8 +1,7 @@ import re import warnings -from pathlib import Path from types import SimpleNamespace -from typing import Dict, Tuple, List +from typing import Dict, List, Tuple import numpy as np @@ -39,9 +38,9 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = If unexpected sections are encountered when loading a sequence file. """ try: - input_file = open(path, "r") + input_file = open(path, 'r') except FileNotFoundError as e: - raise FileNotFoundError(e) + raise FileNotFoundError(e) from e # Event libraries self.adc_library = EventLibrary() @@ -69,66 +68,66 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = section = __skip_comments(input_file) if section == -1: break - if section == "[DEFINITIONS]": + if section == '[DEFINITIONS]': self.definitions = __read_definitions(input_file) # Gradient raster time - if "GradientRasterTime" in self.definitions: - self.gradient_raster_time = self.definitions["GradientRasterTime"] + if 'GradientRasterTime' in self.definitions: + self.gradient_raster_time = self.definitions['GradientRasterTime'] # Radio frequency raster time - if "RadiofrequencyRasterTime" in self.definitions: - self.rf_raster_time = self.definitions["RadiofrequencyRasterTime"] + if 'RadiofrequencyRasterTime' in self.definitions: + self.rf_raster_time = self.definitions['RadiofrequencyRasterTime'] # ADC raster time - if "AdcRasterTime" in self.definitions: - self.adc_raster_time = self.definitions["AdcRasterTime"] + if 'AdcRasterTime' in self.definitions: + self.adc_raster_time = self.definitions['AdcRasterTime'] # Block duration raster - if "BlockDurationRaster" in self.definitions: - self.block_duration_raster = self.definitions["BlockDurationRaster"] + if 'BlockDurationRaster' in self.definitions: + self.block_duration_raster = self.definitions['BlockDurationRaster'] else: - warnings.warn(f"No BlockDurationRaster found in file. Using default of {self.block_duration_raster}.") + warnings.warn(f'No BlockDurationRaster found in file. Using default of {self.block_duration_raster}.') - elif section == "[JEMRIS]": + elif section == '[JEMRIS]': jemris_generated = True - elif section == "[SIGNATURE]": + elif section == '[SIGNATURE]': temp_sign_defs = __read_definitions(input_file) - if "Type" in temp_sign_defs: - self.signature_type = temp_sign_defs["Type"] - if "Hash" in temp_sign_defs: - self.signature_value = temp_sign_defs["Hash"] - self.signature_file = "Text" - elif section == "[VERSION]": + if 'Type' in temp_sign_defs: + self.signature_type = temp_sign_defs['Type'] + if 'Hash' in temp_sign_defs: + self.signature_value = temp_sign_defs['Hash'] + self.signature_file = 'Text' + elif section == '[VERSION]': version_major, version_minor, version_revision = __read_version(input_file) if version_major != self.version_major: - raise RuntimeError(f"Unsupported version_major: {version_major}. Expected: {self.version_major}") + raise RuntimeError(f'Unsupported version_major: {version_major}. Expected: {self.version_major}') version_combined = 1000000 * version_major + 1000 * version_minor + version_revision if version_combined < 1002000: raise RuntimeError( - f"Unsupported version {version_major}.{version_minor}.{version_revision}, only file " - f"format revision 1.2.0 and above are supported." + f'Unsupported version {version_major}.{version_minor}.{version_revision}, only file ' + f'format revision 1.2.0 and above are supported.' ) if version_combined < 1003001: raise RuntimeError( - f"Loading older Pulseq format file (version " - f"{version_major}.{version_minor}.{version_revision}) some code may function not as " - f"expected" + f'Loading older Pulseq format file (version ' + f'{version_major}.{version_minor}.{version_revision}) some code may function not as ' + f'expected' ) - elif section == "[BLOCKS]": + elif section == '[BLOCKS]': if version_major == 0: - raise RuntimeError("Pulseq file MUST include [VERSION] section prior to [BLOCKS] section") + raise RuntimeError('Pulseq file MUST include [VERSION] section prior to [BLOCKS] section') result = __read_blocks( input_file, block_duration_raster=self.block_duration_raster, version_combined=version_combined, ) self.block_events, self.block_durations, delay_ind_temp = result - elif section == "[RF]": + elif section == '[RF]': if jemris_generated: self.rf_library = __read_events(input_file, (1, 1, 1, 1, 1), event_library=self.rf_library) else: @@ -140,54 +139,64 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = ) else: # 1.3.x and below self.rf_library = __read_events(input_file, (1, 1, 1, 1e-6, 1, 1), event_library=self.rf_library) - elif section == "[GRADIENTS]": + elif section == '[GRADIENTS]': if version_combined >= 1004000: # 1.4.x format - self.grad_library = __read_events(input_file, (1, 1, 1, 1e-6), "g", self.grad_library) + self.grad_library = __read_events(input_file, (1, 1, 1, 1e-6), 'g', self.grad_library) else: # 1.3.x and below - self.grad_library = __read_events(input_file, (1, 1, 1e-6), "g", self.grad_library) - elif section == "[TRAP]": + self.grad_library = __read_events(input_file, (1, 1, 1e-6), 'g', self.grad_library) + elif section == '[TRAP]': if jemris_generated: - self.grad_library = __read_events(input_file, (1, 1e-6, 1e-6, 1e-6), "t", self.grad_library) + self.grad_library = __read_events(input_file, (1, 1e-6, 1e-6, 1e-6), 't', self.grad_library) else: - self.grad_library = __read_events(input_file, (1, 1e-6, 1e-6, 1e-6, 1e-6), "t", self.grad_library) - elif section == "[ADC]": + self.grad_library = __read_events(input_file, (1, 1e-6, 1e-6, 1e-6, 1e-6), 't', self.grad_library) + elif section == '[ADC]': self.adc_library = __read_events( input_file, (1, 1e-9, 1e-6, 1, 1), event_library=self.adc_library, append=self.system.adc_dead_time ) - elif section == "[DELAYS]": + elif section == '[DELAYS]': if version_combined >= 1004000: - raise RuntimeError("Pulseq file revision 1.4.0 and above MUST NOT contain [DELAYS] section") + raise RuntimeError('Pulseq file revision 1.4.0 and above MUST NOT contain [DELAYS] section') temp_delay_library = __read_events(input_file, (1e-6,)) - elif section == "[SHAPES]": + elif section == '[SHAPES]': self.shape_library = __read_shapes(input_file, version_major == 1 and version_minor < 4) - elif section == "[EXTENSIONS]": + elif section == '[EXTENSIONS]': self.extensions_library = __read_events(input_file) else: - if section[:18] == "extension TRIGGERS": + if section[:18] == 'extension TRIGGERS': extension_id = int(section[18:]) - self.set_extension_string_ID("TRIGGERS", extension_id) + self.set_extension_string_ID('TRIGGERS', extension_id) self.trigger_library = __read_events(input_file, (1, 1, 1e-6, 1e-6), event_library=self.trigger_library) - elif section[:18] == "extension LABELSET": + elif section[:18] == 'extension LABELSET': extension_id = int(section[18:]) - self.set_extension_string_ID("LABELSET", extension_id) - l1 = lambda s: int(s) - l2 = lambda s: get_supported_labels().index(s) + 1 + self.set_extension_string_ID('LABELSET', extension_id) + + def l1(s): + return int(s) + + def l2(s): + return get_supported_labels().index(s) + 1 + self.label_set_library = __read_and_parse_events(input_file, l1, l2) - elif section[:18] == "extension LABELINC": + elif section[:18] == 'extension LABELINC': extension_id = int(section[18:]) - self.set_extension_string_ID("LABELINC", extension_id) - l1 = lambda s: int(s) - l2 = lambda s: get_supported_labels().index(s) + 1 + self.set_extension_string_ID('LABELINC', extension_id) + + def l1(s): + return int(s) + + def l2(s): + return get_supported_labels().index(s) + 1 + self.label_inc_library = __read_and_parse_events(input_file, l1, l2) else: - raise ValueError(f"Unknown section code: {section}") + raise ValueError(f'Unknown section code: {section}') input_file.close() # Close file if version_combined < 1002000: raise ValueError( - f"Unsupported version {version_combined}, only file format revision 1.2.0 (1002000) and above " - f"are supported." + f'Unsupported version {version_combined}, only file format revision 1.2.0 (1002000) and above ' + f'are supported.' ) # Fix blocks, gradients and RF objects imported from older versions @@ -198,8 +207,8 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = # Scan through the gradient objects and update 't'-s (trapezoids) und 'g'-s (free-shape gradients) for i in self.grad_library.data: - if self.grad_library.type[i] == "t": - if self.grad_library.data[i][1] == 0: + if self.grad_library.type[i] == 't': + if self.grad_library.data[i][1] == 0: # noqa: SIM102 if abs(self.grad_library.data[i][0]) == 0 and self.grad_library.data[i][2] > 0: d = self.grad_library.data[i] self.grad_library.update( @@ -209,7 +218,7 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = self.grad_library.type[i], ) - if self.grad_library.data[i][3] == 0: + if self.grad_library.data[i][3] == 0: # noqa: SIM102 if abs(self.grad_library.data[i][0]) == 0 and self.grad_library.data[i][2] > 0: d = self.grad_library.data[i] self.grad_library.update( @@ -219,7 +228,7 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = self.grad_library.type[i], ) - if self.grad_library.type[i] == "g": + if self.grad_library.type[i] == 'g': self.grad_library.update( i, None, @@ -232,7 +241,7 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = ) # For versions prior to 1.4.0 block_durations have not been initialized - self.block_durations = dict() + self.block_durations = {} # Scan through blocks and calculate durations for block_counter in self.block_events: # Insert delay as temporary block_duration @@ -245,7 +254,7 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = self.block_durations[block_counter] = calc_duration(block) # TODO: Is it possible to avoid expensive get_block calls here? - grad_channels = ["gx", "gy", "gz"] + grad_channels = ['gx', 'gy', 'gz'] grad_prev_last = np.zeros(len(grad_channels)) for block_counter in self.block_events: block = self.get_block(block_counter) @@ -260,11 +269,11 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = grad_prev_last[j] = 0 continue - if grad.type == "grad": + if grad.type == 'grad': if grad.delay > 0: grad_prev_last[j] = 0 - if hasattr(grad, "first"): + if hasattr(grad, 'first'): grad_prev_last[j] = grad.last continue @@ -306,7 +315,7 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = grad.first, grad.last, ) - self.grad_library.update_data(amplitude_ID, None, new_data, "g") + self.grad_library.update_data(amplitude_ID, None, new_data, 'g') else: grad_prev_last[j] = 0 @@ -320,12 +329,12 @@ def read(self, path: str, detect_rf_use: bool = False, remove_duplicates: bool = flip_deg = np.abs(np.sum(rf.signal[:-1] * (rf.t[1:] - rf.t[:-1]))) * 360 offresonance_ppm = 1e6 * rf.freq_offset / self.system.B0 / self.system.gamma if flip_deg < 90.01: # Add 0.01 degree to account for rounding errors encountered in very short RF pulses - self.rf_library.type[k] = "e" + self.rf_library.type[k] = 'e' else: if rf.shape_dur > 6e-3 and -3.5 <= offresonance_ppm <= -3.4: # Approx -3.45 - self.rf_library.type[k] = "s" # Saturation (fat-sat) + self.rf_library.type[k] = 's' # Saturation (fat-sat) else: - self.rf_library.type[k] = "r" + self.rf_library.type[k] = 'r' self.rf_library.data[k] = lib_data # Clear block cache for all blocks that contain the modified RF event @@ -353,10 +362,10 @@ def __read_definitions(input_file) -> Dict[str, str]: definitions : dict{str, str} Dict object containing key value pairs of definitions. """ - definitions = dict() + definitions = {} line = __skip_comments(input_file) - while line != -1 and not (line == "" or line[0] == "#"): - tok = line.split(" ") + while line != -1 and not (line == '' or line[0] == '#'): + tok = line.split(' ') try: # Try converting every element into a float [float(x) for x in tok[1:]] value = np.array(tok[1:], dtype=float) @@ -386,18 +395,18 @@ def __read_version(input_file) -> Tuple[int, int, int]: """ line = __strip_line(input_file) major, minor, revision = 0, 0, 0 - while line != "" and line[0] != "#": - tok = line.split(" ") - if tok[0] == "major": + while line != '' and line[0] != '#': + tok = line.split(' ') + if tok[0] == 'major': major = int(tok[1]) - elif tok[0] == "minor": + elif tok[0] == 'minor': minor = int(tok[1]) - elif tok[0] == "revision": + elif tok[0] == 'revision': if len(tok[1]) != 1: # Example: x.y.zpostN tok[1] = tok[1][0] revision = int(tok[1]) else: - raise RuntimeError(f"Incompatible version. Expected: {major}{minor}{revision}") + raise RuntimeError(f'Incompatible version. Expected: {major}{minor}{revision}') line = __strip_line(input_file) return major, minor, revision @@ -423,13 +432,13 @@ def __read_blocks( delay_idx : list Delay IDs. """ - event_table = dict() - block_durations = dict() - delay_idx = dict() + event_table = {} + block_durations = {} + delay_idx = {} line = __strip_line(input_file) - while line != "" and line != "#": - block_events = np.fromstring(line, dtype=int, sep=" ") + while line != '' and line != '#': + block_events = np.fromstring(line, dtype=int, sep=' ') if version_combined <= 1002001: event_table[block_events[0]] = np.array([0, *block_events[2:], 0]) @@ -469,18 +478,17 @@ def __read_events( event_library : EventLibrary Event library containing Pulseq events. """ - if event_library is None: event_library = EventLibrary() line = __strip_line(input_file) - while line != "" and line != "#": - data = np.fromstring(line, dtype=float, sep=" ") + while line != '' and line != '#': + data = np.fromstring(line, dtype=float, sep=' ') event_id = data[0] data = tuple(data[1:] * scale) if append is not None: - data = data + (append,) - if event_type == "": + data = (*data, append) + if event_type == '': event_library.insert(key_id=event_id, new_data=data) else: event_library.insert(key_id=event_id, new_data=data, data_type=event_type) @@ -508,16 +516,16 @@ def __read_and_parse_events(input_file, *args: callable) -> EventLibrary: event_library = EventLibrary() line = __strip_line(input_file) - while line != "" and line != "#": - datas = re.split(r"(\s+)", line) - datas = [d for d in datas if d != " "] - data = np.zeros(len(datas) - 1, dtype=np.int32) - event_id = int(datas[0]) - for i in range(1, len(datas)): + while line != '' and line != '#': + list_of_data_str = re.split(r'(\s+)', line) + list_of_data_str = [d for d in list_of_data_str if d != ' '] + data = np.zeros(len(list_of_data_str) - 1, dtype=np.int32) + event_id = int(list_of_data_str[0]) + for i in range(1, len(list_of_data_str)): if i > len(args): - data[i - 1] = int(datas[i]) + data[i - 1] = int(list_of_data_str[i]) else: - data[i - 1] = args[i - 1](datas[i]) + data[i - 1] = args[i - 1](list_of_data_str[i]) event_library.insert(key_id=event_id, new_data=data) line = __strip_line(input_file) @@ -541,15 +549,15 @@ def __read_shapes(input_file, force_convert_uncompressed: bool) -> EventLibrary: line = __skip_comments(input_file) - while line != -1 and (line != "" or line[0:8] == "shape_id"): - tok = line.split(" ") + while line != -1 and (line != '' or line[0:8] == 'shape_id'): + tok = line.split(' ') shape_id = int(tok[1]) line = __skip_comments(input_file) - tok = line.split(" ") + tok = line.split(' ') num_samples = int(tok[1]) data = [] line = __skip_comments(input_file) - while line != "" and line != "#": + while line != '' and line != '#': data.append(float(line)) line = __strip_line(input_file) line = __skip_comments(input_file, stop_before_section=True) @@ -583,17 +591,16 @@ def __skip_comments(input_file, stop_before_section: bool = False) -> str: First line in `input_file` after skipping one '#' comment block. Note: File pointer is remembered, so successive calls work as expected. """ - temp_pos = input_file.tell() line = __strip_line(input_file) - while line != -1 and (line == "" or line[0] == "#"): + while line != -1 and (line == '' or line[0] == '#'): temp_pos = input_file.tell() line = __strip_line(input_file) if line != -1: - if stop_before_section and line[0] == "[": + if stop_before_section and line[0] == '[': input_file.seek(temp_pos, 0) - next_line = "" + next_line = '' else: next_line = line else: @@ -617,4 +624,4 @@ def __strip_line(input_file) -> str: remembered, and hence successive calls work as expected. Returns -1 for eof. """ line = input_file.readline() # If line is an empty string, end of the file has been reached - return line.strip() if line != "" else -1 + return line.strip() if line != '' else -1 diff --git a/pypulseq/Sequence/sequence.py b/pypulseq/Sequence/sequence.py index c5500be..40fb187 100755 --- a/pypulseq/Sequence/sequence.py +++ b/pypulseq/Sequence/sequence.py @@ -1,44 +1,41 @@ import itertools import math from collections import OrderedDict +from copy import deepcopy from types import SimpleNamespace -from typing import Tuple, List -from typing import Union +from typing import List, Tuple, Union from warnings import warn -from copy import deepcopy try: from typing import Self except ImportError: from typing import TypeVar - Self = TypeVar("Self", bound="Sequence") + Self = TypeVar('Self', bound='Sequence') import matplotlib as mpl import numpy as np -from scipy.interpolate import PPoly from matplotlib import pyplot as plt +from scipy.interpolate import PPoly -from pypulseq import eps -from pypulseq import __version__ -from pypulseq.Sequence import block, parula -from pypulseq.Sequence.ext_test_report import ext_test_report -from pypulseq.Sequence.read_seq import read -from pypulseq.Sequence.write_seq import write as write_seq -from pypulseq.Sequence.calc_pns import calc_pns -from pypulseq.Sequence.calc_grad_spectrum import calculate_gradient_spectrum - +from pypulseq import __version__, eps from pypulseq.calc_rf_center import calc_rf_center from pypulseq.check_timing import check_timing as ext_check_timing from pypulseq.check_timing import print_error_report from pypulseq.decompress_shape import decompress_shape from pypulseq.event_lib import EventLibrary from pypulseq.opts import Opts +from pypulseq.Sequence import block, parula +from pypulseq.Sequence.calc_grad_spectrum import calculate_gradient_spectrum +from pypulseq.Sequence.calc_pns import calc_pns +from pypulseq.Sequence.ext_test_report import ext_test_report +from pypulseq.Sequence.read_seq import read +from pypulseq.Sequence.write_seq import write as write_seq from pypulseq.supported_labels_rf_use import get_supported_labels from pypulseq.utils.cumsum import cumsum -from pypulseq.utils.tracing import trace_enabled, trace, format_trace +from pypulseq.utils.tracing import format_trace, trace, trace_enabled -major, minor, revision = __version__.split(".") +major, minor, revision = __version__.split('.') class Sequence: @@ -79,37 +76,37 @@ def __init__(self, system: Union[Opts, None] = None, use_block_cache: bool = Tru self.block_events = OrderedDict() self.block_trace = OrderedDict() self.use_block_cache = use_block_cache - self.block_cache = dict() + self.block_cache = {} self.next_free_block_ID = 1 - self.definitions = dict() + self.definitions = {} self.rf_raster_time = self.system.rf_raster_time self.grad_raster_time = self.system.grad_raster_time self.adc_raster_time = self.system.adc_raster_time self.block_duration_raster = self.system.block_duration_raster - self.set_definition("AdcRasterTime", self.adc_raster_time) - self.set_definition("BlockDurationRaster", self.block_duration_raster) - self.set_definition("GradientRasterTime", self.grad_raster_time) - self.set_definition("RadiofrequencyRasterTime", self.rf_raster_time) - self.signature_type = "" - self.signature_file = "" - self.signature_value = "" - - self.block_durations = dict() + self.set_definition('AdcRasterTime', self.adc_raster_time) + self.set_definition('BlockDurationRaster', self.block_duration_raster) + self.set_definition('GradientRasterTime', self.grad_raster_time) + self.set_definition('RadiofrequencyRasterTime', self.rf_raster_time) + self.signature_type = '' + self.signature_file = '' + self.signature_value = '' + + self.block_durations = {} self.extension_numeric_idx = [] self.extension_string_idx = [] def __str__(self) -> str: - s = "Sequence:" - s += "\nshape_library: " + str(self.shape_library) - s += "\nrf_library: " + str(self.rf_library) - s += "\ngrad_library: " + str(self.grad_library) - s += "\nadc_library: " + str(self.adc_library) - s += "\ndelay_library: " + str(self.delay_library) - s += "\nextensions_library: " + str(self.extensions_library) - s += "\nrf_raster_time: " + str(self.rf_raster_time) - s += "\ngrad_raster_time: " + str(self.grad_raster_time) - s += "\nblock_events: " + str(len(self.block_events)) + s = 'Sequence:' + s += '\nshape_library: ' + str(self.shape_library) + s += '\nrf_library: ' + str(self.rf_library) + s += '\ngrad_library: ' + str(self.grad_library) + s += '\nadc_library: ' + str(self.adc_library) + s += '\ndelay_library: ' + str(self.delay_library) + s += '\nextensions_library: ' + str(self.extensions_library) + s += '\nrf_raster_time: ' + str(self.rf_raster_time) + s += '\ngrad_raster_time: ' + str(self.grad_raster_time) + s += '\nblock_events: ' + str(len(self.block_events)) return s def adc_times(self, time_range: Union[List[float], None] = None) -> Tuple[np.ndarray, np.ndarray]: @@ -123,7 +120,6 @@ def adc_times(self, time_range: Union[List[float], None] = None) -> Tuple[np.nda fp_adc : np.ndarray Contains frequency and phase offsets of each ADC object (not samples). """ - # Collect ADC timing data t_adc = [] fp_adc = [] @@ -133,9 +129,9 @@ def adc_times(self, time_range: Union[List[float], None] = None) -> Tuple[np.nda blocks = self.block_events else: if len(time_range) != 2: - raise ValueError("Time range must be list of two elements") + raise ValueError('Time range must be list of two elements') if time_range[0] > time_range[1]: - raise ValueError("End time of time_range must be after begin time") + raise ValueError('End time of time_range must be after begin time') # Calculate end times of each block bd = np.array(list(self.block_durations.values())) @@ -143,7 +139,7 @@ def adc_times(self, time_range: Union[List[float], None] = None) -> Tuple[np.nda # Search block end times for start of time range begin_block = np.searchsorted(t, time_range[0]) # Search block begin times for end of time range - end_block = np.searchsorted(t - bd, time_range[1], side="right") + end_block = np.searchsorted(t - bd, time_range[1], side='right') blocks = list(self.block_durations.keys())[begin_block:end_block] curr_dur = t[begin_block] - bd[begin_block] @@ -170,7 +166,8 @@ def add_block(self, *args: SimpleNamespace) -> None: """ Add a new block/multiple events to the sequence. Adds a sequence block with provided as a block structure - See also: + See Also + -------- - `pypulseq.Sequence.sequence.Sequence.set_block()` - `pypulseq.make_adc.make_adc()` - `pypulseq.make_trapezoid.make_trapezoid()` @@ -181,7 +178,6 @@ def add_block(self, *args: SimpleNamespace) -> None: args : SimpleNamespace Block structure or events to be added as a block to `Sequence`. """ - if trace_enabled(): self.block_trace[self.next_free_block_ID] = SimpleNamespace(block=trace()) @@ -195,9 +191,9 @@ def calculate_gradient_spectrum( frequency_oversampling: float = 3, time_range: Union[List[float], None] = None, plot: bool = True, - combine_mode: str = "max", + combine_mode: str = 'max', use_derivative: bool = False, - acoustic_resonances: List[dict] = [], + acoustic_resonances: Union[List[dict], None] = None, ) -> Tuple[List[np.ndarray], np.ndarray, np.ndarray, np.ndarray]: """ Calculates the gradient spectrum of the sequence. Returns a spectrogram @@ -248,6 +244,9 @@ def calculate_gradient_spectrum( Time axis of the spectrograms (only relevant when combine_mode == 'none'). """ + if acoustic_resonances is None: + acoustic_resonances = [] + return calculate_gradient_spectrum( self, max_frequency=max_frequency, @@ -289,7 +288,7 @@ def calculate_kspace( Sampling timepoints. """ if np.any(np.abs(trajectory_delay) > 100e-6): - raise Warning(f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high") + raise Warning(f'Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high') total_duration = sum(self.block_durations.values()) @@ -410,7 +409,7 @@ def calculate_kspace( if ii_next_excitation >= 0 and i_excitation[ii_next_excitation] == i_period: if abs(t_ktraj[i_period] - t_excitation[ii_next_excitation]) > t_acc: raise Warning( - f"abs(t_ktraj[i_period]-t_excitation[ii_next_excitation]) < {t_acc} failed for ii_next_excitation={ii_next_excitation} error={t_ktraj(i_period) - t_excitation(ii_next_excitation)}" + f'abs(t_ktraj[i_period]-t_excitation[ii_next_excitation]) < {t_acc} failed for ii_next_excitation={ii_next_excitation} error={t_ktraj(i_period) - t_excitation(ii_next_excitation)}' ) dk = -k_traj[:, i_period] if i_period > 0: @@ -437,7 +436,7 @@ def calculate_kspacePP( gradient_offset: Union[float, List[float], np.ndarray] = 0, ) -> Tuple[np.array, np.array, np.array, np.array, np.array]: warn( - "Sequence.calculate_kspacePP has been deprecated, use calculate_kspace instead", + 'Sequence.calculate_kspacePP has been deprecated, use calculate_kspace instead', DeprecationWarning, stacklevel=2, ) @@ -491,7 +490,6 @@ def check_timing(self, print_errors=False) -> Tuple[bool, List[SimpleNamespace]] error_report : List[SimpleNamespace] Error report in case of timing errors. """ - is_ok, error_report = ext_check_timing(self) if not is_ok and print_errors: @@ -521,7 +519,7 @@ def duration(self) -> Tuple[int, int, np.ndarray]: return duration, num_blocks, event_count - def evaluate_labels(self, init: Union[dict, None] = None, evolution: str = "none") -> dict: + def evaluate_labels(self, init: Union[dict, None] = None, evolution: str = 'none') -> dict: """ Evaluate label values of the entire sequence. @@ -562,7 +560,7 @@ def evaluate_labels(self, init: Union[dict, None] = None, evolution: str = "none dictionary. """ - labels = init or dict() + labels = init or {} label_evolution = [] # TODO: MATLAB implementation includes block_range parameter. But in @@ -575,7 +573,7 @@ def evaluate_labels(self, init: Union[dict, None] = None, evolution: str = "none if block.label is not None: # Current block has labels for lab in block.label.values(): - if lab.type == "labelinc": + if lab.type == 'labelinc': # Increment label if lab.label not in labels: labels[lab.label] = 0 @@ -585,16 +583,16 @@ def evaluate_labels(self, init: Union[dict, None] = None, evolution: str = "none # Set label labels[lab.label] = lab.value - if evolution == "label": + if evolution == 'label': label_evolution.append(dict(labels)) - if evolution == "blocks" or (evolution == "adc" and block.adc is not None): + if evolution == 'blocks' or (evolution == 'adc' and block.adc is not None): label_evolution.append(dict(labels)) # Convert evolutions into label dictionary if len(label_evolution) > 0: for lab in labels: - labels[lab] = np.array([e[lab] if lab in e else 0 for e in label_evolution]) + labels[lab] = np.array([e.get(lab, 0) for e in label_evolution]) return labels @@ -615,7 +613,8 @@ def get_block(self, block_index: int) -> SimpleNamespace: Return a block of the sequence specified by the index. The block is created from the sequence data with all events and shapes decompressed. - See also: + See Also + -------- - `pypulseq.Sequence.sequence.Sequence.set_block()`. - `pypulseq.Sequence.sequence.Sequence.add_block()`. @@ -651,7 +650,7 @@ def get_definition(self, key: str) -> str: if key in self.definitions: return self.definitions[key] else: - return "" + return '' def get_extension_type_ID(self, extension_string: str) -> int: """ @@ -705,7 +704,7 @@ def get_extension_type_string(self, extension_id: int) -> str: if extension_id in self.extension_numeric_idx: num = self.extension_numeric_idx.index(extension_id) else: - raise ValueError(f"Extension for the given ID - {extension_id} - is unknown.") + raise ValueError(f'Extension for the given ID - {extension_id} - is unknown.') extension_str = self.extension_string_idx[num] return extension_str @@ -737,7 +736,7 @@ def get_gradients( expressed as scipy PPoly objects. """ if np.any(np.abs(trajectory_delay) > 100e-6): - raise Warning(f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high") + raise Warning(f'Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high') total_duration = sum(self.block_durations.values()) @@ -774,7 +773,7 @@ def get_gradients( if np.abs(gradient_delays[j]) > eps: gw[0] = gw[0] - gradient_delays[j] # Anisotropic gradient delay support if not np.all(np.isfinite(gw)): - raise Warning("Not all elements of the generated waveform are finite.") + raise Warning('Not all elements of the generated waveform are finite.') teps = 1e-12 _temp1 = np.array(([gw[0, 0] - 2 * teps, gw[0, 0] - teps], [0, 0])) @@ -808,10 +807,10 @@ def mod_grad_axis(self, axis: str, modifier: int) -> None: RuntimeError If same gradient event is used on multiple axes. """ - if axis not in ["x", "y", "z"]: + if axis not in ['x', 'y', 'z']: raise ValueError(f"Invalid axis. Must be one of 'x', 'y','z'. Passed: {axis}") - channel_num = ["x", "y", "z"].index(axis) + channel_num = ['x', 'y', 'z'].index(axis) other_channels = [0, 1, 2] other_channels.remove(channel_num) @@ -823,11 +822,11 @@ def mod_grad_axis(self, axis: str, modifier: int) -> None: selected_events = selected_events[selected_events != 0] other_events = np.unique(all_grad_events[:, other_channels]) if len(np.intersect1d(selected_events, other_events)) > 0: - raise RuntimeError("mod_grad_axis does not yet support the same gradient event used on multiple axes.") + raise RuntimeError('mod_grad_axis does not yet support the same gradient event used on multiple axes.') for i in range(len(selected_events)): self.grad_library.data[selected_events[i]][0] *= modifier - if self.grad_library.type[selected_events[i]] == "g" and self.grad_library.lengths[selected_events[i]] == 5: + if self.grad_library.type[selected_events[i]] == 'g' and self.grad_library.lengths[selected_events[i]] == 5: # Need to update first and last fields self.grad_library.data[selected_events[i]][3] *= modifier self.grad_library.data[selected_events[i]][4] *= modifier @@ -838,8 +837,8 @@ def plot( show_blocks: bool = False, save: bool = False, time_range=(0, np.inf), - time_disp: str = "s", - grad_disp: str = "kHz/m", + time_disp: str = 's', + grad_disp: str = 'kHz/m', plot_now: bool = True, ) -> None: """ @@ -847,7 +846,7 @@ def plot( Parameters ---------- - label : str, defualt=str() + label : str, default=str() Plot label values for ADC events: in this example for LIN and REP labels; other valid labes are accepted as a comma-separated list. save : bool, default=False @@ -867,18 +866,18 @@ def plot( plot_type : str, default='Gradient' Gradients display type, must be one of either 'Gradient' or 'Kspace'. """ - mpl.rcParams["lines.linewidth"] = 0.75 # Set default Matplotlib linewidth + mpl.rcParams['lines.linewidth'] = 0.75 # Set default Matplotlib linewidth - valid_time_units = ["s", "ms", "us"] - valid_grad_units = ["kHz/m", "mT/m"] + valid_time_units = ['s', 'ms', 'us'] + valid_grad_units = ['kHz/m', 'mT/m'] valid_labels = get_supported_labels() - if not all([isinstance(x, (int, float)) for x in time_range]) or len(time_range) != 2: - raise ValueError("Invalid time range") + if not all(isinstance(x, (int, float)) for x in time_range) or len(time_range) != 2: + raise ValueError('Invalid time range') if time_disp not in valid_time_units: - raise ValueError("Unsupported time unit") + raise ValueError('Unsupported time unit') if grad_disp not in valid_grad_units: - raise ValueError("Unsupported gradient unit. Supported gradient units are: " + str(valid_grad_units)) + raise ValueError('Unsupported gradient unit. Supported gradient units are: ' + str(valid_grad_units)) fig1, fig2 = plt.figure(1), plt.figure(2) sp11 = fig1.add_subplot(311) @@ -900,7 +899,7 @@ def plot( label_defined = False label_idx_to_plot = [] label_legend_to_plot = [] - label_store = dict() + label_store = {} for i in range(len(valid_labels)): label_store[valid_labels[i]] = 0 if valid_labels[i] in label.upper(): @@ -925,24 +924,24 @@ def plot( block = self.get_block(block_counter) is_valid = time_range[0] <= t0 + self.block_durations[block_counter] and t0 <= time_range[1] if is_valid: - if getattr(block, "label", None) is not None: + if getattr(block, 'label', None) is not None: for i in range(len(block.label)): - if block.label[i].type == "labelinc": + if block.label[i].type == 'labelinc': label_store[block.label[i].label] += block.label[i].value else: label_store[block.label[i].label] = block.label[i].value label_defined = True - if getattr(block, "adc", None) is not None: # ADC + if getattr(block, 'adc', None) is not None: # ADC adc = block.adc # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this # is the present convention - the samples are shifted by 0.5 dwell t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell - sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), "rx") + sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), 'rx') sp13.plot( t_factor * (t0 + t), np.angle(np.exp(1j * adc.phase_offset) * np.exp(1j * 2 * np.pi * t * adc.freq_offset)), - "b.", + 'b.', markersize=0.25, ) @@ -953,13 +952,13 @@ def plot( _t = [t_factor * t] * len(lbl_vals) # Plot each label individually to retrieve each corresponding Line2D object p = itertools.chain.from_iterable( - [sp11.plot(__t, _lbl_vals, ".") for __t, _lbl_vals in zip(_t, lbl_vals)] + [sp11.plot(__t, _lbl_vals, '.') for __t, _lbl_vals in zip(_t, lbl_vals)] ) if len(label_legend_to_plot) != 0: - sp11.legend(p, label_legend_to_plot, loc="upper left") + sp11.legend(p, label_legend_to_plot, loc='upper left') label_legend_to_plot = [] - if getattr(block, "rf", None) is not None: # RF + if getattr(block, 'rf', None) is not None: # RF rf = block.rf tc, ic = calc_rf_center(rf) time = rf.t @@ -985,14 +984,14 @@ def plot( * np.exp(1j * rf.phase_offset) * np.exp(1j * 2 * math.pi * time[ic] * rf.freq_offset) ), - "xb", + 'xb', ) - grad_channels = ["gx", "gy", "gz"] + grad_channels = ['gx', 'gy', 'gz'] for x in range(len(grad_channels)): # Gradients if getattr(block, grad_channels[x], None) is not None: grad = getattr(block, grad_channels[x]) - if grad.type == "grad": + if grad.type == 'grad': # We extend the shape by adding the first and the last points in an effort of making the # display a bit less confusing... time = grad.delay + np.array([0, *grad.tt, grad.shape_dur]) @@ -1011,15 +1010,15 @@ def plot( fig2_subplots[x].plot(t_factor * (t0 + time), waveform) t0 += self.block_durations[block_counter] - grad_plot_labels = ["x", "y", "z"] - sp11.set_ylabel("ADC") - sp12.set_ylabel("RF mag (Hz)") - sp13.set_ylabel("RF/ADC phase (rad)") - sp13.set_xlabel(f"t ({time_disp})") + grad_plot_labels = ['x', 'y', 'z'] + sp11.set_ylabel('ADC') + sp12.set_ylabel('RF mag (Hz)') + sp13.set_ylabel('RF/ADC phase (rad)') + sp13.set_xlabel(f't ({time_disp})') for x in range(3): _label = grad_plot_labels[x] - fig2_subplots[x].set_ylabel(f"G{_label} ({grad_disp})") - fig2_subplots[-1].set_xlabel(f"t ({time_disp})") + fig2_subplots[x].set_ylabel(f'G{_label} ({grad_disp})') + fig2_subplots[-1].set_xlabel(f't ({time_disp})') # Setting display limits disp_range = t_factor * np.array([time_range[0], min(t0, time_range[1])]) @@ -1032,8 +1031,8 @@ def plot( fig1.tight_layout() fig2.tight_layout() if save: - fig1.savefig("seq_plot1.jpg") - fig2.savefig("seq_plot2.jpg") + fig1.savefig('seq_plot1.jpg') + fig2.savefig('seq_plot2.jpg') if plot_now: plt.show() @@ -1101,7 +1100,7 @@ def remove_duplicates(self, in_place: bool = False) -> Self: # Remap shape IDs of arbitrary gradient events for grad_id in seq_copy.grad_library.data: - if seq_copy.grad_library.type[grad_id] == "g": + if seq_copy.grad_library.type[grad_id] == 'g': data = seq_copy.grad_library.data[grad_id] new_data = (data[0],) + (mapping[data[1]], mapping[data[2]]) + data[3:] if data != new_data: @@ -1156,7 +1155,7 @@ def rf_from_lib_data(self, lib_data: list, use: str = str()) -> SimpleNamespace: RF object constructed from `lib_data`. """ rf = SimpleNamespace() - rf.type = "rf" + rf.type = 'rf' amplitude, mag_shape, phase_shape = lib_data[0], lib_data[1], lib_data[2] shape_data = self.shape_library.data[mag_shape] @@ -1187,15 +1186,15 @@ def rf_from_lib_data(self, lib_data: list, use: str = str()) -> SimpleNamespace: rf.dead_time = self.system.rf_dead_time rf.ringdown_time = self.system.rf_ringdown_time - if use != "": + if use != '': use_cases = { - "e": "excitation", - "r": "refocusing", - "i": "inversion", - "s": "saturation", - "p": "preparation", + 'e': 'excitation', + 'r': 'refocusing', + 'i': 'inversion', + 's': 'saturation', + 'p': 'preparation', } - rf.use = use_cases[use] if use in use_cases else "undefined" + rf.use = use_cases.get(use, 'undefined') return rf @@ -1216,7 +1215,6 @@ def rf_times( fp_refocusing : np.ndarray Contains frequency and phase offsets of the excitation RF pulses """ - # Collect RF timing data t_excitation = [] fp_excitation = [] @@ -1228,9 +1226,9 @@ def rf_times( blocks = self.block_events else: if len(time_range) != 2: - raise ValueError("Time range must be list of two elements") + raise ValueError('Time range must be list of two elements') if time_range[0] > time_range[1]: - raise ValueError("End time of time_range must be after begin time") + raise ValueError('End time of time_range must be after begin time') # Calculate end times of each block bd = np.array(list(self.block_durations.values())) @@ -1238,7 +1236,7 @@ def rf_times( # Search block end times for start of time range begin_block = np.searchsorted(t, time_range[0]) # Search block begin times for end of time range - end_block = np.searchsorted(t - bd, time_range[1], side="right") + end_block = np.searchsorted(t - bd, time_range[1], side='right') blocks = list(self.block_durations.keys())[begin_block:end_block] curr_dur = t[begin_block] - bd[begin_block] @@ -1248,13 +1246,13 @@ def rf_times( if block.rf is not None: rf = block.rf t = rf.delay + calc_rf_center(rf)[0] - if not hasattr(rf, "use") or block.rf.use in [ - "excitation", - "undefined", + if not hasattr(rf, 'use') or block.rf.use in [ + 'excitation', + 'undefined', ]: t_excitation.append(curr_dur + t) fp_excitation.append([block.rf.freq_offset, block.rf.phase_offset]) - elif block.rf.use == "refocusing": + elif block.rf.use == 'refocusing': t_refocusing.append(curr_dur + t) fp_refocusing.append([block.rf.freq_offset, block.rf.phase_offset]) @@ -1278,7 +1276,8 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: from events and store at position specified by index. The block or events are provided in uncompressed form and will be stored in the compressed, non-redundant internal libraries. - See also: + See Also + -------- - `pypulseq.Sequence.sequence.Sequence.get_block()` - `pypulseq.Sequence.sequence.Sequence.add_block()` @@ -1289,7 +1288,6 @@ def set_block(self, block_index: int, *args: SimpleNamespace) -> None: args : SimpleNamespace Block or events to be replaced/added or created at `block_index`. """ - if trace_enabled(): self.block_trace[block_index] = SimpleNamespace(block=trace()) @@ -1312,11 +1310,10 @@ def set_definition(self, key: str, value: Union[float, int, list, np.ndarray, st value : int, list, np.ndarray, str or tuple Definition value. """ - if key == "FOV": - if np.max(value) > 1: - text = "Definition FOV uses values exceeding 1 m. " - text += "New Pulseq interpreters expect values in units of meters." - warn(text) + if key == 'FOV' and np.max(value) > 1: + text = 'Definition FOV uses values exceeding 1 m. ' + text += 'New Pulseq interpreters expect values in units of meters.' + warn(text) self.definitions[key] = value @@ -1337,7 +1334,7 @@ def set_extension_string_ID(self, extension_str: str, extension_id: int) -> None If given numeric or string extension ID is not unique. """ if extension_str in self.extension_string_idx or extension_id in self.extension_numeric_idx: - raise ValueError("Numeric or string ID is not unique") + raise ValueError('Numeric or string ID is not unique') self.extension_numeric_idx.append(extension_id) self.extension_string_idx.append(extension_str) @@ -1364,7 +1361,7 @@ def waveforms(self, append_RF: bool = False, time_range: Union[List[float], None ------- wave_data : np.ndarray """ - grad_channels = ["gx", "gy", "gz"] + grad_channels = ['gx', 'gy', 'gz'] # Collect shape pieces if append_RF: @@ -1380,9 +1377,9 @@ def waveforms(self, append_RF: bool = False, time_range: Union[List[float], None blocks = self.block_events else: if len(time_range) != 2: - raise ValueError("Time range must be list of two elements") + raise ValueError('Time range must be list of two elements') if time_range[0] > time_range[1]: - raise ValueError("End time of time_range must be after begin time") + raise ValueError('End time of time_range must be after begin time') # Calculate end times of each block bd = np.array(list(self.block_durations.values())) @@ -1390,7 +1387,7 @@ def waveforms(self, append_RF: bool = False, time_range: Union[List[float], None # Search block end times for start of time range begin_block = np.searchsorted(t, time_range[0]) # Search block begin times for end of time range - end_block = np.searchsorted(t - bd, time_range[1], side="right") + end_block = np.searchsorted(t - bd, time_range[1], side='right') blocks = list(self.block_durations.keys())[begin_block:end_block] curr_dur = t[begin_block] - bd[begin_block] @@ -1400,7 +1397,7 @@ def waveforms(self, append_RF: bool = False, time_range: Union[List[float], None for j in range(len(grad_channels)): grad = getattr(block, grad_channels[j]) if grad is not None: # Gradients - if grad.type == "grad": + if grad.type == 'grad': # Check if we have an extended trapezoid or an arbitrary gradient on a regular raster tt_rast = grad.tt / self.grad_raster_time + 0.5 if np.all(np.abs(tt_rast - np.arange(1, len(tt_rast) + 1)) < eps): # Arbitrary gradient @@ -1513,7 +1510,7 @@ def waveforms(self, append_RF: bool = False, time_range: Union[List[float], None rftdiff = np.diff(wave_data[j][0]) if np.any(rftdiff < eps): - raise Warning("Time vector elements are not monotonically increasing.") + raise Warning('Time vector elements are not monotonically increasing.') return wave_data @@ -1543,7 +1540,6 @@ def waveforms_and_times( fp_adc : np.ndarray Contains frequency and phase offsets of each ADC object (not samples). """ - wave_data = self.waveforms(append_RF=append_RF, time_range=time_range) t_excitation, fp_excitation, t_refocusing, fp_refocusing = self.rf_times(time_range=time_range) t_adc, fp_adc = self.adc_times(time_range=time_range) @@ -1584,8 +1580,8 @@ def waveforms_export(self, time_range=(0, np.inf)) -> dict: - `time_unit`: [seconds], """ # Check time range validity - if not all([isinstance(x, (int, float)) for x in time_range]) or len(time_range) != 2: - raise ValueError("Invalid time range") + if not all(isinstance(x, (int, float)) for x in time_range) or len(time_range) != 2: + raise ValueError('Invalid time range') t0 = 0 adc_t_all = np.array([]) @@ -1637,12 +1633,12 @@ def waveforms_export(self, time_range=(0, np.inf)) -> dict: rf_t_centers = np.concatenate((rf_t_centers, [rf_t[ic]])) rf_signal_centers = np.concatenate((rf_signal_centers, [rf[ic]])) - grad_channels = ["gx", "gy", "gz"] + grad_channels = ['gx', 'gy', 'gz'] for x in range(len(grad_channels)): # Check each gradient channel: x, y, and z if getattr(block, grad_channels[x]) is not None: # If this channel is on in current block grad = getattr(block, grad_channels[x]) - if grad.type == "grad": # Arbitrary gradient option + if grad.type == 'grad': # Arbitrary gradient option # In place unpacking of grad.t with the starred expression g_t = ( t0 @@ -1664,34 +1660,34 @@ def waveforms_export(self, time_range=(0, np.inf)) -> dict: ) g = 1e-3 * grad.amplitude * np.array([0, 0, 1, 1, 0]) - if grad.channel == "x": + if grad.channel == 'x': gx_t_all = np.concatenate((gx_t_all, g_t)) gx_all = np.concatenate((gx_all, g)) - elif grad.channel == "y": + elif grad.channel == 'y': gy_t_all = np.concatenate((gy_t_all, g_t)) gy_all = np.concatenate((gy_all, g)) - elif grad.channel == "z": + elif grad.channel == 'z': gz_t_all = np.concatenate((gz_t_all, g_t)) gz_all = np.concatenate((gz_all, g)) t0 += self.block_durations[block_counter] # "Current time" gets updated to end of block just examined all_waveforms = { - "t_adc": adc_t_all, - "t_rf": rf_t_all, - "t_rf_centers": rf_t_centers, - "t_gx": gx_t_all, - "t_gy": gy_t_all, - "t_gz": gz_t_all, - "adc": adc_signal_all, - "rf": rf_signal_all, - "rf_centers": rf_signal_centers, - "gx": gx_all, - "gy": gy_all, - "gz": gz_all, - "grad_unit": "[kHz/m]", - "rf_unit": "[Hz]", - "time_unit": "[seconds]", + 't_adc': adc_t_all, + 't_rf': rf_t_all, + 't_rf_centers': rf_t_centers, + 't_gx': gx_t_all, + 't_gy': gy_t_all, + 't_gz': gz_t_all, + 'adc': adc_signal_all, + 'rf': rf_signal_all, + 'rf_centers': rf_signal_centers, + 'gx': gx_all, + 'gy': gy_all, + 'gz': gz_all, + 'grad_unit': '[kHz/m]', + 'rf_unit': '[Hz]', + 'time_unit': '[seconds]', } return all_waveforms @@ -1723,29 +1719,29 @@ def write( if check_timing: is_ok, error_report = self.check_timing() if not is_ok: - warn(f"write(): {len(error_report)} timing errors found in the sequence", stacklevel=2) + warn(f'write(): {len(error_report)} timing errors found in the sequence', stacklevel=2) # Calculate sequence duration and stored it in the TotalDuration definition - self.set_definition("TotalDuration", sum(self.block_durations.values())) + self.set_definition('TotalDuration', sum(self.block_durations.values())) # Check whether all gradients in the last block are ramped down properly last_block_id = next(reversed(self.block_events)) last_block = self.get_block(last_block_id) - for channel, event in zip(("x", "y", "z"), (last_block.gx, last_block.gy, last_block.gz)): + for channel, event in zip(('x', 'y', 'z'), (last_block.gx, last_block.gy, last_block.gz)): if ( event is not None - and event.type == "grad" + and event.type == 'grad' and abs(event.last) > self.system.max_slew * self.system.grad_raster_time ): - warn_msg = f"write(): Gradient on channel {channel} in last sequence block does not ramp down to 0" + warn_msg = f'write(): Gradient on channel {channel} in last sequence block does not ramp down to 0' if trace_enabled(): trace = self.block_trace.get(last_block_id, None) - if hasattr(trace, "block"): - warn_msg += "\nLast block defined here:\n" + format_trace(trace.block) - if hasattr(trace, "g" + channel): - warn_msg += f"\n`g{channel}` defined here:\n" + format_trace(getattr(trace, "g" + channel)) + if hasattr(trace, 'block'): + warn_msg += '\nLast block defined here:\n' + format_trace(trace.block) + if hasattr(trace, 'g' + channel): + warn_msg += f'\n`g{channel}` defined here:\n' + format_trace(getattr(trace, 'g' + channel)) warn(warn_msg, stacklevel=2) @@ -1754,8 +1750,8 @@ def write( # Return the sequence md5 signature if requested if signature is not None: - self.signature_type = "md5" - self.signature_file = "text" + self.signature_type = 'md5' + self.signature_file = 'text' self.signature_value = signature return signature else: diff --git a/pypulseq/Sequence/write_seq.py b/pypulseq/Sequence/write_seq.py index 14eca9b..ce9fc91 100755 --- a/pypulseq/Sequence/write_seq.py +++ b/pypulseq/Sequence/write_seq.py @@ -21,11 +21,11 @@ def write(self, file_name: Union[str, Path], create_signature, remove_duplicates remove_duplicates : bool Before writing, remove and remap events that would be duplicates after the rounding done during writing - + Returns ------- - md5 or None : If create_signature is True, it returns the written .seq file's signature as a string, - otherwise it returns None. Note that, if remove_duplicates is True, signature belongs to the + md5 or None : If create_signature is True, it returns the written .seq file's signature as a string, + otherwise it returns None. Note that, if remove_duplicates is True, signature belongs to the deduplicated sequences signature, and not the Sequence that is stored in the Sequence object. Raises @@ -45,48 +45,44 @@ def write(self, file_name: Union[str, Path], create_signature, remove_duplicates if remove_duplicates: self = self.remove_duplicates() - with open(file_name, "w") as output_file: - output_file.write("# Pulseq sequence file\n") - output_file.write("# Created by PyPulseq\n\n") + with open(file_name, 'w') as output_file: + output_file.write('# Pulseq sequence file\n') + output_file.write('# Created by PyPulseq\n\n') - output_file.write("[VERSION]\n") - output_file.write(f"major {self.version_major}\n") - output_file.write(f"minor {self.version_minor}\n") - output_file.write(f"revision {self.version_revision}\n") - output_file.write("\n") + output_file.write('[VERSION]\n') + output_file.write(f'major {self.version_major}\n') + output_file.write(f'minor {self.version_minor}\n') + output_file.write(f'revision {self.version_revision}\n') + output_file.write('\n') if len(self.definitions) != 0: - output_file.write("[DEFINITIONS]\n") - keys = sorted(list(self.definitions.keys())) + output_file.write('[DEFINITIONS]\n') + keys = sorted(self.definitions.keys()) values = [self.definitions[k] for k in keys] for block_counter in range(len(keys)): - output_file.write(f"{keys[block_counter]} ") + output_file.write(f'{keys[block_counter]} ') if isinstance(values[block_counter], str): - output_file.write(values[block_counter] + " ") + output_file.write(values[block_counter] + ' ') elif isinstance(values[block_counter], (int, float)): - output_file.write(f"{values[block_counter]:0.9g} ") - elif isinstance( - values[block_counter], (list, tuple, np.ndarray) - ): # For example, [FOVx, FOVy, FOVz] + output_file.write(f'{values[block_counter]:0.9g} ') + elif isinstance(values[block_counter], (list, tuple, np.ndarray)): # e.g. [FOV_x, FOV_y, FOV_z] for i in range(len(values[block_counter])): if isinstance(values[block_counter][i], (int, float)): - output_file.write(f"{values[block_counter][i]:0.9g} ") + output_file.write(f'{values[block_counter][i]:0.9g} ') else: - output_file.write(f"{values[block_counter][i]} ") + output_file.write(f'{values[block_counter][i]} ') else: - raise RuntimeError("Unsupported definition") - output_file.write("\n") - output_file.write("\n") + raise RuntimeError('Unsupported definition') + output_file.write('\n') + output_file.write('\n') - output_file.write("# Format of blocks:\n") - output_file.write("# NUM DUR RF GX GY GZ ADC EXT\n") - output_file.write("[BLOCKS]\n") - id_format_width = "{:" + str(len(str(len(self.block_events)))) + "d}" - id_format_str = id_format_width + " {:3d} {:3d} {:3d} {:3d} {:3d} {:2d} {:2d}\n" + output_file.write('# Format of blocks:\n') + output_file.write('# NUM DUR RF GX GY GZ ADC EXT\n') + output_file.write('[BLOCKS]\n') + id_format_width = '{:' + str(len(str(len(self.block_events)))) + 'd}' + id_format_str = id_format_width + ' {:3d} {:3d} {:3d} {:3d} {:3d} {:2d} {:2d}\n' for block_counter in self.block_events: - block_duration = ( - self.block_durations[block_counter] / self.block_duration_raster - ) + block_duration = self.block_durations[block_counter] / self.block_duration_raster block_duration_rounded = round(block_duration) assert abs(block_duration_rounded - block_duration) < 1e-6 @@ -99,43 +95,35 @@ def write(self, file_name: Union[str, Path], create_signature, remove_duplicates ) ) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.rf_library.data) != 0: - output_file.write("# Format of RF events:\n") - output_file.write( - "# id amplitude mag_id phase_id time_shape_id delay freq phase\n" - ) - output_file.write( - "# .. Hz .... .... .... us Hz rad\n" - ) - output_file.write("[RF]\n") - id_format_str = "{:.0f} {:12g} {:.0f} {:.0f} {:.0f} {:g} {:g} {:g}\n" # Refer lines 20-21 + output_file.write('# Format of RF events:\n') + output_file.write('# id amplitude mag_id phase_id time_shape_id delay freq phase\n') + output_file.write('# .. Hz .... .... .... us Hz rad\n') + output_file.write('[RF]\n') + id_format_str = '{:.0f} {:12g} {:.0f} {:.0f} {:.0f} {:g} {:g} {:g}\n' # Refer lines 20-21 for k in self.rf_library.data: lib_data1 = self.rf_library.data[k][0:4] lib_data2 = self.rf_library.data[k][5:7] - delay = ( - round(self.rf_library.data[k][4] / self.rf_raster_time) - * self.rf_raster_time - * 1e6 - ) + delay = round(self.rf_library.data[k][4] / self.rf_raster_time) * self.rf_raster_time * 1e6 s = id_format_str.format(k, *lib_data1, delay, *lib_data2) output_file.write(s) - output_file.write("\n") + output_file.write('\n') grad_lib_values = np.array(list(self.grad_library.type.values())) - arb_grad_mask = grad_lib_values == "g" if self.grad_library.type else False - trap_grad_mask = grad_lib_values == "t" if self.grad_library.type else False + arb_grad_mask = grad_lib_values == 'g' if self.grad_library.type else False + trap_grad_mask = grad_lib_values == 't' if self.grad_library.type else False if np.any(arb_grad_mask): - output_file.write("# Format of arbitrary gradients:\n") + output_file.write('# Format of arbitrary gradients:\n') output_file.write( - "# time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster)\n" + '# time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster)\n' ) - output_file.write("# id amplitude amp_shape_id time_shape_id delay\n") - output_file.write("# .. Hz/m .. .. us\n") - output_file.write("[GRADIENTS]\n") - id_format_str = "{:.0f} {:12g} {:.0f} {:.0f} {:.0f}\n" # Refer lines 20-21 + output_file.write('# id amplitude amp_shape_id time_shape_id delay\n') + output_file.write('# .. Hz/m .. .. us\n') + output_file.write('[GRADIENTS]\n') + id_format_str = '{:.0f} {:12g} {:.0f} {:.0f} {:.0f}\n' # Refer lines 20-21 keys = np.array(list(self.grad_library.data.keys())) for k in keys[arb_grad_mask]: s = id_format_str.format( @@ -144,19 +132,17 @@ def write(self, file_name: Union[str, Path], create_signature, remove_duplicates round(self.grad_library.data[k][3] * 1e6), ) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if np.any(trap_grad_mask): - output_file.write("# Format of trapezoid gradients:\n") - output_file.write("# id amplitude rise flat fall delay\n") - output_file.write("# .. Hz/m us us us us\n") - output_file.write("[TRAP]\n") + output_file.write('# Format of trapezoid gradients:\n') + output_file.write('# id amplitude rise flat fall delay\n') + output_file.write('# .. Hz/m us us us us\n') + output_file.write('[TRAP]\n') keys = np.array(list(self.grad_library.data.keys())) - id_format_str = "{:2.0f} {:12g} {:3.0f} {:4.0f} {:3.0f} {:3.0f}\n" + id_format_str = '{:2.0f} {:12g} {:3.0f} {:4.0f} {:3.0f} {:3.0f}\n' for k in keys[trap_grad_mask]: - data = np.copy( - self.grad_library.data[k] - ) # Make a copy to leave the original untouched + data = np.copy(self.grad_library.data[k]) # Make a copy to leave the original untouched data[1:] = np.round(1e6 * data[1:]) """ Python & Numpy always round to nearest even value - inconsistent with MATLAB Pulseq's .seq files. @@ -165,120 +151,106 @@ def write(self, file_name: Union[str, Path], create_signature, remove_duplicates """ s = id_format_str.format(k, *data) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.adc_library.data) != 0: - output_file.write("# Format of ADC events:\n") - output_file.write("# id num dwell delay freq phase\n") - output_file.write("# .. .. ns us Hz rad\n") - output_file.write("[ADC]\n") - id_format_str = ( - "{:.0f} {:.0f} {:.0f} {:.0f} {:g} {:g}\n" # Refer lines 20-21 - ) + output_file.write('# Format of ADC events:\n') + output_file.write('# id num dwell delay freq phase\n') + output_file.write('# .. .. ns us Hz rad\n') + output_file.write('[ADC]\n') + id_format_str = '{:.0f} {:.0f} {:.0f} {:.0f} {:g} {:g}\n' # Refer lines 20-21 for k in self.adc_library.data: data = np.multiply(self.adc_library.data[k][0:5], [1, 1e9, 1e6, 1, 1]) s = id_format_str.format(k, *data) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.extensions_library.data) != 0: - output_file.write("# Format of extension lists:\n") - output_file.write("# id type ref next_id\n") - output_file.write("# next_id of 0 terminates the list\n") - output_file.write( - "# Extension list is followed by extension specifications\n" - ) - output_file.write("[EXTENSIONS]\n") - id_format_str = "{:.0f} {:.0f} {:.0f} {:.0f}\n" # Refer lines 20-21 + output_file.write('# Format of extension lists:\n') + output_file.write('# id type ref next_id\n') + output_file.write('# next_id of 0 terminates the list\n') + output_file.write('# Extension list is followed by extension specifications\n') + output_file.write('[EXTENSIONS]\n') + id_format_str = '{:.0f} {:.0f} {:.0f} {:.0f}\n' # Refer lines 20-21 for k in self.extensions_library.data: s = id_format_str.format(k, *np.round(self.extensions_library.data[k])) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.trigger_library.data) != 0: - output_file.write( - "# Extension specification for digital output and input triggers:\n" - ) - output_file.write("# id type channel delay (us) duration (us)\n") - output_file.write( - f'extension TRIGGERS {self.get_extension_type_ID("TRIGGERS")}\n' - ) - id_format_str = "{:.0f} {:.0f} {:.0f} {:.0f} {:.0f}\n" # Refer lines 20-21 + output_file.write('# Extension specification for digital output and input triggers:\n') + output_file.write('# id type channel delay (us) duration (us)\n') + output_file.write(f'extension TRIGGERS {self.get_extension_type_ID("TRIGGERS")}\n') + id_format_str = '{:.0f} {:.0f} {:.0f} {:.0f} {:.0f}\n' # Refer lines 20-21 for k in self.trigger_library.data: - s = id_format_str.format( - k, *np.round(self.trigger_library.data[k] * np.array([1, 1, 1e6, 1e6])) - ) + s = id_format_str.format(k, *np.round(self.trigger_library.data[k] * np.array([1, 1, 1e6, 1e6]))) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.label_set_library.data) != 0: labels = get_supported_labels() - output_file.write("# Extension specification for setting labels:\n") - output_file.write("# id set labelstring\n") - tid = self.get_extension_type_ID("LABELSET") - output_file.write(f"extension LABELSET {tid}\n") - id_format_str = "{:.0f} {:.0f} {}\n" # Refer lines 20-21 + output_file.write('# Extension specification for setting labels:\n') + output_file.write('# id set labelstring\n') + tid = self.get_extension_type_ID('LABELSET') + output_file.write(f'extension LABELSET {tid}\n') + id_format_str = '{:.0f} {:.0f} {}\n' # Refer lines 20-21 for k in self.label_set_library.data: value = self.label_set_library.data[k][0] - label_id = labels[ - int(self.label_set_library.data[k][1]) - 1 - ] # label_id is +1 in add_block() + label_id = labels[int(self.label_set_library.data[k][1]) - 1] # label_id is +1 in add_block() s = id_format_str.format(k, value, label_id) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.label_inc_library.data) != 0: labels = get_supported_labels() - output_file.write("# Extension specification for setting labels:\n") - output_file.write("# id set labelstring\n") - tid = self.get_extension_type_ID("LABELINC") - output_file.write(f"extension LABELINC {tid}\n") - id_format_str = "{:.0f} {:.0f} {}\n" # See comment at the beginning of this method definition + output_file.write('# Extension specification for setting labels:\n') + output_file.write('# id set labelstring\n') + tid = self.get_extension_type_ID('LABELINC') + output_file.write(f'extension LABELINC {tid}\n') + id_format_str = '{:.0f} {:.0f} {}\n' # See comment at the beginning of this method definition for k in self.label_inc_library.data: value = self.label_inc_library.data[k][0] - label_id = labels[ - self.label_inc_library.data[k][1] - 1 - ] # label_id is +1 in add_block() + label_id = labels[self.label_inc_library.data[k][1] - 1] # label_id is +1 in add_block() s = id_format_str.format(k, value, label_id) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if len(self.shape_library.data) != 0: - output_file.write("# Sequence Shapes\n") - output_file.write("[SHAPES]\n\n") + output_file.write('# Sequence Shapes\n') + output_file.write('[SHAPES]\n\n') for k in self.shape_library.data: shape_data = self.shape_library.data[k] - s = "shape_id {:.0f}\n".format(k) + s = 'shape_id {:.0f}\n'.format(k) output_file.write(s) - s = "num_samples {:.0f}\n".format(shape_data[0]) + s = 'num_samples {:.0f}\n'.format(shape_data[0]) output_file.write(s) - s = ("{:.9g}\n" * len(shape_data[1:])).format(*shape_data[1:]) + s = ('{:.9g}\n' * len(shape_data[1:])).format(*shape_data[1:]) output_file.write(s) - output_file.write("\n") + output_file.write('\n') if create_signature: # Sign the file # Calculate digest - with open(file_name, "r") as output_file: + with open(file_name, 'r') as output_file: buffer = output_file.read() - md5 = hashlib.md5(buffer.encode("utf-8")).hexdigest() + md5 = hashlib.md5(buffer.encode('utf-8')).hexdigest() # Write signature - with open(file_name, "a") as output_file: - output_file.write("\n[SIGNATURE]\n") + with open(file_name, 'a') as output_file: + output_file.write('\n[SIGNATURE]\n') output_file.write( - "# This is the hash of the Pulseq file, calculated right before the [SIGNATURE] section was added\n" + '# This is the hash of the Pulseq file, calculated right before the [SIGNATURE] section was added\n' ) output_file.write( - "# It can be reproduced/verified with md5sum if the file trimmed to the position right above [SIGNATURE]\n" + '# It can be reproduced/verified with md5sum if the file trimmed to the position right above [SIGNATURE]\n' ) output_file.write( - "# The new line character preceding [SIGNATURE] BELONGS to the signature (and needs to be stripped away for " - "recalculating/verification)\n" + '# The new line character preceding [SIGNATURE] BELONGS to the signature (and needs to be stripped away for ' + 'recalculating/verification)\n' ) - output_file.write("Type md5\n") - output_file.write(f"Hash {md5}\n") + output_file.write('Type md5\n') + output_file.write(f'Hash {md5}\n') return md5 diff --git a/pypulseq/add_gradients.py b/pypulseq/add_gradients.py index 4bfe91c..c4e2aac 100644 --- a/pypulseq/add_gradients.py +++ b/pypulseq/add_gradients.py @@ -12,7 +12,7 @@ from pypulseq.opts import Opts from pypulseq.points_to_waveform import points_to_waveform from pypulseq.utils.cumsum import cumsum -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def add_gradients( @@ -49,7 +49,7 @@ def add_gradients( max_slew = system.max_slew if len(grads) == 0: - raise ValueError("No gradients specified") + raise ValueError('No gradients specified') if len(grads) == 1: # Trapezoids only require a shallow copy if grads[0].type == 'trap': @@ -65,18 +65,22 @@ def add_gradients( channel = grads[0].channel # Check if we have a set of traps with the same timing - if (all(g.type == 'trap' for g in grads) - and all(g.rise_time == grads[0].rise_time for g in grads) - and all(g.flat_time == grads[0].flat_time for g in grads) - and all(g.fall_time == grads[0].fall_time for g in grads) - and all(g.delay == grads[0].delay for g in grads)): - grad = make_trapezoid(grads[0].channel, - amplitude=sum(g.amplitude for g in grads)+eps, - rise_time=grads[0].rise_time, - flat_time=grads[0].flat_time, - fall_time=grads[0].fall_time, - delay=grads[0].delay, - system=system) + if ( + all(g.type == 'trap' for g in grads) + and all(g.rise_time == grads[0].rise_time for g in grads) + and all(g.flat_time == grads[0].flat_time for g in grads) + and all(g.fall_time == grads[0].fall_time for g in grads) + and all(g.delay == grads[0].delay for g in grads) + ): + grad = make_trapezoid( + grads[0].channel, + amplitude=sum(g.amplitude for g in grads) + eps, + rise_time=grads[0].rise_time, + flat_time=grads[0].flat_time, + fall_time=grads[0].fall_time, + delay=grads[0].delay, + system=system, + ) if trace_enabled(): grad.trace = trace() return grad @@ -85,13 +89,13 @@ def add_gradients( delays, firsts, lasts, durs, is_trap, is_arb = [], [], [], [], [], [] for ii in range(len(grads)): if grads[ii].channel != channel: - raise ValueError("Cannot add gradients on different channels.") + raise ValueError('Cannot add gradients on different channels.') delays.append(grads[ii].delay) firsts.append(grads[ii].first) lasts.append(grads[ii].last) durs.append(calc_duration(grads[ii])) - is_trap.append(grads[ii].type == "trap") + is_trap.append(grads[ii].type == 'trap') if is_trap[-1]: is_arb.append(False) else: @@ -104,10 +108,8 @@ def add_gradients( times = [] for ii in range(len(grads)): g = grads[ii] - if g.type == "trap": - times.extend( - cumsum(g.delay, g.rise_time, g.flat_time, g.fall_time) - ) + if g.type == 'trap': + times.extend(cumsum(g.delay, g.rise_time, g.flat_time, g.fall_time)) else: times.extend(g.delay + g.tt) @@ -116,16 +118,14 @@ def add_gradients( ieps = np.flatnonzero(dt < eps) if np.any(ieps): dtx = np.array([times[0], *dt]) - dtx[ieps] = ( - dtx[ieps] + dtx[ieps + 1] - ) # Assumes that no more than two too similar values can occur + dtx[ieps] = dtx[ieps] + dtx[ieps + 1] # Assumes that no more than two too similar values can occur dtx = np.delete(dtx, ieps + 1) times = np.cumsum(dtx) amplitudes = np.zeros_like(times) for ii in range(len(grads)): g = grads[ii] - if g.type == "trap": + if g.type == 'trap': if g.flat_time > 0: # Trapezoid or triangle tt = list(cumsum(g.delay, g.rise_time, g.flat_time, g.fall_time)) waveform = [0, g.amplitude, g.amplitude, 0] @@ -151,9 +151,7 @@ def add_gradients( amplitudes += np.interp(xp=tt, fp=waveform, x=times, left=0, right=0) - grad = make_extended_trapezoid( - channel=channel, amplitudes=amplitudes, times=times, system=system - ) + grad = make_extended_trapezoid(channel=channel, amplitudes=amplitudes, times=times, system=system) if trace_enabled(): grad.trace = trace() @@ -166,11 +164,11 @@ def add_gradients( durs = np.array(durs) # Convert everything to a regularly-sampled waveform - waveforms = dict() + waveforms = {} max_length = 0 for ii in range(len(grads)): g = grads[ii] - if g.type == "grad": + if g.type == 'grad': if is_arb[ii]: waveforms[ii] = g.waveform else: @@ -179,18 +177,14 @@ def add_gradients( times=g.tt, grad_raster_time=system.grad_raster_time, ) - elif g.type == "trap": + elif g.type == 'trap': if g.flat_time > 0: # Triangle or trapezoid times = np.array( [ g.delay - common_delay, g.delay - common_delay + g.rise_time, g.delay - common_delay + g.rise_time + g.flat_time, - g.delay - - common_delay - + g.rise_time - + g.flat_time - + g.fall_time, + g.delay - common_delay + g.rise_time + g.flat_time + g.fall_time, ] ) amplitudes = np.array([0, g.amplitude, g.amplitude, 0]) @@ -209,12 +203,14 @@ def add_gradients( grad_raster_time=system.grad_raster_time, ) else: - raise ValueError("Unknown gradient type") + raise ValueError('Unknown gradient type') if g.delay - common_delay > 0: # Stop for numpy.arange is not g.delay - common_delay - system.grad_raster_time like in Matlab # so as to include the endpoint - waveforms[ii] = np.concatenate((np.zeros(round((g.delay-common_delay)/system.grad_raster_time)), waveforms[ii])) + waveforms[ii] = np.concatenate( + (np.zeros(round((g.delay - common_delay) / system.grad_raster_time)), waveforms[ii]) + ) num_points = len(waveforms[ii]) max_length = max(num_points, max_length) diff --git a/pypulseq/add_ramps.py b/pypulseq/add_ramps.py index d25ab44..c1c3c8d 100644 --- a/pypulseq/add_ramps.py +++ b/pypulseq/add_ramps.py @@ -1,6 +1,6 @@ from copy import copy from types import SimpleNamespace -from typing import Union, List +from typing import List, Union import numpy as np @@ -12,7 +12,7 @@ def add_ramps( k: Union[list, np.ndarray, tuple], max_grad: int = 0, max_slew: int = 0, - rf: SimpleNamespace = None, + rf: Union[SimpleNamespace, None] = None, system: Union[Opts, None] = None, ) -> List[np.ndarray]: """ @@ -47,11 +47,9 @@ def add_ramps( """ if system is None: system = Opts.default - + if not isinstance(k, (list, np.ndarray, tuple)): - raise ValueError( - f"k has to be one of list, np.ndarray, tuple. Passed: {type(k)}" - ) + raise ValueError(f'k has to be one of list, np.ndarray, tuple. Passed: {type(k)}') k_arg = copy(k) if max_grad > 0: @@ -64,14 +62,12 @@ def add_ramps( k = np.vstack(k) num_channels = k.shape[0] - k = np.vstack( - (k, np.zeros((3 - num_channels, k.shape[1]))) - ) # Pad with zeros if needed + k = np.vstack((k, np.zeros((3 - num_channels, k.shape[1])))) # Pad with zeros if needed k_up, ok1 = calc_ramp(k0=np.zeros((3, 2)), k_end=k[:, :2], system=system) k_down, ok2 = calc_ramp(k0=k[:, -2:], k_end=np.zeros((3, 2)), system=system) if not (ok1 and ok2): - raise RuntimeError("Failed to calculate gradient ramps") + raise RuntimeError('Failed to calculate gradient ramps') # Add start and end points to ramps k_up = np.hstack((np.zeros((3, 2)), k_up)) @@ -88,10 +84,6 @@ def add_ramps( result.append(k[i]) if rf is not None: - result.append( - np.concatenate( - (np.zeros(k_up.shape[1] * 10), rf, np.zeros(k_down.shape[1] * 10)) - ) - ) + result.append(np.concatenate((np.zeros(k_up.shape[1] * 10), rf, np.zeros(k_down.shape[1] * 10)))) return result diff --git a/pypulseq/align.py b/pypulseq/align.py index 0dcbc5e..66d1d97 100644 --- a/pypulseq/align.py +++ b/pypulseq/align.py @@ -7,9 +7,7 @@ from pypulseq.calc_duration import calc_duration -def align( - **kwargs: Union[SimpleNamespace, List[SimpleNamespace]] -) -> List[SimpleNamespace]: +def align(**kwargs: Union[SimpleNamespace, List[SimpleNamespace]]) -> List[SimpleNamespace]: """ Sets delays of the objects within the block to achieve the desired alignment of the objects in the block. Aligns objects as per specified alignment options by setting delays of the pulse sequence events within the block. All @@ -40,13 +38,11 @@ def align( """ alignment_specs = list(kwargs.keys()) if not isinstance(alignment_specs[0], str): - raise ValueError( - f"First parameter must be of type str. Passed: {type(alignment_specs[0])}" - ) + raise ValueError(f'First parameter must be of type str. Passed: {type(alignment_specs[0])}') - alignment_options = ["left", "center", "right"] + alignment_options = ['left', 'center', 'right'] if np.any([align_opt not in alignment_options for align_opt in alignment_specs]): - raise ValueError("Invalid alignment spec.") + raise ValueError('Invalid alignment spec.') alignments = [] objects = [] @@ -75,7 +71,7 @@ def align( objects[i].delay = dur - calc_duration(objects[i]) + objects[i].delay if objects[i].delay < 0: raise ValueError( - "align() attempts to set a negative delay, probably some RF pulses ignore rf_ringdown_time" + 'align() attempts to set a negative delay, probably some RF pulses ignore rf_ringdown_time' ) return objects diff --git a/pypulseq/block_to_events.py b/pypulseq/block_to_events.py index fee9f0b..82656ff 100644 --- a/pypulseq/block_to_events.py +++ b/pypulseq/block_to_events.py @@ -18,13 +18,9 @@ def block_to_events(*args: SimpleNamespace) -> Tuple[SimpleNamespace, ...]: """ if len(args) == 1 and hasattr(args[0], 'rf'): events = list(vars(args[0]).values()) # Get all attrs - events = list( - filter(lambda filter_none: filter_none is not None, events) - ) # Filter None attributes - events = __get_label_events_if_any( - *events - ) # Flatten label events from dict datatype - + events = list(filter(lambda filter_none: filter_none is not None, events)) # Filter None attributes + events = __get_label_events_if_any(*events) # Flatten label events from dict datatype + else: # args is a tuple of events return args diff --git a/pypulseq/calc_duration.py b/pypulseq/calc_duration.py index 6ccddb6..b3412b0 100755 --- a/pypulseq/calc_duration.py +++ b/pypulseq/calc_duration.py @@ -1,7 +1,5 @@ from types import SimpleNamespace -import numpy as np - from pypulseq.block_to_events import block_to_events @@ -36,29 +34,25 @@ def calc_duration(*args: SimpleNamespace) -> float: continue if not isinstance(event, (dict, SimpleNamespace)): - raise TypeError( - "input(s) should be of type SimpleNamespace or a dict() in case of LABELINC or LABELSET" - ) + raise TypeError('input(s) should be of type SimpleNamespace or a dict() in case of LABELINC or LABELSET') - if event.type == "delay": + if event.type == 'delay': duration = max(duration, event.delay) - elif event.type == "rf": - duration = max( - duration, event.delay + event.shape_dur + event.ringdown_time - ) - elif event.type == "grad": + elif event.type == 'rf': + duration = max(duration, event.delay + event.shape_dur + event.ringdown_time) + elif event.type == 'grad': duration = max(duration, event.delay + event.shape_dur) - elif event.type == "adc": + elif event.type == 'adc': duration = max( - duration, - event.delay + event.num_samples * event.dwell + event.dead_time, + duration, + event.delay + event.num_samples * event.dwell + event.dead_time, ) - elif event.type == "trap": + elif event.type == 'trap': duration = max( - duration, - event.delay + event.rise_time + event.flat_time + event.fall_time, + duration, + event.delay + event.rise_time + event.flat_time + event.fall_time, ) - elif event.type == "output" or event.type == "trigger": + elif event.type == 'output' or event.type == 'trigger': duration = max(duration, event.delay + event.duration) return duration diff --git a/pypulseq/calc_ramp.py b/pypulseq/calc_ramp.py index 38ab236..fd3342e 100644 --- a/pypulseq/calc_ramp.py +++ b/pypulseq/calc_ramp.py @@ -8,9 +8,9 @@ def calc_ramp( k0: np.ndarray, k_end: np.ndarray, - max_grad: np.ndarray = np.zeros(0), + max_grad: Union[np.ndarray, None] = None, max_points: int = 500, - max_slew: np.ndarray = np.zeros(0), + max_slew: Union[np.ndarray, None] = None, system: Union[Opts, None] = None, ) -> Tuple[np.ndarray, bool]: """ @@ -45,13 +45,17 @@ def calc_ramp( if system is None: system = Opts.default + if max_grad is None: + max_grad = np.zeros(0) + + if max_slew is None: + max_slew = np.zeros(0) + def __inside_limits(grad, slew): if mode == 0: grad2 = np.sum(np.square(grad), axis=1) slew2 = np.sum(np.square(slew), axis=1) - ok = np.all(np.max(grad2) <= np.square(max_grad)) and np.all( - np.max(slew2) <= np.square(max_slew) - ) + ok = np.all(np.max(grad2) <= np.square(max_grad)) and np.all(np.max(slew2) <= np.square(max_slew)) else: ok = (np.sum(np.max(np.abs(grad), axis=1) <= max_grad) == 3) and ( np.sum(np.max(np.abs(slew), axis=1) <= max_slew) == 3 @@ -109,9 +113,7 @@ def __joinleft0(k0, k_end, use_points, G0, G_end): kglsl = kglsl + h * hdirection / np.linalg.norm(hdirection) k_left = kglsl - success, k = __joinright0( - k_left, k_end, (k_left - k0) / grad_raster, G_end, use_points - 1 - ) + success, k = __joinright0(k_left, k_end, (k_left - k0) / grad_raster, G_end, use_points - 1) if len(k) != 0: if len(k.shape) == 1: k = k.reshape((len(k), 1)) @@ -163,7 +165,7 @@ def __joinleft1(k0, k_end, use_points, G0, G_end): elif okSgl[ii] == 1: k_left[ii] = kgl[ii] else: - print("Unknown error") + print('Unknown error') success, k = __joinright1( k0=k_left, @@ -227,9 +229,7 @@ def __joinright0(k0, k_end, use_points, G0, G_end): c = np.linalg.norm(dkprol) c1 = np.divide(np.square(a) - np.square(b) + np.square(c), (2 * c)) h = np.sqrt(np.square(a) - np.square(c1)) - kglsl = k_end + np.multiply( - c1, np.divide(dkprol, np.linalg.norm(dkprol)) - ) + kglsl = k_end + np.multiply(c1, np.divide(dkprol, np.linalg.norm(dkprol))) projondkprol = (kgl * dkprol.T) * (dkprol / np.linalg.norm(dkprol)) hdirection = kgl - projondkprol kglsl = kglsl + h * hdirection / np.linalg.norm(hdirection) @@ -293,7 +293,7 @@ def __joinright1(k0, k_end, use_points, G0, G_end): elif okSgl[ii] == 1: k_right[ii] = kgl[ii] else: - print("Unknown error") + print('Unknown error') success, k = __joinleft1( k0=k0, @@ -328,7 +328,7 @@ def __joinright1(k0, k_end, use_points, G0, G_end): elif len(max_grad) == 3 and len(max_slew) == 3: mode = 1 else: - raise ValueError("Input value max grad or max slew in invalid format.") + raise ValueError('Input value max grad or max slew in invalid format.') G0 = (k0[:, 1] - k0[:, 0]) / grad_raster G_end = (k_end[:, 1] - k_end[:, 0]) / grad_raster @@ -343,15 +343,11 @@ def __joinright1(k0, k_end, use_points, G0, G_end): if mode == 0: if np.linalg.norm(G0) > max_grad or np.linalg.norm(G_end) > max_grad: break - success, k_out = __joinleft0( - k0=k0, k_end=k_end, G0=G0, G_end=G_end, use_points=use_points - ) + success, k_out = __joinleft0(k0=k0, k_end=k_end, G0=G0, G_end=G_end, use_points=use_points) else: if abs(G0) > abs(max_grad) or abs(G_end) > abs(max_grad): break - success, k_out = __joinleft1( - k0=k0, k_end=k_end, use_points=use_points, G0=G0, G_end=G_end - ) + success, k_out = __joinleft1(k0=k0, k_end=k_end, use_points=use_points, G0=G0, G_end=G_end) use_points += 1 return k_out, success diff --git a/pypulseq/calc_rf_bandwidth.py b/pypulseq/calc_rf_bandwidth.py index b07c757..0a0a7bf 100644 --- a/pypulseq/calc_rf_bandwidth.py +++ b/pypulseq/calc_rf_bandwidth.py @@ -1,8 +1,8 @@ +import math from types import SimpleNamespace -from typing import Union, Tuple +from typing import Tuple, Union import numpy as np -import math from pypulseq.calc_rf_center import calc_rf_center @@ -27,6 +27,7 @@ def calc_rf_bandwidth( Boolean flag to indicate if frequency axis of RF pulse will be returned. return_spectrum : bool, default=False Boolean flag to indicate if spectrum of RF pulse will be returned. + Returns ------- bw : float diff --git a/pypulseq/check_timing.py b/pypulseq/check_timing.py index cb3fac7..6b76d41 100644 --- a/pypulseq/check_timing.py +++ b/pypulseq/check_timing.py @@ -1,19 +1,18 @@ from types import SimpleNamespace -from typing import Tuple, List, Any +from typing import Any, List, Tuple -from pypulseq import eps, Sequence +from pypulseq import Sequence, eps from pypulseq.calc_duration import calc_duration from pypulseq.utils.tracing import format_trace - error_messages = { - "RASTER": "{value*multiplier:.2f} {unit} does not align to {raster} (Nearest valid value: {value_rounded*multiplier:.0f} {unit}, error: {error*multiplier:.2f} {unit})", - "ADC_DEAD_TIME": "ADC delay is smaller than ADC dead time ({value*multiplier:.2f} {unit} < {dead_time*multiplier:.0f} {unit})", - "POST_ADC_DEAD_TIME": "Post-ADC dead time exceeds block duration ({value*multiplier:.2f} {unit} + {dead_time*multiplier:.0f} {unit} > {duration*multiplier} {unit})", - "BLOCK_DURATION_MISMATCH": "Inconsistency between the stored block duration ({duration*multiplier:.2f} {unit}) and the content of the block ({value*multiplier:.2f} {unit})", - "RF_DEAD_TIME": "Delay of {value*multiplier:.2f} {unit} is smaller than the RF dead time {dead_time*multiplier:.0f} {unit}", - "RF_RINGDOWN_TIME": "Time between the end of the RF pulse at {value*multiplier:.2f} {unit} and the end of the block at {duration * multiplier:.2f} {unit} is shorter than rf_ringdown_time ({ringdown_time*multiplier:.0f} {unit})", - "NEGATIVE_DELAY": "Delay is negative {value*multiplier:.2f} {unit}", + 'RASTER': '{value*multiplier:.2f} {unit} does not align to {raster} (Nearest valid value: {value_rounded*multiplier:.0f} {unit}, error: {error*multiplier:.2f} {unit})', + 'ADC_DEAD_TIME': 'ADC delay is smaller than ADC dead time ({value*multiplier:.2f} {unit} < {dead_time*multiplier:.0f} {unit})', + 'POST_ADC_DEAD_TIME': 'Post-ADC dead time exceeds block duration ({value*multiplier:.2f} {unit} + {dead_time*multiplier:.0f} {unit} > {duration*multiplier} {unit})', + 'BLOCK_DURATION_MISMATCH': 'Inconsistency between the stored block duration ({duration*multiplier:.2f} {unit}) and the content of the block ({value*multiplier:.2f} {unit})', + 'RF_DEAD_TIME': 'Delay of {value*multiplier:.2f} {unit} is smaller than the RF dead time {dead_time*multiplier:.0f} {unit}', + 'RF_RINGDOWN_TIME': 'Time between the end of the RF pulse at {value*multiplier:.2f} {unit} and the end of the block at {duration * multiplier:.2f} {unit} is shorter than rf_ringdown_time ({ringdown_time*multiplier:.0f} {unit})', + 'NEGATIVE_DELAY': 'Delay is negative {value*multiplier:.2f} {unit}', } @@ -38,7 +37,7 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): value_rounded=c_rounded * b, error=(a - c_rounded * b), raster=raster, - error_type="RASTER", + error_type='RASTER', ) ) @@ -49,16 +48,16 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): # Check block duration duration = calc_duration(block) div_check( - duration, seq.system.block_duration_raster, event="block", field="duration", raster="block_duration_raster" + duration, seq.system.block_duration_raster, event='block', field='duration', raster='block_duration_raster' ) if abs(duration - seq.block_durations[block_counter]) > eps: error_report.append( SimpleNamespace( block=block_counter, - event="block", - field="duration", - error_type="BLOCK_DURATION_MISMATCH", + event='block', + field='duration', + error_type='BLOCK_DURATION_MISMATCH', value=duration, duration=seq.block_durations[block_counter], ) @@ -70,47 +69,47 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): if e is None or isinstance(e, (float, int)): # Special handling for block_duration continue elif not isinstance(e, (dict, SimpleNamespace)): - raise ValueError("Wrong data type of variable arguments, list[SimpleNamespace] expected.") + raise ValueError('Wrong data type of variable arguments, list[SimpleNamespace] expected.') if isinstance(e, list) and len(e) > 1: # For now this is only the case for arrays of extensions, but we cannot actually check extensions anyway... continue - if hasattr(e, "type") and e.type == "adc": + if hasattr(e, 'type') and e.type == 'adc': raster = seq.system.adc_raster_time - raster_str = "adc_raster_time" - elif hasattr(e, "type") and e.type == "rf": + raster_str = 'adc_raster_time' + elif hasattr(e, 'type') and e.type == 'rf': raster = seq.system.rf_raster_time - raster_str = "rf_raster_time" + raster_str = 'rf_raster_time' else: raster = seq.system.grad_raster_time - raster_str = "grad_raster_time" + raster_str = 'grad_raster_time' - if hasattr(e, "delay"): + if hasattr(e, 'delay'): if e.delay < -eps: error_report.append( SimpleNamespace( - block=block_counter, event=event, field="delay", error_type="NEGATIVE_DELAY", value=e.delay + block=block_counter, event=event, field='delay', error_type='NEGATIVE_DELAY', value=e.delay ) ) - div_check(e.delay, raster, event=event, field="delay", raster=raster_str) + div_check(e.delay, raster, event=event, field='delay', raster=raster_str) - if hasattr(e, "duration"): - div_check(e.duration, raster, event=event, field="duration", raster=raster_str) + if hasattr(e, 'duration'): + div_check(e.duration, raster, event=event, field='duration', raster=raster_str) - if hasattr(e, "dwell"): - div_check(e.dwell, seq.system.adc_raster_time, event=event, field="dwell", raster="adc_raster_time") + if hasattr(e, 'dwell'): + div_check(e.dwell, seq.system.adc_raster_time, event=event, field='dwell', raster='adc_raster_time') - if hasattr(e, "type") and e.type == "trap": + if hasattr(e, 'type') and e.type == 'trap': div_check( - e.rise_time, seq.system.grad_raster_time, event=event, field="rise_time", raster="grad_raster_time" + e.rise_time, seq.system.grad_raster_time, event=event, field='rise_time', raster='grad_raster_time' ) div_check( - e.flat_time, seq.system.grad_raster_time, event=event, field="flat_time", raster="grad_raster_time" + e.flat_time, seq.system.grad_raster_time, event=event, field='flat_time', raster='grad_raster_time' ) div_check( - e.fall_time, seq.system.grad_raster_time, event=event, field="fall_time", raster="grad_raster_time" + e.fall_time, seq.system.grad_raster_time, event=event, field='fall_time', raster='grad_raster_time' ) # Check RF dead times @@ -119,9 +118,9 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): error_report.append( SimpleNamespace( block=block_counter, - event="rf", - field="delay", - error_type="RF_DEAD_TIME", + event='rf', + field='delay', + error_type='RF_DEAD_TIME', value=block.rf.delay, dead_time=block.rf.dead_time, ) @@ -131,9 +130,9 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): error_report.append( SimpleNamespace( block=block_counter, - event="rf", - field="duration", - error_type="RF_RINGDOWN_TIME", + event='rf', + field='duration', + error_type='RF_RINGDOWN_TIME', value=block.rf.delay + block.rf.t[-1], duration=duration, ringdown_time=block.rf.ringdown_time, @@ -146,9 +145,9 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): error_report.append( SimpleNamespace( block=block_counter, - event="adc", - field="delay", - error_type="ADC_DEAD_TIME", + event='adc', + field='delay', + error_type='ADC_DEAD_TIME', value=block.adc.delay, dead_time=seq.system.adc_dead_time, ) @@ -160,9 +159,9 @@ def div_check(a: float, b: float, event: str, field: str, raster: str): error_report.append( SimpleNamespace( block=block_counter, - event="adc", - field="duration", - error_type="POST_ADC_DEAD_TIME", + event='adc', + field='duration', + error_type='POST_ADC_DEAD_TIME', value=block.adc.delay + block.adc.num_samples * block.adc.dwell, duration=duration, dead_time=seq.system.adc_dead_time, @@ -197,7 +196,7 @@ def indent_string(x: str, n: int = 2) -> str: """ Adds indentations (`n` spaces) to every line in a string """ - return "\n".join(" " * n + y for y in x.splitlines()) + return '\n'.join(' ' * n + y for y in x.splitlines()) def print_error_report( @@ -214,41 +213,41 @@ def print_error_report( for e in error_report[:max_errors]: if e.block != current_block: - print(f"Block {e.block}:") + print(f'Block {e.block}:') current_block = e.block trace = seq.block_trace.get(current_block, None) - if hasattr(trace, "block"): + if hasattr(trace, 'block'): print( - ("\x1b[38;5;8m" if colored else "") - + "Block created here:\n" + ('\x1b[38;5;8m' if colored else '') + + 'Block created here:\n' + format_trace(trace.block) - + ("\x1b[0m" if colored else "") + + ('\x1b[0m' if colored else '') ) - unit = "us" + unit = 'us' multiplier = 1e6 - if e.field == "dwell": - unit = "ns" + if e.field == 'dwell': + unit = 'ns' multiplier = 1e9 error_message = format_string(error_messages[e.error_type], **e.__dict__, unit=unit, multiplier=multiplier) print( - f"- {e.event}.{e.field}: " - + ("\x1b[38;5;9m" if colored else "") + f'- {e.event}.{e.field}: ' + + ('\x1b[38;5;9m' if colored else '') + error_message - + ("\x1b[0m" if colored else "") + + ('\x1b[0m' if colored else '') ) - if hasattr(trace, e.event) and e.event != "block": + if hasattr(trace, e.event) and e.event != 'block': print( - ("\x1b[38;5;8m" if colored else "") - + f" `{e.event}` created here:\n" + ('\x1b[38;5;8m' if colored else '') + + f' `{e.event}` created here:\n' + format_trace(getattr(trace, e.event), indent=2) - + ("\x1b[0m" if colored else "") + + ('\x1b[0m' if colored else '') ) if len(error_report) > max_errors: blocks = [e.block for e in error_report[max_errors:]] - print(f"--- {len(error_report) - max_errors} more errors in blocks {min(blocks)} to {max(blocks)} hidden ---") + print(f'--- {len(error_report) - max_errors} more errors in blocks {min(blocks)} to {max(blocks)} hidden ---') diff --git a/pypulseq/compress_shape.py b/pypulseq/compress_shape.py index 38d7a31..0a4d640 100755 --- a/pypulseq/compress_shape.py +++ b/pypulseq/compress_shape.py @@ -3,9 +3,7 @@ import numpy as np -def compress_shape( - decompressed_shape: np.ndarray, force_compression: bool = False -) -> SimpleNamespace: +def compress_shape(decompressed_shape: np.ndarray, force_compression: bool = False) -> SimpleNamespace: """ Compress a gradient or pulse shape waveform using a run-length compression scheme on the derivative. This strategy encodes constant and linear waveforms with very few samples. A structure is returned with the fields: @@ -27,11 +25,9 @@ def compress_shape( A `SimpleNamespace` object containing the number of samples and the compressed data. """ if np.any(~np.isfinite(decompressed_shape)): - raise ValueError("compress_shape() received infinite samples.") + raise ValueError('compress_shape() received infinite samples.') - if ( - not force_compression and len(decompressed_shape) <= 4 - ): # Avoid compressing very short shapes + if not force_compression and len(decompressed_shape) <= 4: # Avoid compressing very short shapes compressed_shape = SimpleNamespace() compressed_shape.num_samples = len(decompressed_shape) compressed_shape.data = decompressed_shape @@ -40,23 +36,21 @@ def compress_shape( # Single precision floating point has ~7.25 decimal places quant_factor = 1e-7 decompressed_shape_scaled = decompressed_shape / quant_factor - datq = np.round( - np.concatenate((decompressed_shape_scaled[[0]], np.diff(decompressed_shape_scaled))) - ) + datq = np.round(np.concatenate((decompressed_shape_scaled[[0]], np.diff(decompressed_shape_scaled)))) qerr = decompressed_shape_scaled - np.cumsum(datq) qcor = np.concatenate(([0], np.diff(np.round(qerr)))) datd = datq + qcor # RLE of datd - starts = np.concatenate(([0], np.flatnonzero(datd[1:] != datd[:-1])+1)) + starts = np.concatenate(([0], np.flatnonzero(datd[1:] != datd[:-1]) + 1)) lengths = np.diff(np.concatenate((starts, [len(datd)]))) values = datd[starts] * quant_factor - + # Repeat values of any run-length>1 three times: (value, value, length) - rl_gt1 = lengths>1 - repeats = 1 + rl_gt1*2 + rl_gt1 = lengths > 1 + repeats = 1 + rl_gt1 * 2 v = np.repeat(values, repeats) - + # Calculate indices of length elements and insert length values inds = np.cumsum(repeats) - 1 v[inds[rl_gt1]] = lengths[rl_gt1] - 2 diff --git a/pypulseq/convert.py b/pypulseq/convert.py index b83555b..e932433 100755 --- a/pypulseq/convert.py +++ b/pypulseq/convert.py @@ -34,8 +34,8 @@ def convert( If an invalid `from_unit` is passed. Must be one of 'Hz/m', 'mT/m', or 'rad/ms/mm'. If an invalid `to_unit` is passed. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms'. """ - valid_grad_units = ["Hz/m", "mT/m", "rad/ms/mm"] - valid_slew_units = ["Hz/m/s", "mT/m/ms", "T/m/s", "rad/ms/mm/ms"] + valid_grad_units = ['Hz/m', 'mT/m', 'rad/ms/mm'] + valid_slew_units = ['Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms'] valid_units = valid_grad_units + valid_slew_units if from_unit not in valid_units: @@ -44,13 +44,13 @@ def convert( "or must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms' for slew rate." ) - if to_unit != "" and to_unit not in valid_units: + if to_unit != '' and to_unit not in valid_units: raise ValueError( "Invalid to_unit. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms' for gradients;" "or must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms' for slew rate.." ) - if to_unit == "": + if to_unit == '': if from_unit in valid_grad_units: to_unit = valid_grad_units[0] elif from_unit in valid_slew_units: @@ -58,34 +58,34 @@ def convert( # Convert to standard units # Grad units - if from_unit == "Hz/m": + if from_unit == 'Hz/m': standard = from_value - elif from_unit == "mT/m": + elif from_unit == 'mT/m': standard = from_value * 1e-3 * gamma - elif from_unit == "rad/ms/mm": + elif from_unit == 'rad/ms/mm': standard = from_value * 1e6 / (2 * np.pi) # Slew units - elif from_unit == "Hz/m/s": + elif from_unit == 'Hz/m/s': standard = from_value - elif from_unit == "mT/m/ms" or from_unit == "T/m/s": + elif from_unit == 'mT/m/ms' or from_unit == 'T/m/s': standard = from_value * gamma - elif from_unit == "rad/ms/mm/ms": + elif from_unit == 'rad/ms/mm/ms': standard = from_value * 1e9 / (2 * np.pi) # Convert from standard units # Grad units - if to_unit == "Hz/m": + if to_unit == 'Hz/m': out = standard - elif to_unit == "mT/m": + elif to_unit == 'mT/m': out = 1e3 * standard / gamma - elif to_unit == "rad/ms/mm": + elif to_unit == 'rad/ms/mm': out = standard * 2 * np.pi * 1e-6 # Slew units - elif to_unit == "Hz/m/s": + elif to_unit == 'Hz/m/s': out = standard - elif to_unit == "mT/m/ms" or to_unit == "T/m/s": + elif to_unit == 'mT/m/ms' or to_unit == 'T/m/s': out = standard / gamma - elif to_unit == "rad/ms/mm/ms": + elif to_unit == 'rad/ms/mm/ms': out = standard * 2 * np.pi * 1e-9 return out diff --git a/pypulseq/decompress_shape.py b/pypulseq/decompress_shape.py index 2f012cf..f76565f 100755 --- a/pypulseq/decompress_shape.py +++ b/pypulseq/decompress_shape.py @@ -3,9 +3,7 @@ import numpy as np -def decompress_shape( - compressed_shape: SimpleNamespace, force_decompression: bool = False -) -> np.ndarray: +def decompress_shape(compressed_shape: SimpleNamespace, force_decompression: bool = False) -> np.ndarray: """ Decompress a gradient or pulse shape compressed with a run-length compression scheme on the derivative. The given shape is structure with the following fields: @@ -52,9 +50,7 @@ def decompress_shape( if current_unpack_samples < 0: # Rejects false positives continue elif current_unpack_samples > 0: # We have an unpacked block to copy - decompressed_shape[ - count_unpack : count_unpack + current_unpack_samples - ] = data_pack[count_pack:next_pack] + decompressed_shape[count_unpack : count_unpack + current_unpack_samples] = data_pack[count_pack:next_pack] count_pack += current_unpack_samples count_unpack += current_unpack_samples diff --git a/pypulseq/event_lib.py b/pypulseq/event_lib.py index 90eab3a..5b19d86 100755 --- a/pypulseq/event_lib.py +++ b/pypulseq/event_lib.py @@ -6,15 +6,16 @@ except ImportError: from typing import TypeVar - Self = TypeVar("Self", bound="EventLibrary") + Self = TypeVar('Self', bound='EventLibrary') import math + import numpy as np class EventLibrary: """ - Defines an event library ot maintain a list of events. Provides methods to insert new data and find existing data. + Defines an event library to maintain a list of events. Provides methods to insert new data and find existing data. Sequence Properties: - data - A struct array with field 'array' to store data of varying lengths, remaining compatible with codegen. @@ -37,16 +38,16 @@ class EventLibrary: """ def __init__(self, numpy_data=False): - self.data = dict() - self.type = dict() - self.keymap = dict() + self.data = {} + self.type = {} + self.keymap = {} self.next_free_ID = 1 self.numpy_data = numpy_data def __str__(self) -> str: - s = "EventLibrary:" - s += "\ndata: " + str(len(self.data)) - s += "\ntype: " + str(len(self.type)) + s = 'EventLibrary:' + s += '\ndata: ' + str(len(self.data)) + s += '\ntype: ' + str(len(self.type)) return s def find(self, new_data: np.ndarray) -> Tuple[int, bool]: @@ -101,7 +102,6 @@ def find_or_insert(self, new_data: np.ndarray, data_type: str = str()) -> Tuple[ found : bool If `new_data` was found in the event library or not. """ - if self.numpy_data: new_data = np.asarray(new_data) new_data.flags.writeable = False @@ -183,9 +183,9 @@ def get(self, key_id: int) -> dict: dict """ return { - "key": key_id, - "data": self.data[key_id], - "type": self.type[key_id], + 'key': key_id, + 'data': self.data[key_id], + 'type': self.type[key_id], } def out(self, key_id: int) -> SimpleNamespace: @@ -212,7 +212,7 @@ def out(self, key_id: int) -> SimpleNamespace: def update( self, key_id: int, - old_data: Union[np.ndarray, None], + old_data: Union[np.ndarray, None], # noqa: ARG002 new_data: np.ndarray, data_type: str = str(), ): @@ -224,9 +224,8 @@ def update( new_data : numpy.ndarray data_type : str, default=str() """ - if key_id in self.data: - if self.data[key_id] in self.keymap: - del self.keymap[self.data[key_id]] + if key_id in self.data and self.data[key_id] in self.keymap: + del self.keymap[self.data[key_id]] self.insert(key_id, new_data, data_type) @@ -276,7 +275,7 @@ def remove_duplicates(self, digits: Union[int, Tuple[int]]) -> Tuple[Self, dict] def round_data(data: Tuple[float], digits: Tuple[int]) -> Tuple[float]: """ Round the data tuple to a specified number of significant digits, - specified by `digits`. Rounding behaviour is similar to the {.Ng} + specified by `digits`. Rounding behavior is similar to the {.Ng} format specifier if N > 0, and similar to {.0f} otherwise. """ return tuple( @@ -287,7 +286,7 @@ def round_data(data: Tuple[float], digits: Tuple[int]) -> Tuple[float]: def round_data_numpy(data: np.ndarray, digits: int) -> np.ndarray: """ Round the data array to a specified number of significant digits, - specified by `digits`. Rounding behaviour is similar to the {.Ng} + specified by `digits`. Rounding behavior is similar to the {.Ng} format specifier if N > 0, and similar to {.0f} otherwise. """ mags = 10 ** (digits - (np.ceil(np.log10(abs(data) + 1e-12))) if digits > 0 else -digits) diff --git a/pypulseq/make_adc.py b/pypulseq/make_adc.py index ab423dd..6dab5bf 100755 --- a/pypulseq/make_adc.py +++ b/pypulseq/make_adc.py @@ -1,13 +1,11 @@ +import itertools +from math import ceil, floor, gcd, isclose, prod from types import SimpleNamespace -from typing import Union +from typing import List, Optional, Tuple, Union from warnings import warn -from typing import Optional, Tuple, List -from math import isclose, floor, ceil, gcd, prod -import itertools -import numpy as np from pypulseq.opts import Opts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_adc( @@ -27,7 +25,7 @@ def make_adc( num_samples: int Number of readout samples. system : Opts, default=Opts() - System limits. Default is a system limits object initialised to default values. + System limits. Default is a system limits object initialized to default values. dwell : float, default=0 ADC dead time in seconds (s) after sampling. duration : float, default=0 @@ -53,7 +51,7 @@ def make_adc( system = Opts.default adc = SimpleNamespace() - adc.type = "adc" + adc.type = 'adc' adc.num_samples = num_samples adc.dwell = dwell adc.delay = delay @@ -62,7 +60,7 @@ def make_adc( adc.dead_time = system.adc_dead_time if (dwell == 0 and duration == 0) or (dwell > 0 and duration > 0): - raise ValueError("Either dwell or duration must be defined") + raise ValueError('Either dwell or duration must be defined') if duration > 0: adc.dwell = duration / num_samples @@ -72,7 +70,7 @@ def make_adc( if adc.dead_time > adc.delay: warn( - f"Specified ADC delay {adc.delay*1e6:.2f} us is less than the dead time {adc.dead_time*1e6:.0f} us. Delay was increased to the dead time.", + f'Specified ADC delay {adc.delay*1e6:.2f} us is less than the dead time {adc.dead_time*1e6:.0f} us. Delay was increased to the dead time.', stacklevel=2, ) adc.delay = adc.dead_time @@ -84,7 +82,7 @@ def make_adc( def calc_adc_segments( - num_samples: int, dwell: float, system: Optional[Opts] = None, mode: str = "lengthen" + num_samples: int, dwell: float, system: Optional[Opts] = None, mode: str = 'lengthen' ) -> Tuple[int, int]: """Calculate splitting of the ADC in segments with equal samples. @@ -95,7 +93,7 @@ def calc_adc_segments( dwell : float Dwell time of the ADC in [s] system : Optional[Opts], default=None - System limits. Default is a system limits object initialised to default values. + System limits. Default is a system limits object initialized to default values. mode : str, default='lengthen' The total number of samples can either be shortened or lengthened to match the constraints. @@ -124,9 +122,9 @@ def calc_adc_segments( If number of segments exceeds 128. """ # Define maximum number of segments for the ADC - MAX_SEGMENTS = 128 + max_segments = 128 - if mode not in ["shorten", "lengthen"]: + if mode not in ['shorten', 'lengthen']: raise ValueError(f"'mode' must be 'shorten' or 'lengthen' but is {mode}") if system is None: @@ -155,7 +153,7 @@ def calc_adc_segments( min_samples_segment *= system.adc_samples_divisor / gcd_adcdiv # Get segment multiplier - if mode == "shorten": + if mode == 'shorten': samples_seg_multip = floor(num_samples / min_samples_segment) else: samples_seg_multip = ceil(num_samples / min_samples_segment) @@ -166,28 +164,28 @@ def calc_adc_segments( if len(adc_seg_primes) > 1: num_segments_candids = set() for k in range(1, len(adc_seg_primes) + 1): - num_segments_candids |= set(prod(perm) for perm in itertools.combinations(adc_seg_primes, k)) + num_segments_candids |= {prod(perm) for perm in itertools.combinations(adc_seg_primes, k)} # Find suitable candidate for num_segments in sorted(num_segments_candids): num_samples_seg = samples_seg_multip * min_samples_segment / num_segments - if num_samples_seg <= system.adc_samples_limit and num_segments <= MAX_SEGMENTS: + if num_samples_seg <= system.adc_samples_limit and num_segments <= max_segments: break # Found segments and samples else: # Only one possible solution num_samples_seg = samples_seg_multip * min_samples_segment # Does output already fulfill constraints? - if num_samples_seg <= system.adc_samples_limit and num_segments <= MAX_SEGMENTS: + if num_samples_seg <= system.adc_samples_limit and num_segments <= max_segments: break else: # Shorten or lengthen the number of samples per segment - samples_seg_multip += 1 if mode == "lengthen" else -1 + samples_seg_multip += 1 if mode == 'lengthen' else -1 # Validate constraints if samples_seg_multip <= 0: - raise ValueError("Could not find suitable segmentation.") + raise ValueError('Could not find suitable segmentation.') if num_samples_seg == 0: - raise ValueError("Could not find suitable number of samples per segment.") - if num_segments > MAX_SEGMENTS: - raise ValueError(f"Number of segments ({num_segments}) exceeds allowed number of {MAX_SEGMENTS}") + raise ValueError('Could not find suitable number of samples per segment.') + if num_segments > max_segments: + raise ValueError(f'Number of segments ({num_segments}) exceeds allowed number of {max_segments}') return int(num_segments), int(num_samples_seg) diff --git a/pypulseq/make_adiabatic_pulse.py b/pypulseq/make_adiabatic_pulse.py index 249a78f..4d9f5c6 100644 --- a/pypulseq/make_adiabatic_pulse.py +++ b/pypulseq/make_adiabatic_pulse.py @@ -1,16 +1,16 @@ +import math from types import SimpleNamespace from typing import Tuple, Union from warnings import warn import numpy as np -import math from pypulseq import eps from pypulseq.calc_rf_center import calc_rf_center from pypulseq.make_trapezoid import make_trapezoid from pypulseq.opts import Opts from pypulseq.supported_labels_rf_use import get_supported_rf_uses -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_adiabatic_pulse( @@ -50,12 +50,14 @@ def make_adiabatic_pulse( - mu (float): a constant, determines amplitude of frequency sweep. - dur (float): pulse time (s). - Returns: + Returns + ------- 2-element tuple containing - **a** (*array*): AM waveform. - **om** (*array*): FM waveform (radians/s). - References: + References + ---------- Baum, J., Tycko, R. and Pines, A. (1985). 'Broadband and adiabatic inversion of a two-level system by phase-modulated pulses'. Phys. Rev. A., 32:3435-3447. @@ -69,15 +71,17 @@ def make_adiabatic_pulse( - bw (float): pulse bandwidth. - dur (float): pulse time (s). - Returns: + Returns + ------- 2-element tuple containing - **a** (*array*): AM waveform. - **om** (*array*): FM waveform (radians/s). - References: + References + ---------- Kupce, E. and Freeman, R. (1995). 'Stretched Adiabatic Pulses for Broadband Spin Inversion'. - J. Magn. Reson. Ser. A., 117:246-256. + J. Magn. Reason. Set. A., 117:246-256. Parameters ---------- @@ -132,14 +136,14 @@ def make_adiabatic_pulse( system = Opts.default if return_gz and slice_thickness <= 0: - raise ValueError("Slice thickness must be provided") + raise ValueError('Slice thickness must be provided') - valid_pulse_types = ["hypsec", "wurst"] + valid_pulse_types = ['hypsec', 'wurst'] if (not pulse_type) or (pulse_type not in valid_pulse_types): - raise ValueError(f"Invalid type parameter. Must be one of {valid_pulse_types}.Passed: {pulse_type}") + raise ValueError(f'Invalid type parameter. Must be one of {valid_pulse_types}.Passed: {pulse_type}') valid_rf_use_labels = get_supported_rf_uses() - if use != "" and use not in valid_rf_use_labels: - raise ValueError(f"Invalid use parameter. Must be one of {valid_rf_use_labels}. Passed: {use}") + if use != '' and use not in valid_rf_use_labels: + raise ValueError(f'Invalid use parameter. Must be one of {valid_rf_use_labels}. Passed: {use}') if dwell is None: dwell = system.rf_raster_time @@ -149,9 +153,9 @@ def make_adiabatic_pulse( # Number of points must be divisible by 4 - requirement of individual pulse functions n_samples = math.floor(n_raw / 4) * 4 - if pulse_type == "hypsec": + if pulse_type == 'hypsec': amp_mod, freq_mod = _hypsec(n=n_samples, beta=beta, mu=mu, dur=duration) - elif pulse_type == "wurst": + elif pulse_type == 'wurst': amp_mod, freq_mod = _wurst(n=n_samples, n_fac=n_fac, bw=bandwidth, dur=duration) phase_mod = np.cumsum(freq_mod) * dwell @@ -192,14 +196,14 @@ def make_adiabatic_pulse( n_pad = n_raw - n_samples pad_left = n_pad // 2 pad_right = n_pad - pad_left - signal = np.pad(signal, (pad_left, pad_right), mode="constant") + signal = np.pad(signal, (pad_left, pad_right), mode='constant') n_samples = n_raw # Calculate time points t = (np.arange(n_samples) + 0.5) * dwell rf = SimpleNamespace() - rf.type = "rf" + rf.type = 'rf' rf.signal = signal rf.t = t rf.shape_dur = n_samples * dwell @@ -208,9 +212,12 @@ def make_adiabatic_pulse( rf.dead_time = system.rf_dead_time rf.ringdown_time = system.rf_ringdown_time rf.delay = delay - rf.use = use if use != "" else "inversion" + rf.use = use if use != '' else 'inversion' if rf.dead_time > rf.delay: - warn(f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', stacklevel=2) + warn( + f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', + stacklevel=2, + ) rf.delay = rf.dead_time if return_gz: @@ -226,9 +233,9 @@ def make_adiabatic_pulse( # Set to zero, not None for compatibility with existing make_trapezoid max_slew_slice_select = 0 - if pulse_type == "hypsec": + if pulse_type == 'hypsec': bandwidth = mu * beta / np.pi - elif pulse_type == "wurst": + elif pulse_type == 'wurst': bandwidth = bandwidth center_pos, _ = calc_rf_center(rf) @@ -236,18 +243,20 @@ def make_adiabatic_pulse( amplitude = bandwidth / slice_thickness area = amplitude * duration gz = make_trapezoid( - channel="z", + channel='z', system=system, flat_time=duration, flat_area=area, max_grad=max_grad_slice_select, - max_slew=max_slew_slice_select) + max_slew=max_slew_slice_select, + ) gzr = make_trapezoid( - channel="z", + channel='z', system=system, area=-area * (1 - center_pos) - 0.5 * (gz.area - area), max_grad=max_grad_slice_select, - max_slew=max_slew_slice_select) + max_slew=max_slew_slice_select, + ) if rf.delay > gz.rise_time: # Round-up to gradient raster gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time @@ -266,7 +275,7 @@ def make_adiabatic_pulse( """Adiabatic Pulse Design functions. The below functions are originally from ssigpy/sigpy/mri/rf/adiabatic.py - Used under the terms of the Sigpy BSD 3-clause licence. + Used under the terms of the Sigpy BSD 3-clause license. Copyright (c) 2016, Frank Ong. Copyright (c) 2016, The Regents of the University of California. @@ -312,18 +321,19 @@ def _bir4(n: int, beta: float, kappa: float, theta: float, dw0: np.ndarray): theta (float): flip angle in radians. dw0: FM waveform scaling (radians/s). - Returns: + Returns + ------- 2-element tuple containing - **a** (*array*): AM waveform. - **om** (*array*): FM waveform (radians/s). - References: + References + ---------- Staewen, R.S. et al. (1990). '3-D FLASH Imaging using a single surface coil and a new adiabatic pulse, BIR-4'. Invest. Radiology, 25:559-567. """ - dphi = np.pi + theta / 2 t = np.arange(0, n) / n @@ -357,18 +367,19 @@ def _hypsec(n: int = 512, beta: float = 800.0, mu: float = 4.9, dur: float = 0.0 mu (float): a constant, determines amplitude of frequency sweep. dur (float): pulse time (s). - Returns: + Returns + ------- 2-element tuple containing - **a** (*array*): AM waveform. - **om** (*array*): FM waveform (radians/s). - References: + References + ---------- Baum, J., Tycko, R. and Pines, A. (1985). 'Broadband and adiabatic inversion of a two-level system by phase-modulated pulses'. Phys. Rev. A., 32:3435-3447. """ - t = np.arange(-n // 2, n // 2) / n * dur a = np.cosh(beta * t) ** -1 @@ -389,18 +400,19 @@ def _wurst(n: int = 512, n_fac: int = 40, bw: float = 40e3, dur: float = 2e-3): dur (float): pulse time (s). - Returns: + Returns + ------- 2-element tuple containing - **a** (*array*): AM waveform. - **om** (*array*): FM waveform (radians/s). - References: + References + ---------- Kupce, E. and Freeman, R. (1995). 'Stretched Adiabatic Pulses for Broadband Spin Inversion'. - J. Magn. Reson. Ser. A., 117:246-256. + J. Magn. Reason. Set. A., 117:246-256. """ - t = np.arange(0, n) * dur / n a = 1 - np.power(np.abs(np.cos(np.pi * t / dur)), n_fac) @@ -430,20 +442,21 @@ def _goia_wurst( b1_max (float): maximum b1 (Hz) bw (float): pulse bandwidth (Hz) - Returns: + Returns + ------- 3-element tuple containing: - **a** (*array*): AM waveform (Hz) - **om** (*array*): FM waveform (Hz) - **g** (*array*): normalized gradient waveform - References: + References + ---------- O. C. Andronesi, S. Ramadan, E.-M. Ratai, D. Jennings, C. E. Mountford, A. G. Sorenson. - J Magn Reson, 203:283-293, 2010. + J Magn Reason, 203:283-293, 2010. """ - t = np.arange(0, n) * dur / n a = b1_max * (1 - np.abs(np.sin(np.pi / 2 * (2 * t / dur - 1))) ** n_b1) @@ -475,20 +488,21 @@ def _bloch_siegert_fm( perturbation gamma (float): gyromagnetic ratio - Returns: + Returns + ------- om (array): FM waveform (radians/s). - References: + References + ---------- M. M. Khalighi, B. K. Rutt, and A. B. Kerr. Adiabatic RF pulse design for Bloch-Siegert B1+ mapping. - Magn Reson Med, 70(3):829–835, 2013. + Magn Reason Med, 70(3):829-835, 2013. M. Jankiewicz, J. C. Gore, and W. A. Grissom. Improved encoding pulses for Bloch-Siegert B1+ mapping. - J Magn Reson, 226:79–87, 2013. + J Magn Reason, 226:79-87, 2013. """ - # set gamma to PyPulseq default if not provided if gamma is None: gamma = 2 * np.pi * 42.576e6 diff --git a/pypulseq/make_arbitrary_grad.py b/pypulseq/make_arbitrary_grad.py index 205dcb5..40bcdd7 100644 --- a/pypulseq/make_arbitrary_grad.py +++ b/pypulseq/make_arbitrary_grad.py @@ -4,7 +4,7 @@ import numpy as np from pypulseq.opts import Opts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_arbitrary_grad( @@ -72,14 +72,14 @@ def make_arbitrary_grad( if max_slew is None or max_slew == 0: max_slew = system.max_slew - if channel not in ["x", "y", "z"]: - raise ValueError(f"Invalid channel. Must be one of x, y or z. Passed: {channel}") + if channel not in ['x', 'y', 'z']: + raise ValueError(f'Invalid channel. Must be one of x, y or z. Passed: {channel}') slew_rate = np.diff(waveform) / system.grad_raster_time if max(abs(slew_rate)) >= max_slew: - raise ValueError(f"Slew rate violation {max(abs(slew_rate)) / max_slew * 100}") + raise ValueError(f'Slew rate violation {max(abs(slew_rate)) / max_slew * 100}') if max(abs(waveform)) >= max_grad: - raise ValueError(f"Gradient amplitude violation {max(abs(waveform)) / max_grad * 100}") + raise ValueError(f'Gradient amplitude violation {max(abs(waveform)) / max_grad * 100}') if not first: first = (3 * waveform[0] - waveform[1]) * 0.5 # linear extrapolation @@ -88,7 +88,7 @@ def make_arbitrary_grad( last = (3 * waveform[-1] - waveform[-2]) * 0.5 # linear extrapolation grad = SimpleNamespace() - grad.type = "grad" + grad.type = 'grad' grad.channel = channel grad.waveform = waveform grad.delay = delay diff --git a/pypulseq/make_arbitrary_rf.py b/pypulseq/make_arbitrary_rf.py index e45b8bb..40a229d 100644 --- a/pypulseq/make_arbitrary_rf.py +++ b/pypulseq/make_arbitrary_rf.py @@ -1,18 +1,15 @@ +import math +from copy import copy from types import SimpleNamespace from typing import Tuple, Union -from copy import copy +from warnings import warn import numpy as np -import math -from warnings import warn -from pypulseq import make_delay, calc_duration from pypulseq.make_trapezoid import make_trapezoid -from pypulseq.make_delay import make_delay -from pypulseq.calc_duration import calc_duration from pypulseq.opts import Opts from pypulseq.supported_labels_rf_use import get_supported_rf_uses -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_arbitrary_rf( @@ -87,7 +84,7 @@ def make_arbitrary_rf( system = Opts.default valid_use_pulses = get_supported_rf_uses() - if use != "" and use not in valid_use_pulses: + if use != '' and use not in valid_use_pulses: raise ValueError( f"Invalid use parameter. Must be one of 'excitation', 'refocusing' or 'inversion'. Passed: {use}" ) @@ -97,17 +94,17 @@ def make_arbitrary_rf( signal = np.squeeze(signal) if signal.ndim > 1: - raise ValueError(f"signal should have ndim=1. Passed ndim={signal.ndim}") + raise ValueError(f'signal should have ndim=1. Passed ndim={signal.ndim}') if not no_signal_scaling: signal = signal / np.abs(np.sum(signal * dwell)) * flip_angle / (2 * np.pi) - N = len(signal) - duration = N * dwell - t = (np.arange(1, N + 1) - 0.5) * dwell + n_samples = len(signal) + duration = n_samples * dwell + t = (np.arange(1, n_samples + 1) - 0.5) * dwell rf = SimpleNamespace() - rf.type = "rf" + rf.type = 'rf' rf.signal = signal rf.t = t rf.shape_dur = duration @@ -117,18 +114,21 @@ def make_arbitrary_rf( rf.ringdown_time = system.rf_ringdown_time rf.delay = delay - if use != "": + if use != '': rf.use = use if rf.dead_time > rf.delay: - warn(f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', stacklevel=2) + warn( + f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', + stacklevel=2, + ) rf.delay = rf.dead_time if return_gz: if slice_thickness <= 0: - raise ValueError("Slice thickness must be provided.") + raise ValueError('Slice thickness must be provided.') if bandwidth <= 0: - raise ValueError("Bandwidth of pulse must be provided.") + raise ValueError('Bandwidth of pulse must be provided.') if max_grad > 0: system = copy(system) @@ -137,22 +137,16 @@ def make_arbitrary_rf( system = copy(system) system.max_slew = max_slew - BW = bandwidth if time_bw_product > 0: - BW = time_bw_product / duration + bandwidth = time_bw_product / duration - amplitude = BW / slice_thickness + amplitude = bandwidth / slice_thickness area = amplitude * duration - gz = make_trapezoid( - channel="z", system=system, flat_time=duration, flat_area=area - ) + gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) if rf.delay > gz.rise_time: # Round-up to gradient raster - gz.delay = ( - math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) - * system.grad_raster_time - ) + gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time if rf.delay < (gz.rise_time + gz.delay): rf.delay = gz.rise_time + gz.delay diff --git a/pypulseq/make_block_pulse.py b/pypulseq/make_block_pulse.py index 02c2dcf..defbb74 100644 --- a/pypulseq/make_block_pulse.py +++ b/pypulseq/make_block_pulse.py @@ -1,20 +1,20 @@ from types import SimpleNamespace -from typing import Tuple, Union +from typing import Union from warnings import warn import numpy as np from pypulseq.opts import Opts from pypulseq.supported_labels_rf_use import get_supported_rf_uses -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_block_pulse( flip_angle: float, delay: float = 0, - duration: float = None, - bandwidth: float = None, - time_bw_product: float = None, + duration: Union[float, None] = None, + bandwidth: Union[float, None] = None, + time_bw_product: Union[float, None] = None, freq_offset: float = 0, phase_offset: float = 0, system: Union[Opts, None] = None, @@ -63,47 +63,39 @@ def make_block_pulse( """ if system is None: system = Opts.default - + valid_use_pulses = get_supported_rf_uses() - if use != "" and use not in valid_use_pulses: - raise ValueError( - "Invalid use parameter. " - f"Must be one of {valid_use_pulses}. Passed: {use}" - ) + if use != '' and use not in valid_use_pulses: + raise ValueError('Invalid use parameter. ' f'Must be one of {valid_use_pulses}. Passed: {use}') if duration is None and bandwidth is None: warn('Using default 4 ms duration for block pulse.') - duration = 4E-3 - elif duration is not None and bandwidth is not None\ - and duration > 0: + duration = 4e-3 + elif duration is not None and bandwidth is not None and duration > 0: # Multiple arguments - raise ValueError( - "One of bandwidth or duration must be defined, but not both.") - elif duration is not None\ - and duration > 0: + raise ValueError('One of bandwidth or duration must be defined, but not both.') + elif duration is not None and duration > 0: # Explicitly handle this most expected case. # There is probably a better way of writing this if block pass - elif duration is None\ - and bandwidth is not None\ - and bandwidth > 0: - if time_bw_product is not None\ - and time_bw_product > 0: + elif duration is None and bandwidth is not None and bandwidth > 0: + if time_bw_product is not None and time_bw_product > 0: duration = time_bw_product / bandwidth else: duration = 1 / (4 * bandwidth) else: # Invalid arguments raise ValueError( - "One of bandwidth or duration must be defined and be > 0. " - f"duration = {duration} s, bandwidth = {bandwidth} Hz.") + 'One of bandwidth or duration must be defined and be > 0. ' + f'duration = {duration} s, bandwidth = {bandwidth} Hz.' + ) - N = round(duration / system.rf_raster_time) - t = np.array([0, N]) * system.rf_raster_time + n_samples = round(duration / system.rf_raster_time) + t = np.array([0, n_samples]) * system.rf_raster_time signal = flip_angle / (2 * np.pi) / duration * np.ones_like(t) rf = SimpleNamespace() - rf.type = "rf" + rf.type = 'rf' rf.signal = signal rf.t = t rf.shape_dur = t[-1] @@ -113,7 +105,7 @@ def make_block_pulse( rf.ringdown_time = system.rf_ringdown_time rf.delay = delay - if use != "": + if use != '': rf.use = use if rf.dead_time > rf.delay: diff --git a/pypulseq/make_delay.py b/pypulseq/make_delay.py index 81f8710..f997397 100755 --- a/pypulseq/make_delay.py +++ b/pypulseq/make_delay.py @@ -1,6 +1,7 @@ -import numpy as np from types import SimpleNamespace +import numpy as np + def make_delay(d: float) -> SimpleNamespace: """ @@ -21,10 +22,9 @@ def make_delay(d: float) -> SimpleNamespace: ValueError If delay is invalid (not finite or < 0). """ - delay = SimpleNamespace() if not np.isfinite(d) or d < 0: - raise ValueError("Delay {:.2f} ms is invalid".format(d * 1e3)) - delay.type = "delay" + raise ValueError('Delay {:.2f} ms is invalid'.format(d * 1e3)) + delay.type = 'delay' delay.delay = d return delay diff --git a/pypulseq/make_digital_output_pulse.py b/pypulseq/make_digital_output_pulse.py index 9b6d0f9..1b9c2d6 100644 --- a/pypulseq/make_digital_output_pulse.py +++ b/pypulseq/make_digital_output_pulse.py @@ -23,7 +23,7 @@ def make_digital_output_pulse( System limits. Returns - ------ + ------- trig : SimpleNamespace Trigger event. @@ -34,14 +34,12 @@ def make_digital_output_pulse( """ if system is None: system = Opts.default - - if channel not in ["osc0", "osc1", "ext1"]: - raise ValueError( - f"Channel {channel} is invalid. Must be one of 'osc0','osc1', or 'ext1'." - ) + + if channel not in ['osc0', 'osc1', 'ext1']: + raise ValueError(f"Channel {channel} is invalid. Must be one of 'osc0','osc1', or 'ext1'.") trig = SimpleNamespace() - trig.type = "output" + trig.type = 'output' trig.channel = channel trig.delay = delay trig.duration = duration diff --git a/pypulseq/make_extended_trapezoid.py b/pypulseq/make_extended_trapezoid.py index 565a9e4..3645176 100644 --- a/pypulseq/make_extended_trapezoid.py +++ b/pypulseq/make_extended_trapezoid.py @@ -7,24 +7,25 @@ from pypulseq.make_arbitrary_grad import make_arbitrary_grad from pypulseq.opts import Opts from pypulseq.points_to_waveform import points_to_waveform -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_extended_trapezoid( channel: str, - amplitudes: np.ndarray = np.zeros(1), + amplitudes: Union[np.ndarray, None] = None, convert_to_arbitrary: bool = False, max_grad: float = 0, max_slew: float = 0, skip_check: bool = False, system: Union[Opts, None] = None, - times: np.ndarray = np.zeros(1), + times: Union[np.ndarray, None] = None, ) -> SimpleNamespace: """ Create a gradient by specifying a set of points (amplitudes) at specified time points(times) at a given channel with given system limits. Returns an arbitrary gradient object. - See also: + See Also + -------- - `pypulseq.Sequence.sequence.Sequence.add_block()` - `pypulseq.opts.Opts` - `pypulseq.make_trapezoid.make_trapezoid()` @@ -62,41 +63,35 @@ def make_extended_trapezoid( If all elements in `amplitudes` are zero. If first amplitude of a gradient is non-ero and does not connect to a previous block. """ + if amplitudes is None: + amplitudes = np.zeros(1) + + if times is None: + times = np.zeros(1) + if system is None: system = Opts.default - - if channel not in ["x", "y", "z"]: - raise ValueError( - f"Invalid channel. Must be one of 'x', 'y' or 'z'. Passed: {channel}" - ) + + if channel not in ['x', 'y', 'z']: + raise ValueError(f"Invalid channel. Must be one of 'x', 'y' or 'z'. Passed: {channel}") times = np.asarray(times) amplitudes = np.asarray(amplitudes) if len(times) != len(amplitudes): - raise ValueError("Times and amplitudes must have the same length.") + raise ValueError('Times and amplitudes must have the same length.') if np.all(times == 0): - raise ValueError("At least one of the given times must be non-zero") + raise ValueError('At least one of the given times must be non-zero') if np.any(np.diff(times) <= 0): - raise ValueError( - "Times must be in ascending order and all times must be distinct" - ) + raise ValueError('Times must be in ascending order and all times must be distinct') - if ( - abs( - round(times[-1] / system.grad_raster_time) * system.grad_raster_time - - times[-1] - ) - > eps - ): - raise ValueError("The last time point must be on a gradient raster") + if abs(round(times[-1] / system.grad_raster_time) * system.grad_raster_time - times[-1]) > eps: + raise ValueError('The last time point must be on a gradient raster') if skip_check is False and times[0] > 0 and amplitudes[0] != 0: - raise ValueError( - "If first amplitude of a gradient is non-zero, it must connect to previous block" - ) + raise ValueError('If first amplitude of a gradient is non-zero, it must connect to previous block') if max_grad <= 0: max_grad = system.max_grad @@ -106,9 +101,7 @@ def make_extended_trapezoid( if convert_to_arbitrary: # Represent the extended trapezoid on the regularly sampled time grid - waveform = points_to_waveform( - times=times, amplitudes=amplitudes, grad_raster_time=system.grad_raster_time - ) + waveform = points_to_waveform(times=times, amplitudes=amplitudes, grad_raster_time=system.grad_raster_time) grad = make_arbitrary_grad( channel=channel, waveform=waveform, @@ -119,24 +112,16 @@ def make_extended_trapezoid( ) else: # Keep the original possibly irregular sampling - if np.any( - np.abs( - np.round(times / system.grad_raster_time) * system.grad_raster_time - - times - ) - > eps - ): + if np.any(np.abs(np.round(times / system.grad_raster_time) * system.grad_raster_time - times) > eps): raise ValueError( 'All time points must be on a gradient raster or "convert_to_arbitrary" option must be used.' ) grad = SimpleNamespace() - grad.type = "grad" + grad.type = 'grad' grad.channel = channel grad.waveform = amplitudes - grad.delay = ( - round(times[0] / system.grad_raster_time) * system.grad_raster_time - ) + grad.delay = round(times[0] / system.grad_raster_time) * system.grad_raster_time grad.tt = times - grad.delay grad.shape_dur = grad.tt[-1] grad.area = 0.5 * ((grad.tt[1:] - grad.tt[:-1]) * (grad.waveform[1:] + grad.waveform[:-1])).sum() diff --git a/pypulseq/make_extended_trapezoid_area.py b/pypulseq/make_extended_trapezoid_area.py index 106925a..c3ae152 100644 --- a/pypulseq/make_extended_trapezoid_area.py +++ b/pypulseq/make_extended_trapezoid_area.py @@ -6,7 +6,7 @@ from pypulseq.make_extended_trapezoid import make_extended_trapezoid from pypulseq.opts import Opts from pypulseq.utils.cumsum import cumsum -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_extended_trapezoid_area( @@ -72,7 +72,6 @@ def _find_solution(duration: int) -> Union[None, Tuple[int, int, int, float]]: ------- Tuple of ramp-up time, flat time, ramp-down time, gradient amplitude or None if no solution was found """ - # Determine timings to check for possible solutions ramp_up_times = [] ramp_down_times = [] @@ -80,9 +79,7 @@ def _find_solution(duration: int) -> Union[None, Tuple[int, int, int, float]]: # First, consider solutions that use maximum slew rate: # Analytically calculate calculate the point where: # grad_start + ramp_up_time * max_slew == grad_end + ramp_down_time * max_slew - ramp_up_time = (duration * max_slew * raster_time - grad_start + grad_end) / ( - 2*max_slew*raster_time - ) + ramp_up_time = (duration * max_slew * raster_time - grad_start + grad_end) / (2 * max_slew * raster_time) ramp_up_time = round(ramp_up_time) # Check if gradient amplitude exceeds max_grad, if so, adjust ramp @@ -94,17 +91,13 @@ def _find_solution(duration: int) -> Union[None, Tuple[int, int, int, float]]: ramp_down_time = duration - ramp_up_time # Add possible solution if timing is valid - if (ramp_up_time > 0 - and ramp_down_time > 0 - and ramp_up_time + ramp_down_time <= duration): + if ramp_up_time > 0 and ramp_down_time > 0 and ramp_up_time + ramp_down_time <= duration: ramp_up_times.append(ramp_up_time) ramp_down_times.append(ramp_down_time) # Analytically calculate calculate the point where: # grad_start - ramp_up_time * max_slew == grad_end - ramp_down_time * max_slew - ramp_up_time = (duration * max_slew * raster_time + grad_start - grad_end) / ( - 2*max_slew*raster_time - ) + ramp_up_time = (duration * max_slew * raster_time + grad_start - grad_end) / (2 * max_slew * raster_time) ramp_up_time = round(ramp_up_time) # Check if gradient amplitude exceeds -max_grad, if so, adjust ramp @@ -116,9 +109,7 @@ def _find_solution(duration: int) -> Union[None, Tuple[int, int, int, float]]: ramp_down_time = duration - ramp_up_time # Add possible solution if timing is valid - if (ramp_up_time > 0 - and ramp_down_time > 0 - and ramp_up_time + ramp_down_time <= duration): + if ramp_up_time > 0 and ramp_down_time > 0 and ramp_up_time + ramp_down_time <= duration: ramp_up_times.append(ramp_up_time) ramp_down_times.append(ramp_down_time) @@ -176,10 +167,10 @@ def _find_solution(duration: int) -> Union[None, Tuple[int, int, int, float]]: # From this point onwards, solutions can always be found by extending # the duration and doing a binary search. max_duration = max( - round(_calc_ramp_time(0, grad_start) / raster_time), - round(_calc_ramp_time(0, grad_end) / raster_time), - min_duration, - ) + round(_calc_ramp_time(0, grad_start) / raster_time), + round(_calc_ramp_time(0, grad_end) / raster_time), + min_duration, + ) # Linear search solution = None @@ -230,6 +221,6 @@ def binary_search(fun, lower_limit, upper_limit): grad.trace = trace() if not abs(grad.area - area) < 1e-8: - raise ValueError(f"Could not find a solution for area={area}.") + raise ValueError(f'Could not find a solution for area={area}.') return grad, np.array(times), amplitudes diff --git a/pypulseq/make_gauss_pulse.py b/pypulseq/make_gauss_pulse.py index 2148edf..6d17e47 100755 --- a/pypulseq/make_gauss_pulse.py +++ b/pypulseq/make_gauss_pulse.py @@ -1,15 +1,15 @@ import math -from warnings import warn +from copy import copy from types import SimpleNamespace from typing import Tuple, Union -from copy import copy +from warnings import warn import numpy as np from pypulseq.make_trapezoid import make_trapezoid from pypulseq.opts import Opts from pypulseq.supported_labels_rf_use import get_supported_rf_uses -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_gauss_pulse( @@ -90,48 +90,49 @@ def make_gauss_pulse( """ if system is None: system = Opts.default - - if use != "" and use not in get_supported_rf_uses(): - raise ValueError( - f"Invalid use parameter. Must be one of {get_supported_rf_uses()}. Passed: {use}" - ) + + if use != '' and use not in get_supported_rf_uses(): + raise ValueError(f'Invalid use parameter. Must be one of {get_supported_rf_uses()}. Passed: {use}') if dwell == 0: dwell = system.rf_raster_time if bandwidth == 0: - BW = time_bw_product / duration + bandwidth = time_bw_product / duration else: - BW = bandwidth + bandwidth = bandwidth alpha = apodization - N = round(duration / dwell) - t = (np.arange(1, N + 1) - 0.5) * dwell + n_samples = round(duration / dwell) + t = (np.arange(1, n_samples + 1) - 0.5) * dwell tt = t - (duration * center_pos) window = 1 - alpha + alpha * np.cos(2 * np.pi * tt / duration) - signal = window * __gauss(BW * tt) + signal = window * __gauss(bandwidth * tt) flip = np.sum(signal) * dwell * 2 * np.pi signal = signal * flip_angle / flip rf = SimpleNamespace() - rf.type = "rf" + rf.type = 'rf' rf.signal = signal rf.t = t - rf.shape_dur = N * dwell + rf.shape_dur = n_samples * dwell rf.freq_offset = freq_offset rf.phase_offset = phase_offset rf.dead_time = system.rf_dead_time rf.ringdown_time = system.rf_ringdown_time rf.delay = delay - if use != "": + if use != '': rf.use = use if rf.dead_time > rf.delay: - warn(f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', stacklevel=2) + warn( + f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', + stacklevel=2, + ) rf.delay = rf.dead_time if return_gz: if slice_thickness == 0: - raise ValueError("Slice thickness must be provided") + raise ValueError('Slice thickness must be provided') if max_grad > 0: system = copy(system) @@ -141,22 +142,17 @@ def make_gauss_pulse( system = copy(system) system.max_slew = max_slew - amplitude = BW / slice_thickness + amplitude = bandwidth / slice_thickness area = amplitude * duration - gz = make_trapezoid( - channel="z", system=system, flat_time=duration, flat_area=area - ) + gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) gzr = make_trapezoid( - channel="z", + channel='z', system=system, area=-area * (1 - center_pos) - 0.5 * (gz.area - area), ) if rf.delay > gz.rise_time: - gz.delay = ( - math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) - * system.grad_raster_time - ) + gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time if rf.delay < (gz.rise_time + gz.delay): rf.delay = gz.rise_time + gz.delay diff --git a/pypulseq/make_label.py b/pypulseq/make_label.py index fab6920..52d91c7 100644 --- a/pypulseq/make_label.py +++ b/pypulseq/make_label.py @@ -4,9 +4,7 @@ from pypulseq.supported_labels_rf_use import get_supported_labels -def make_label( - label: str, type: str, value: Union[bool, float, int] -) -> SimpleNamespace: +def make_label(label: str, type: str, value: Union[bool, float, int]) -> SimpleNamespace: # noqa: A002 """ Create an ADC Label. @@ -39,16 +37,16 @@ def make_label( "Invalid label. Must be one of 'SLC', 'SEG', 'REP', 'AVG', 'SET', 'ECO', 'PHS', 'LIN', 'PAR', " "NAV', 'REV', or 'SMS'." ) - if type not in ["SET", "INC"]: + if type not in ['SET', 'INC']: raise ValueError("Invalid type. Must be one of 'SET' or 'INC'.") if not isinstance(value, (bool, float, int)): - raise ValueError("Must supply a valid numerical or logical value.") + raise ValueError('Must supply a valid numerical or logical value.') out = SimpleNamespace() - if type == "SET": - out.type = "labelset" - elif type == "INC": - out.type = "labelinc" + if type == 'SET': + out.type = 'labelset' + elif type == 'INC': + out.type = 'labelinc' out.label = label out.value = value diff --git a/pypulseq/make_sigpy_pulse.py b/pypulseq/make_sigpy_pulse.py index d2190ba..c1859bf 100644 --- a/pypulseq/make_sigpy_pulse.py +++ b/pypulseq/make_sigpy_pulse.py @@ -1,23 +1,24 @@ import math -from warnings import warn +from copy import copy from types import SimpleNamespace from typing import Tuple, Union -from copy import copy +from warnings import warn import numpy as np try: import sigpy.mri.rf as rf import sigpy.plot as pl -except ModuleNotFoundError: +except ModuleNotFoundError as err: raise ModuleNotFoundError( "SigPy is not installed. Install it using 'pip install sigpy' or 'pip install pypulseq[sigpy]'." - ) + ) from err from pypulseq.make_trapezoid import make_trapezoid from pypulseq.opts import Opts from pypulseq.sigpy_pulse_opts import SigpyPulseOpts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled + def sigpy_n_seq( flip_angle: float, @@ -32,7 +33,7 @@ def sigpy_n_seq( slice_thickness: float = 0, system: Union[Opts, None] = None, time_bw_product: float = 4, - pulse_cfg: SigpyPulseOpts = SigpyPulseOpts(), + pulse_cfg: Union[SigpyPulseOpts, None] = None, use: str = str(), plot: bool = True, ) -> Union[SimpleNamespace, Tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace]]: @@ -66,7 +67,7 @@ def sigpy_n_seq( Slice thickness of accompanying slice select trapezoidal event. The slice thickness determines the area of the slice select event. system : Opts, optional - System limits. Default is a system limits object initialised to default values. + System limits. Default is a system limits object initialized to default values. time_bw_product : float, optional, default=4 Time-bandwidth product. use : str, optional, default=str() @@ -92,13 +93,16 @@ def sigpy_n_seq( if system is None: system = Opts.default - valid_use_pulses = ["excitation", "refocusing", "inversion"] - if use != "" and use not in valid_use_pulses: + if pulse_cfg is None: + pulse_cfg = SigpyPulseOpts() + + valid_use_pulses = ['excitation', 'refocusing', 'inversion'] + if use != '' and use not in valid_use_pulses: raise ValueError( f"Invalid use parameter. Must be one of 'excitation', 'refocusing' or 'inversion'. Passed: {use}" ) - if pulse_cfg.pulse_type == "slr": + if pulse_cfg.pulse_type == 'slr': [signal, t, pulse] = make_slr( flip_angle=flip_angle, time_bw_product=time_bw_product, @@ -107,7 +111,7 @@ def sigpy_n_seq( pulse_cfg=pulse_cfg, disp=plot, ) - if pulse_cfg.pulse_type == "sms": + if pulse_cfg.pulse_type == 'sms': [signal, t, pulse] = make_sms( flip_angle=flip_angle, time_bw_product=time_bw_product, @@ -118,7 +122,7 @@ def sigpy_n_seq( ) rfp = SimpleNamespace() - rfp.type = "rf" + rfp.type = 'rf' rfp.signal = signal rfp.t = t rfp.shape_dur = t[-1] @@ -128,16 +132,19 @@ def sigpy_n_seq( rfp.ringdown_time = system.rf_ringdown_time rfp.delay = delay - if use != "": + if use != '': rfp.use = use if rfp.dead_time > rfp.delay: - warn(f'Specified RF delay {rfp.delay*1e6:.2f} us is less than the dead time {rfp.dead_time*1e6:.0f} us. Delay was increased to the dead time.', stacklevel=2) + warn( + f'Specified RF delay {rfp.delay*1e6:.2f} us is less than the dead time {rfp.dead_time*1e6:.0f} us. Delay was increased to the dead time.', + stacklevel=2, + ) rfp.delay = rfp.dead_time if return_gz: if slice_thickness == 0: - raise ValueError("Slice thickness must be provided") + raise ValueError('Slice thickness must be provided') if max_grad > 0: system = copy(system) @@ -146,12 +153,12 @@ def sigpy_n_seq( if max_slew > 0: system = copy(system) system.max_slew = max_slew - BW = time_bw_product / duration - amplitude = BW / slice_thickness + bandwidth = time_bw_product / duration + amplitude = bandwidth / slice_thickness area = amplitude * duration - gz = make_trapezoid(channel="z", system=system, flat_time=duration, flat_area=area) + gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) gzr = make_trapezoid( - channel="z", + channel='z', system=system, area=-area * (1 - center_pos) - 0.5 * (gz.area - area), ) @@ -185,14 +192,17 @@ def make_slr( time_bw_product: float = 4, duration: float = 0, system: Union[Opts, None] = None, - pulse_cfg: SigpyPulseOpts = SigpyPulseOpts(), + pulse_cfg: Union[SigpyPulseOpts, None] = None, disp: bool = False, ): if system is None: system = Opts.default - N = int(round(duration / 1e-6)) - t = np.arange(1, N + 1) * system.rf_raster_time + if pulse_cfg is None: + pulse_cfg = SigpyPulseOpts() + + n_samples = int(round(duration / 1e-6)) + t = np.arange(1, n_samples + 1) * system.rf_raster_time # Insert sigpy ptype = pulse_cfg.ptype @@ -202,7 +212,7 @@ def make_slr( cancel_alpha_phs = pulse_cfg.cancel_alpha_phs pulse = rf.slr.dzrf( - n=N, + n=n_samples, tb=time_bw_product, ptype=ptype, ftype=ftype, @@ -223,8 +233,8 @@ def make_slr( np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000), True, ) - Mxy = 2 * np.multiply(np.conj(a), b) - pl.LinePlot(Mxy) + mag_xy = 2 * np.multiply(np.conj(a), b) + pl.LinePlot(mag_xy) return signal, t, pulse @@ -234,14 +244,17 @@ def make_sms( time_bw_product: float = 4, duration: float = 0, system: Union[Opts, None] = None, - pulse_cfg: SigpyPulseOpts = SigpyPulseOpts(), + pulse_cfg: Union[SigpyPulseOpts, None] = None, disp: bool = False, ): if system is None: system = Opts.default - N = int(round(duration / 1e-6)) - t = np.arange(1, N + 1) * system.rf_raster_time + if pulse_cfg is None: + pulse_cfg = SigpyPulseOpts() + + n_samples = int(round(duration / 1e-6)) + t = np.arange(1, n_samples + 1) * system.rf_raster_time # Insert sigpy ptype = pulse_cfg.ptype @@ -254,7 +267,7 @@ def make_sms( phs_0_pt = pulse_cfg.phs_0_pt pulse_in = rf.slr.dzrf( - n=N, + n=n_samples, tb=time_bw_product, ptype=ptype, ftype=ftype, @@ -277,7 +290,7 @@ def make_sms( np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000), True, ) - Mxy = 2 * np.multiply(np.conj(a), b) - pl.LinePlot(Mxy) + mag_xy = 2 * np.multiply(np.conj(a), b) + pl.LinePlot(mag_xy) return signal, t, pulse diff --git a/pypulseq/make_sinc_pulse.py b/pypulseq/make_sinc_pulse.py index 25bfdee..dfd32aa 100755 --- a/pypulseq/make_sinc_pulse.py +++ b/pypulseq/make_sinc_pulse.py @@ -1,15 +1,15 @@ import math -from warnings import warn +from copy import copy from types import SimpleNamespace from typing import Tuple, Union -from copy import copy +from warnings import warn import numpy as np from pypulseq.make_trapezoid import make_trapezoid from pypulseq.opts import Opts from pypulseq.supported_labels_rf_use import get_supported_rf_uses -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def make_sinc_pulse( @@ -63,7 +63,7 @@ def make_sinc_pulse( Slice thickness of accompanying slice select trapezoidal event. The slice thickness determines the area of the slice select event. system : Opts, default=Opts() - System limits. Default is a system limits object initialised to default values. + System limits. Default is a system limits object initialized to default values. time_bw_product : float, default=4 Time-bandwidth product. use : str, default=str() @@ -88,34 +88,32 @@ def make_sinc_pulse( """ if system is None: system = Opts.default - + valid_pulse_uses = get_supported_rf_uses() - if use != "" and use not in valid_pulse_uses: - raise ValueError( - f"Invalid use parameter. Must be one of {valid_pulse_uses}. Passed: {use}" - ) + if use != '' and use not in valid_pulse_uses: + raise ValueError(f'Invalid use parameter. Must be one of {valid_pulse_uses}. Passed: {use}') if dwell == 0: dwell = system.rf_raster_time if duration <= 0: - raise ValueError("RF pulse duration must be positive.") + raise ValueError('RF pulse duration must be positive.') - BW = time_bw_product / duration + bandwidth = time_bw_product / duration alpha = apodization - N = round(duration / dwell) - t = (np.arange(1, N + 1) - 0.5) * dwell + n_samples = round(duration / dwell) + t = (np.arange(1, n_samples + 1) - 0.5) * dwell tt = t - (duration * center_pos) window = 1 - alpha + alpha * np.cos(2 * np.pi * tt / duration) - signal = np.multiply(window, np.sinc(BW * tt)) + signal = np.multiply(window, np.sinc(bandwidth * tt)) flip = np.sum(signal) * dwell * 2 * np.pi signal = signal * flip_angle / flip rf = SimpleNamespace() - rf.type = "rf" + rf.type = 'rf' rf.signal = signal rf.t = t - rf.shape_dur = N * dwell + rf.shape_dur = n_samples * dwell rf.freq_offset = freq_offset rf.phase_offset = phase_offset rf.dead_time = system.rf_dead_time @@ -126,12 +124,15 @@ def make_sinc_pulse( rf.use = use if rf.dead_time > rf.delay: - warn(f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', stacklevel=2) + warn( + f'Specified RF delay {rf.delay*1e6:.2f} us is less than the dead time {rf.dead_time*1e6:.0f} us. Delay was increased to the dead time.', + stacklevel=2, + ) rf.delay = rf.dead_time if return_gz: if slice_thickness == 0: - raise ValueError("Slice thickness must be provided") + raise ValueError('Slice thickness must be provided') if max_grad > 0: system = copy(system) @@ -141,22 +142,17 @@ def make_sinc_pulse( system = copy(system) system.max_slew = max_slew - amplitude = BW / slice_thickness + amplitude = bandwidth / slice_thickness area = amplitude * duration - gz = make_trapezoid( - channel="z", system=system, flat_time=duration, flat_area=area - ) + gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) gzr = make_trapezoid( - channel="z", + channel='z', system=system, area=-area * (1 - center_pos) - 0.5 * (gz.area - area), ) if rf.delay > gz.rise_time: - gz.delay = ( - math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) - * system.grad_raster_time - ) + gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time if rf.delay < (gz.rise_time + gz.delay): rf.delay = gz.rise_time + gz.delay diff --git a/pypulseq/make_trapezoid.py b/pypulseq/make_trapezoid.py index 289f6e4..52948a2 100644 --- a/pypulseq/make_trapezoid.py +++ b/pypulseq/make_trapezoid.py @@ -1,33 +1,24 @@ +import math from types import SimpleNamespace from typing import Union import numpy as np -import math from pypulseq.opts import Opts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def calculate_shortest_params_for_area(area, max_slew, max_grad, grad_raster_time): - rise_time = ( - math.ceil(math.sqrt(abs(area) / max_slew) / grad_raster_time) - * grad_raster_time - ) + rise_time = math.ceil(math.sqrt(abs(area) / max_slew) / grad_raster_time) * grad_raster_time if rise_time < grad_raster_time: # Area was almost 0 maybe rise_time = grad_raster_time amplitude = np.divide(area, rise_time) # To handle nan t_eff = rise_time if abs(amplitude) > max_grad: - t_eff = ( - math.ceil(abs(area) / max_grad / grad_raster_time) - * grad_raster_time - ) + t_eff = math.ceil(abs(area) / max_grad / grad_raster_time) * grad_raster_time amplitude = area / t_eff - rise_time = ( - math.ceil(abs(amplitude) / max_slew / grad_raster_time) - * grad_raster_time - ) + rise_time = math.ceil(abs(amplitude) / max_slew / grad_raster_time) * grad_raster_time if rise_time == 0: rise_time = grad_raster_time @@ -41,11 +32,11 @@ def calculate_shortest_params_for_area(area, max_slew, max_grad, grad_raster_tim def make_trapezoid( channel: str, amplitude: float = 0, - area: float = None, + area: Union[float, None] = None, delay: float = 0, duration: float = 0, fall_time: float = 0, - flat_area: float = None, + flat_area: Union[float, None] = None, flat_time: float = -1, max_grad: float = 0, max_slew: float = 0, @@ -63,7 +54,8 @@ def make_trapezoid( - flat_time, area and rise_time Additional options may be supplied with the above. - See also: + See Also + -------- - `pypulseq.Sequence.sequence.Sequence.add_block()` - `pypulseq.opts.Opts` @@ -110,10 +102,8 @@ def make_trapezoid( if system is None: system = Opts.default - if channel not in ["x", "y", "z"]: - raise ValueError( - f"Invalid channel. Must be one of `x`, `y` or `z`. Passed: {channel}" - ) + if channel not in ['x', 'y', 'z']: + raise ValueError(f'Invalid channel. Must be one of `x`, `y` or `z`. Passed: {channel}') if max_grad <= 0: max_grad = system.max_grad @@ -127,7 +117,7 @@ def make_trapezoid( if fall_time > 0: if rise_time == 0: raise ValueError( - "Invalid arguments. Must always supply `rise_time` if `fall_time` is specified explicitly." + 'Invalid arguments. Must always supply `rise_time` if `fall_time` is specified explicitly.' ) else: fall_time = 0.0 @@ -138,23 +128,20 @@ def make_trapezoid( if flat_time != -1: if amplitude != 0: amplitude2 = amplitude - elif area is not None\ - and rise_time > 0: + elif area is not None and rise_time > 0: # We have rise_time, flat_time and area. amplitude2 = area / (rise_time + flat_time) elif flat_area is not None: amplitude2 = flat_area / flat_time else: raise ValueError( - "When `flat_time` is provided, either `flat_area`, " - "or `amplitude`, or `rise_time` and `area` must be provided as well." - ) + 'When `flat_time` is provided, either `flat_area`, ' + 'or `amplitude`, or `rise_time` and `area` must be provided as well.' + ) if rise_time == 0: rise_time = abs(amplitude2) / max_slew - rise_time = ( - math.ceil(rise_time / system.grad_raster_time) * system.grad_raster_time - ) + rise_time = math.ceil(rise_time / system.grad_raster_time) * system.grad_raster_time if rise_time == 0: rise_time = system.grad_raster_time if fall_time == 0: @@ -162,36 +149,31 @@ def make_trapezoid( elif duration > 0: if amplitude == 0: if rise_time == 0: - _, rise_time, flat_time, fall_time = calculate_shortest_params_for_area(area, max_slew, max_grad, system.grad_raster_time) + _, rise_time, flat_time, fall_time = calculate_shortest_params_for_area( + area, max_slew, max_grad, system.grad_raster_time + ) min_duration = rise_time + flat_time + fall_time assert duration >= min_duration, ( - f"Requested area is too large for this gradient. Minimum required duration is " - f"{round(min_duration * 1e6)} us" + f'Requested area is too large for this gradient. Minimum required duration is ' + f'{round(min_duration * 1e6)} us' ) - dC = 1 / abs(2 * max_slew) + 1 / abs(2 * max_slew) - amplitude2 = ( - duration - math.sqrt(duration**2 - 4 * abs(area) * dC) - ) / (2 * dC) + dc = 1 / abs(2 * max_slew) + 1 / abs(2 * max_slew) + amplitude2 = (duration - math.sqrt(duration**2 - 4 * abs(area) * dc)) / (2 * dc) else: if fall_time == 0: fall_time = rise_time amplitude2 = area / (duration - 0.5 * rise_time - 0.5 * fall_time) - possible = ( - duration >= (rise_time + fall_time) and abs(amplitude2) <= max_grad - ) + possible = duration >= (rise_time + fall_time) and abs(amplitude2) <= max_grad assert possible, ( - f"Requested area is too large for this gradient. Probably amplitude is violated " - f"{round(abs(amplitude) / max_grad * 100)}" + f'Requested area is too large for this gradient. Probably amplitude is violated ' + f'{round(abs(amplitude) / max_grad * 100)}' ) else: amplitude2 = amplitude if rise_time == 0: - rise_time = ( - math.ceil(abs(amplitude2) / max_slew / system.grad_raster_time) - * system.grad_raster_time - ) + rise_time = math.ceil(abs(amplitude2) / max_slew / system.grad_raster_time) * system.grad_raster_time if rise_time == 0: rise_time = system.grad_raster_time @@ -204,16 +186,19 @@ def make_trapezoid( amplitude2 = area / (rise_time / 2 + fall_time / 2 + flat_time) else: if area is None: - raise ValueError("Must supply area or duration.") + raise ValueError('Must supply area or duration.') else: # Find the shortest possible duration. - amplitude2, rise_time, flat_time, fall_time = calculate_shortest_params_for_area(area, max_slew, max_grad, system.grad_raster_time) + amplitude2, rise_time, flat_time, fall_time = calculate_shortest_params_for_area( + area, max_slew, max_grad, system.grad_raster_time + ) - assert abs(amplitude2) <= max_grad, ( - f"Refined amplitude ({abs(amplitude2):0.0f} Hz/m) is larger than max ({max_grad:0.0f} Hz/m).") + assert ( + abs(amplitude2) <= max_grad + ), f'Refined amplitude ({abs(amplitude2):0.0f} Hz/m) is larger than max ({max_grad:0.0f} Hz/m).' grad = SimpleNamespace() - grad.type = "trap" + grad.type = 'trap' grad.channel = channel grad.amplitude = amplitude2 grad.rise_time = rise_time diff --git a/pypulseq/make_trigger.py b/pypulseq/make_trigger.py index a0e8ed5..f34d962 100644 --- a/pypulseq/make_trigger.py +++ b/pypulseq/make_trigger.py @@ -10,14 +10,14 @@ def make_trigger( channel: str, delay: float = 0, duration: float = 0, system: Union[Opts, None] = None ) -> SimpleNamespace: """ - Create a trigger halt event for a synchronisation with an external signal from a given channel with an optional + Create a trigger halt event for a synchronization with an external signal from a given channel with an optional given delay prio to the sync and duration after the sync. Possible channel values: 'physio1','physio2' (Siemens specific). See also `pypulseq.Sequence.sequence.Sequence.add_block()`. - Parameters - ---------- + Parameters + ---------- channel : str Must be one of 'physio1' or 'physio2'. delay : float, default=0 @@ -27,26 +27,24 @@ def make_trigger( system : Opts, default=Opts() System limits. - Returns - ------- + Returns + ------- trigger : SimpleNamespace Trigger event. - Raises - ------ + Raises + ------ ValueError If invalid `channel` is passed. Must be one of 'physio1' or 'physio2'. """ if system is None: system = Opts.default - - if channel not in ["physio1", "physio2"]: - raise ValueError( - f"Channel {channel} is invalid. Must be one of 'physio1' or 'physio2'." - ) + + if channel not in ['physio1', 'physio2']: + raise ValueError(f"Channel {channel} is invalid. Must be one of 'physio1' or 'physio2'.") trigger = SimpleNamespace() - trigger.type = "trigger" + trigger.type = 'trigger' trigger.channel = channel trigger.delay = delay trigger.duration = duration diff --git a/pypulseq/opts.py b/pypulseq/opts.py index 971b832..5f5dd21 100755 --- a/pypulseq/opts.py +++ b/pypulseq/opts.py @@ -1,11 +1,12 @@ from typing import Optional + from pypulseq.convert import convert class Opts: """ System limits of an MR scanner. - + Note: Default values can be overwritten by creating an Opts object and calling `set_as_default`. @@ -50,6 +51,7 @@ class Opts: If invalid `grad_unit` is passed. Must be one of 'Hz/m', 'mT/m' or 'rad/ms/mm'. If invalid `slew_unit` is passed. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s' or 'rad/ms/mm/ms'. """ + def __init__( self, adc_dead_time: Optional[float] = None, @@ -57,7 +59,7 @@ def __init__( block_duration_raster: Optional[float] = None, gamma: Optional[float] = None, grad_raster_time: Optional[float] = None, - grad_unit: str = "Hz/m", + grad_unit: str = 'Hz/m', max_grad: Optional[float] = None, max_slew: Optional[float] = None, rf_dead_time: Optional[float] = None, @@ -66,57 +68,52 @@ def __init__( adc_samples_limit: Optional[int] = None, adc_samples_divisor: Optional[int] = None, rise_time: Optional[float] = None, - slew_unit: str = "Hz/m/s", + slew_unit: str = 'Hz/m/s', B0: Optional[float] = None, ): - valid_grad_units = ["Hz/m", "mT/m", "rad/ms/mm"] - valid_slew_units = ["Hz/m/s", "mT/m/ms", "T/m/s", "rad/ms/mm/ms"] + valid_grad_units = ['Hz/m', 'mT/m', 'rad/ms/mm'] + valid_slew_units = ['Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms'] if grad_unit not in valid_grad_units: raise ValueError( - f"Invalid gradient unit. Must be one of 'Hz/m', 'mT/m' or 'rad/ms/mm'. " - f"Passed: {grad_unit}" + f"Invalid gradient unit. Must be one of 'Hz/m', 'mT/m' or 'rad/ms/mm'. " f'Passed: {grad_unit}' ) if slew_unit not in valid_slew_units: raise ValueError( f"Invalid slew rate unit. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s' or 'rad/ms/mm/ms'. " - f"Passed: {slew_unit}" + f'Passed: {slew_unit}' ) if gamma is None: gamma = Opts.default.gamma if max_grad is not None: - max_grad = convert( - from_value=max_grad, from_unit=grad_unit, to_unit="Hz/m", gamma=abs(gamma) - ) + max_grad = convert(from_value=max_grad, from_unit=grad_unit, to_unit='Hz/m', gamma=abs(gamma)) else: max_grad = Opts.default.max_grad if max_slew is not None: - max_slew = convert( - from_value=max_slew, from_unit=slew_unit, to_unit="Hz/m", gamma=abs(gamma) - ) + max_slew = convert(from_value=max_slew, from_unit=slew_unit, to_unit='Hz/m', gamma=abs(gamma)) else: max_slew = Opts.default.max_slew if rise_time is not None: max_slew = max_grad / rise_time - + if adc_dead_time is None: adc_dead_time = Opts.default.adc_dead_time if adc_raster_time is None: adc_raster_time = Opts.default.adc_raster_time if block_duration_raster is None: block_duration_raster = Opts.default.block_duration_raster - + if rf_dead_time is None: rf_dead_time = Opts.default.rf_dead_time if rf_raster_time is None: rf_raster_time = Opts.default.rf_raster_time if grad_raster_time is None: - grad_raster_time = Opts.default.grad_raster_time + grad_raster_time = Opts.default.grad_raster_time if rf_ringdown_time is None: rf_ringdown_time = Opts.default.rf_ringdown_time if adc_samples_limit is None: @@ -140,34 +137,37 @@ def __init__( self.adc_samples_divisor = adc_samples_divisor self.gamma = gamma self.B0 = B0 - + def set_as_default(self): Opts.default = self - + @classmethod def reset_default(cls): - cls.default = Opts(max_grad=convert(from_value=40, from_unit="mT/m"), - max_slew=convert(from_value=170, from_unit="T/m/s"), - rf_dead_time=0, - rf_ringdown_time=0, - adc_dead_time=0, - adc_raster_time=100e-9, - rf_raster_time=1e-6, - grad_raster_time=10e-6, - block_duration_raster=10e-6, - adc_samples_limit=0, - adc_samples_divisor=4, - gamma=42576000, - B0=1.5) + cls.default = Opts( + max_grad=convert(from_value=40, from_unit='mT/m'), + max_slew=convert(from_value=170, from_unit='T/m/s'), + rf_dead_time=0, + rf_ringdown_time=0, + adc_dead_time=0, + adc_raster_time=100e-9, + rf_raster_time=1e-6, + grad_raster_time=10e-6, + block_duration_raster=10e-6, + adc_samples_limit=0, + adc_samples_divisor=4, + gamma=42576000, + B0=1.5, + ) def __str__(self) -> str: """ Print a string representation of the system limits objects. """ variables = vars(self) - s = [f"{key}: {value}" for key, value in variables.items()] - s = "\n".join(s) - s = "System limits:\n" + s + s = [f'{key}: {value}' for key, value in variables.items()] + s = '\n'.join(s) + s = 'System limits:\n' + s return s + Opts.reset_default() diff --git a/pypulseq/points_to_waveform.py b/pypulseq/points_to_waveform.py index 322ec28..b0c7878 100644 --- a/pypulseq/points_to_waveform.py +++ b/pypulseq/points_to_waveform.py @@ -1,9 +1,7 @@ import numpy as np -def points_to_waveform( - amplitudes: np.ndarray, grad_raster_time: float, times: np.ndarray -) -> np.ndarray: +def points_to_waveform(amplitudes: np.ndarray, grad_raster_time: float, times: np.ndarray) -> np.ndarray: """ 1D interpolate amplitude values `amplitudes` at time indices `times` as per the gradient raster time `grad_raster_time` to generate a gradient waveform. @@ -22,7 +20,6 @@ def points_to_waveform( waveform : numpy.ndarray Gradient waveform. """ - amplitudes = np.asarray(amplitudes) times = np.asarray(times) diff --git a/pypulseq/recon_examples/2dFFT.py b/pypulseq/recon_examples/2dFFT.py deleted file mode 100644 index 009c3c8..0000000 --- a/pypulseq/recon_examples/2dFFT.py +++ /dev/null @@ -1,20 +0,0 @@ -import numpy as np -from matplotlib import pyplot as plt - -from dat2py import dat2py_main - -path = r"C:\Users\sravan953\Downloads\FINAL_meas_MID00169_FID00800_pulseq_3D_mprage.dat" -kspace, img = dat2py_main.main(dat_file_path=path) -img = np.abs(np.sqrt(np.sum(np.square(img), -1))) - -plt.imshow(img) -plt.show() - - -def main(): - path = r"C:\Users\sravan953\Desktop\20210424_7datas\gre_meas_MID00176_FID00172_pulseq.dat" - # kspace, img = dat2py_main.main(dat_file_path=path) - - -if __name__ == '__main__': - main() diff --git a/pypulseq/rotate.py b/pypulseq/rotate.py index 2f9a779..bedce55 100644 --- a/pypulseq/rotate.py +++ b/pypulseq/rotate.py @@ -4,23 +4,18 @@ import numpy as np from pypulseq.add_gradients import add_gradients -from pypulseq.scale_grad import scale_grad from pypulseq.opts import Opts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.scale_grad import scale_grad +from pypulseq.utils.tracing import trace, trace_enabled def __get_grad_abs_mag(grad: SimpleNamespace) -> np.ndarray: - if grad.type == "trap": + if grad.type == 'trap': return abs(grad.amplitude) return np.max(np.abs(grad.waveform)) -def rotate( - *args: SimpleNamespace, - angle: float, - axis: str, - system : Union[Opts, None] = None -) -> List[SimpleNamespace]: +def rotate(*args: SimpleNamespace, angle: float, axis: str, system: Union[Opts, None] = None) -> List[SimpleNamespace]: """ Rotates the corresponding gradient(s) about the given axis by the specified amount. Gradients parallel to the rotation axis and non-gradient(s) are not affected. Possible rotation axes are 'x', 'y' or 'z'. @@ -43,8 +38,8 @@ def rotate( """ if system is None: system = Opts.default - - axes = ["x", "y", "z"] + + axes = ['x', 'y', 'z'] # Cycle through the objects and rotate gradients non-parallel to the given rotation axis. Rotated gradients # assigned to the same axis are then added together. @@ -57,12 +52,12 @@ def rotate( axes.remove(axis) axes_to_rotate = axes if len(axes_to_rotate) != 2: - raise ValueError("Incorrect axes specification.") + raise ValueError('Incorrect axes specification.') for i in range(len(args)): event = args[i] - if (event.type != "grad" and event.type != "trap") or event.channel == axis: + if (event.type != 'grad' and event.type != 'trap') or event.channel == axis: i_bypass.append(i) else: if event.channel == axes_to_rotate[0]: diff --git a/pypulseq/scale_grad.py b/pypulseq/scale_grad.py index 2977111..37af6e2 100644 --- a/pypulseq/scale_grad.py +++ b/pypulseq/scale_grad.py @@ -20,7 +20,7 @@ def scale_grad(grad: SimpleNamespace, scale: float) -> SimpleNamespace: """ # copy() to emulate pass-by-value; otherwise passed grad event is modified scaled_grad = copy(grad) - if scaled_grad.type == "trap": + if scaled_grad.type == 'trap': scaled_grad.amplitude = scaled_grad.amplitude * scale scaled_grad.area = scaled_grad.area * scale scaled_grad.flat_area = scaled_grad.flat_area * scale @@ -29,7 +29,7 @@ def scale_grad(grad: SimpleNamespace, scale: float) -> SimpleNamespace: scaled_grad.first = scaled_grad.first * scale scaled_grad.last = scaled_grad.last * scale - if hasattr(scaled_grad, "id"): - delattr(scaled_grad, "id") + if hasattr(scaled_grad, 'id'): + delattr(scaled_grad, 'id') return scaled_grad diff --git a/pypulseq/seq_examples/notebooks/write_t2_se.ipynb b/pypulseq/seq_examples/notebooks/write_t2_se.ipynb index f22182a..440d9dc 100644 --- a/pypulseq/seq_examples/notebooks/write_t2_se.ipynb +++ b/pypulseq/seq_examples/notebooks/write_t2_se.ipynb @@ -126,9 +126,15 @@ }, "outputs": [], "source": [ - "system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', \n", - " grad_raster_time=10e-6, rf_ringdown_time=10e-6, \n", - " rf_dead_time=100e-6)\n", + "system = Opts(\n", + " max_grad=32,\n", + " grad_unit='mT/m',\n", + " max_slew=130,\n", + " slew_unit='T/m/s',\n", + " grad_raster_time=10e-6,\n", + " rf_ringdown_time=10e-6,\n", + " rf_dead_time=100e-6,\n", + ")\n", "seq = Sequence(system)" ] }, @@ -181,14 +187,25 @@ "source": [ "flip90 = round(rf_flip * pi / 180, 3)\n", "flip180 = 180 * pi / 180\n", - "rf90, gz90, _ = make_sinc_pulse(flip_angle=flip90, system=system, duration=4e-3, \n", - " slice_thickness=slice_thickness, apodization=0.5, \n", - " time_bw_product=4, return_gz = True)\n", - "rf180, gz180, _ = make_sinc_pulse(flip_angle=flip180, system=system, \n", - " duration=2.5e-3, \n", - " slice_thickness=slice_thickness, \n", - " apodization=0.5, \n", - " time_bw_product=4, phase_offset=90 * pi/180, return_gz = True)" + "rf90, gz90, _ = make_sinc_pulse(\n", + " flip_angle=flip90,\n", + " system=system,\n", + " duration=4e-3,\n", + " slice_thickness=slice_thickness,\n", + " apodization=0.5,\n", + " time_bw_product=4,\n", + " return_gz=True,\n", + ")\n", + "rf180, gz180, _ = make_sinc_pulse(\n", + " flip_angle=flip180,\n", + " system=system,\n", + " duration=2.5e-3,\n", + " slice_thickness=slice_thickness,\n", + " apodization=0.5,\n", + " time_bw_product=4,\n", + " phase_offset=90 * pi / 180,\n", + " return_gz=True,\n", + ")" ] }, { @@ -214,8 +231,7 @@ "source": [ "delta_k = 1 / fov\n", "k_width = Nx * delta_k\n", - "gx = make_trapezoid(channel='x', system=system, flat_area=k_width, \n", - " flat_time=readout_time)\n", + "gx = make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time)\n", "adc = make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time)" ] }, @@ -239,13 +255,10 @@ }, "outputs": [], "source": [ - "phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_k\n", - "gz_reph = make_trapezoid(channel='z', system=system, area=-gz90.area / 2,\n", - " duration=2.5e-3)\n", - "gx_pre = make_trapezoid(channel='x', system=system, flat_area=k_width / 2, \n", - " flat_time=readout_time / 2)\n", - "gy_pre = make_trapezoid(channel='y', system=system, area=phase_areas[-1], \n", - " duration=2e-3)" + "phase_areas = (np.arrange(Ny) - (Ny / 2)) * delta_k\n", + "gz_reph = make_trapezoid(channel='z', system=system, area=-gz90.area / 2, duration=2.5e-3)\n", + "gx_pre = make_trapezoid(channel='x', system=system, flat_area=k_width / 2, flat_time=readout_time / 2)\n", + "gy_pre = make_trapezoid(channel='y', system=system, area=phase_areas[-1], duration=2e-3)" ] }, { @@ -268,8 +281,7 @@ }, "outputs": [], "source": [ - "gz_spoil = make_trapezoid(channel='z', system=system, area=gz90.area * 4,\n", - " duration=pre_time * 4)" + "gz_spoil = make_trapezoid(channel='z', system=system, area=gz90.area * 4, duration=pre_time * 4)" ] }, { @@ -333,29 +345,27 @@ "z = np.linspace((-delta_z / 2), (delta_z / 2), n_slices) + rf_offset\n", "\n", "for k in range(nsa): # Averages\n", - " for j in range(n_slices): # Slices\n", - " # Apply RF offsets\n", - " freq_offset = gz90.amplitude * z[j]\n", - " rf90.freq_offset = freq_offset\n", + " for j in range(n_slices): # Slices\n", + " # Apply RF offsets\n", + " freq_offset = gz90.amplitude * z[j]\n", + " rf90.freq_offset = freq_offset\n", "\n", - " freq_offset = gz180.amplitude * z[j]\n", - " rf180.freq_offset = freq_offset\n", + " freq_offset = gz180.amplitude * z[j]\n", + " rf180.freq_offset = freq_offset\n", "\n", - " for i in range(Ny): # Phase encodes\n", - " seq.add_block(rf90, gz90)\n", - " gy_pre = make_trapezoid(channel='y', system=system, \n", - " area=phase_areas[-i -1], duration=2e-3)\n", - " seq.add_block(gx_pre, gy_pre, gz_reph)\n", - " seq.add_block(delay1)\n", - " seq.add_block(gz_spoil)\n", - " seq.add_block(rf180, gz180)\n", - " seq.add_block(gz_spoil)\n", - " seq.add_block(delay2)\n", - " seq.add_block(gx, adc)\n", - " gy_pre = make_trapezoid(channel='y', system=system, \n", - " area=-phase_areas[-j -1], duration=2e-3)\n", - " seq.add_block(gy_pre, gz_spoil)\n", - " seq.add_block(delay_TR)" + " for i in range(Ny): # Phase encodes\n", + " seq.add_block(rf90, gz90)\n", + " gy_pre = make_trapezoid(channel='y', system=system, area=phase_areas[-i - 1], duration=2e-3)\n", + " seq.add_block(gx_pre, gy_pre, gz_reph)\n", + " seq.add_block(delay1)\n", + " seq.add_block(gz_spoil)\n", + " seq.add_block(rf180, gz180)\n", + " seq.add_block(gz_spoil)\n", + " seq.add_block(delay2)\n", + " seq.add_block(gx, adc)\n", + " gy_pre = make_trapezoid(channel='y', system=system, area=-phase_areas[-j - 1], duration=2e-3)\n", + " seq.add_block(gy_pre, gz_spoil)\n", + " seq.add_block(delay_TR)" ] }, { diff --git a/pypulseq/seq_examples/scripts/demo_read.py b/pypulseq/seq_examples/scripts/demo_read.py index 6aeb21d..531a5f4 100644 --- a/pypulseq/seq_examples/scripts/demo_read.py +++ b/pypulseq/seq_examples/scripts/demo_read.py @@ -6,20 +6,18 @@ """ Read a sequence into MATLAB. The `Sequence` class provides an implementation of the _open file format_ for MR sequences described here: http://pulseq.github.io/specification.pdf. This example demonstrates parsing an MRI sequence stored in -this format, accessing sequence parameters and visualising the sequence. +this format, accessing sequence parameters and visualizing the sequence. """ # Read a sequence file - a sequence can be loaded from the open MR file format using the `read` method. -seq_name = "epi_rs.seq" +seq_name = 'epi_rs.seq' -system = pp.Opts( - B0=2.89 -) # Need system here if we want 'detectRFuse' to detect fat-sat pulses +system = pp.Opts(B0=2.89) # Need system here if we want 'detectRFuse' to detect fat-sat pulses seq = pp.Sequence(system) seq.read(seq_name, detect_rf_use=True) # Sanity check to see if the reading and writing are consistent -seq.write("read_test.seq") +seq.write('read_test.seq') # os_system(f'diff -s -u {seq_name} read_test.seq -echo') # Linux only """ @@ -27,7 +25,7 @@ are accessed with the `get_definition()` method. These are user-specified definitions and do not effect the execution of the sequence. """ -seq_name = seq.get_definition("Name") +seq_name = seq.get_definition('Name') # Calculate and display real TE, TR as well as slew rates and gradient amplitudes test_report = seq.test_report() @@ -43,12 +41,12 @@ plt.subplot(211) plt.plot(rf.t, np.abs(rf.signal)) -plt.ylabel("RF magnitude") +plt.ylabel('RF magnitude') plt.subplot(212) plt.plot(1e3 * rf.t, np.angle(rf.signal)) -plt.xlabel("t (ms)") -plt.ylabel("RF phase") +plt.xlabel('t (ms)') +plt.ylabel('RF phase') # The next three blocks contain: three gradient events; a delay; and readout gradient with ADC event, each with # corresponding fields defining the details of the events. @@ -56,13 +54,13 @@ b3 = seq.get_block(3) b4 = seq.get_block(4) -# Plot the sequence. Visualise the sequence using the `plot()` method of the class. This creates a new figure and shows +# Plot the sequence. Visualize the sequence using the `plot()` method of the class. This creates a new figure and shows # ADC, RF and gradient events. The axes are linked so zooming is consistent. In this example, a simple gradient echo # sequence for MRI is displayed. # seq.plot() """ -The details of individual pulses are not well-represented when the entire sequence is visualised. Interactive zooming +The details of individual pulses are not well-represented when the entire sequence is visualized. Interactive zooming is helpful here. Alternatively, a time range can be specified. An additional parameter also allows the display units to be changed for easy reading. Further, the handle of the created figure can be returned if required. """ @@ -75,13 +73,13 @@ """ rf2 = rf duration = rf2.t[-1] -t = rf2.t - duration / 2 # Centre time about 0 +t = rf2.t - duration / 2 # Center time about 0 alpha = 0.5 BW = 4 / duration # Time bandwidth product = 4 window = 1.0 - alpha + alpha * np.cos(2 * np.pi * t / duration) # Hamming window signal = window * np.sinc(BW * t) -# Normalise area to achieve 2*pi rotation +# Normalize area to achieve 2*pi rotation signal = signal / (seq.rf_raster_time * np.sum(np.real(signal))) # Scale to 45 degree flip angle @@ -91,7 +89,7 @@ seq.set_block(1, b1) # Second check to see what has changed -seq.write("read_test2.seq") +seq.write('read_test2.seq') # os_system(f'diff -s -u {seq_name} read_test2.seq -echo') # Linux only # The amplitude of the first rf pulse is reduced due to the reduced flip-angle. Notice the reduction is not exactly a diff --git a/pypulseq/seq_examples/scripts/pns_test.py b/pypulseq/seq_examples/scripts/pns_test.py index bd07509..90579d3 100644 --- a/pypulseq/seq_examples/scripts/pns_test.py +++ b/pypulseq/seq_examples/scripts/pns_test.py @@ -5,14 +5,18 @@ from copy import copy # Set system limits -sys = pp.Opts(max_grad=32, grad_unit='mT/m', - max_slew=130, slew_unit='T/m/s', - rf_ringdown_time=20e-6, - rf_dead_time=100e-6, - adc_dead_time=20e-6, - B0=2.89) - -seq = pp.Sequence() # Create a new sequence object +sys = pp.Opts( + max_grad=32, + grad_unit='mT/m', + max_slew=130, + slew_unit='T/m/s', + rf_ringdown_time=20e-6, + rf_dead_time=100e-6, + adc_dead_time=20e-6, + B0=2.89, +) + +seq = pp.Sequence() # Create a new sequence object ## prepare test objects # pns is induced by the ramps, so we use long gradients to isolate the @@ -20,30 +24,34 @@ gpt = 10e-3 delay = 30e-3 rt_min = sys.grad_raster_time -rt_test = np.floor(sys.max_grad/sys.max_slew/sys.grad_raster_time)*sys.grad_raster_time -ga_min = sys.max_slew*rt_min -ga_test = sys.max_slew*rt_test +rt_test = np.floor(sys.max_grad / sys.max_slew / sys.grad_raster_time) * sys.grad_raster_time +ga_min = sys.max_slew * rt_min +ga_test = sys.max_slew * rt_test -gx_min = pp.make_trapezoid(channel='x',system=sys,amplitude=ga_min,rise_time=rt_min,fall_time=2*rt_min,flat_time=gpt) +gx_min = pp.make_trapezoid( + channel='x', system=sys, amplitude=ga_min, rise_time=rt_min, fall_time=2 * rt_min, flat_time=gpt +) gy_min = copy(gx_min) gy_min.channel = 'y' gz_min = copy(gx_min) gz_min.channel = 'z' -gx_test = pp.make_trapezoid(channel='x',system=sys,amplitude=ga_test,rise_time=rt_test,fall_time=2*rt_test,flat_time=gpt) +gx_test = pp.make_trapezoid( + channel='x', system=sys, amplitude=ga_test, rise_time=rt_test, fall_time=2 * rt_test, flat_time=gpt +) gy_test = copy(gx_test) gy_test.channel = 'y' gz_test = copy(gx_test) gz_test.channel = 'z' -g_min = [gx_min,gy_min,gz_min] -g_test = [gx_test,gy_test,gz_test] +g_min = [gx_min, gy_min, gz_min] +g_test = [gx_test, gy_test, gz_test] # dummy FID sequence -# Create non-selective pulse -rf = pp.make_block_pulse(np.pi/2,duration=0.1e-3, system=sys) +# Create non-selective pulse +rf = pp.make_block_pulse(np.pi / 2, duration=0.1e-3, system=sys) # Define delays and ADC events -adc = pp.make_adc(512,duration=6.4e-3, system=sys) +adc = pp.make_adc(512, duration=6.4e-3, system=sys) ## Define sequence blocks @@ -53,10 +61,10 @@ seq.add_block(pp.make_delay(delay)) seq.add_block(g_test[a]) seq.add_block(pp.make_delay(delay)) - for b in range(a+1, 3): - seq.add_block(g_min[a],g_min[b]) + for b in range(a + 1, 3): + seq.add_block(g_min[a], g_min[b]) seq.add_block(pp.make_delay(delay)) - seq.add_block(g_test[a],g_test[b]) + seq.add_block(g_test[a], g_test[b]) seq.add_block(pp.make_delay(delay)) seq.add_block(*g_min) @@ -70,7 +78,7 @@ ## check whether the timing of the sequence is correct ok, error_report = seq.check_timing() -if (ok): +if ok: print('Timing check passed successfully') else: print('Timing check failed! Error listing follows:') @@ -79,18 +87,18 @@ ## do some visualizations -seq.plot() # Plot all sequence waveforms +seq.plot() # Plot all sequence waveforms ## 'install' to the IDEA simulator # seq.write('idea/external.seq') ## PNS calc -pns_ok, pns_n, pns_c, tpns = seq.calculate_pns(safe_example_hw(), do_plots=True) # Safe example HW +pns_ok, pns_n, pns_c, tpns = seq.calculate_pns(safe_example_hw(), do_plots=True) # Safe example HW # pns_ok, pns_n, pns_c, tpns = seq.calculate_pns('idea/asc/MP_GPA_K2309_2250V_951A_AS82.asc', do_plots=True) # prisma # pns_ok, pns_n, pns_c, tpns = seq.calculate_pns('idea/asc/MP_GPA_K2309_2250V_951A_GC98SQ.asc', do_plots=True) # aera-xq -# ## load simulation results +# ## load simulation results # #[sll,~,~,vscale]=dsv_read('idea/dsv/prisma_pulseq_SLL.dsv') # #[sll,~,~,vscale]=dsv_read('idea/dsv/aera_pulseq_SLL.dsv') @@ -108,4 +116,4 @@ # ssl_e=ssl_s+length(pns_n)-1 # #figureplot(sll(ssl_s:ssl_e))hold on plot(pns_n) # figureplot((sll(ssl_s:ssl_e)-pns_n)./pns_n*100) -# title('relative difference in #') \ No newline at end of file +# title('relative difference in #') diff --git a/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py b/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py index 90c820e..62aab9d 100644 --- a/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py +++ b/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py @@ -10,9 +10,9 @@ system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', grad_raster_time=10e-6, rf_ringdown_time=10e-6, rf_dead_time=100e-6, @@ -42,9 +42,7 @@ ) flip90 = 90 * pi / 180 -rf90 = pp.make_block_pulse( - flip_angle=flip90, system=system, duration=500e-6, time_bw_product=4 -) +rf90 = pp.make_block_pulse(flip_angle=flip90, system=system, duration=500e-6, time_bw_product=4) # ========= # Readout @@ -52,56 +50,36 @@ delta_k = 1 / fov k_width = Nx * delta_k readout_time = 6.4e-3 -gx = pp.make_trapezoid( - channel="x", system=system, flat_area=k_width, flat_time=readout_time -) +gx = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time) adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time) # ========= # Prephase and Rephase # ========= phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_k -gy_pre = pp.make_trapezoid( - channel="y", system=system, area=phase_areas[-1], duration=2e-3 -) +gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[-1], duration=2e-3) -gx_pre = pp.make_trapezoid(channel="x", system=system, area=-gx.area / 2, duration=2e-3) +gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=2e-3) -gz_reph = pp.make_trapezoid( - channel="z", system=system, area=-gz.area / 2, duration=2e-3 -) +gz_reph = pp.make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=2e-3) # ========= # Spoilers # ========= pre_time = 8e-4 -gx_spoil = pp.make_trapezoid( - channel="x", system=system, area=gz.area * 4, duration=pre_time * 4 -) -gy_spoil = pp.make_trapezoid( - channel="y", system=system, area=gz.area * 4, duration=pre_time * 4 -) -gz_spoil = pp.make_trapezoid( - channel="z", system=system, area=gz.area * 4, duration=pre_time * 4 -) +gx_spoil = pp.make_trapezoid(channel='x', system=system, area=gz.area * 4, duration=pre_time * 4) +gy_spoil = pp.make_trapezoid(channel='y', system=system, area=gz.area * 4, duration=pre_time * 4) +gz_spoil = pp.make_trapezoid(channel='z', system=system, area=gz.area * 4, duration=pre_time * 4) # ========= # Delays # ========= TE, TI, TR = 13e-3, 140e-3, 65e-3 -delay_TE = ( - TE - pp.calc_duration(rf) / 2 - pp.calc_duration(gy_pre) - pp.calc_duration(gx) / 2 -) +delay_TE = TE - pp.calc_duration(rf) / 2 - pp.calc_duration(gy_pre) - pp.calc_duration(gx) / 2 delay_TE = pp.make_delay(delay_TE) delay_TI = TI - pp.calc_duration(rf90) / 2 - pp.calc_duration(gx_spoil) delay_TI = pp.make_delay(delay_TI) -delay_TR = ( - TR - - pp.calc_duration(rf) / 2 - - pp.calc_duration(gx) / 2 - - pp.calc_duration(gy_pre) - - TE -) +delay_TR = TR - pp.calc_duration(rf) / 2 - pp.calc_duration(gx) / 2 - pp.calc_duration(gy_pre) - TE delay_TR = pp.make_delay(delay_TR) for j in range(n_slices): @@ -113,17 +91,13 @@ seq.add_block(gx_spoil, gy_spoil, gz_spoil) seq.add_block(delay_TI) seq.add_block(rf, gz) - gy_pre = pp.make_trapezoid( - channel="y", system=system, area=phase_areas[i], duration=2e-3 - ) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[i], duration=2e-3) seq.add_block(gx_pre, gy_pre, gz_reph) seq.add_block(delay_TE) seq.add_block(gx, adc) - gy_pre = pp.make_trapezoid( - channel="y", system=system, area=-phase_areas[i], duration=2e-3 - ) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=-phase_areas[i], duration=2e-3) seq.add_block(gx_spoil, gy_pre) seq.add_block(delay_TR) -seq.set_definition(key="Name", value="2D T1 MPRAGE") -seq.write("2d_mprage_pypulseq.seq") +seq.set_definition(key='Name', value='2D T1 MPRAGE') +seq.write('2d_mprage_pypulseq.seq') diff --git a/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py b/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py index 73e32a4..392a692 100644 --- a/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py +++ b/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py @@ -10,9 +10,9 @@ system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', grad_raster_time=10e-6, rf_ringdown_time=10e-6, rf_dead_time=100e-6, @@ -28,14 +28,10 @@ # RF preparatory, excitation # ========= flip_exc = 12 * pi / 180 -rf = pp.make_block_pulse( - flip_angle=flip_exc, system=system, duration=250e-6, time_bw_product=4 -) +rf = pp.make_block_pulse(flip_angle=flip_exc, system=system, duration=250e-6, time_bw_product=4) flip_prep = 90 * pi / 180 -rf_prep = pp.make_block_pulse( - flip_angle=flip_prep, system=system, duration=500e-6, time_bw_product=4 -) +rf_prep = pp.make_block_pulse(flip_angle=flip_prep, system=system, duration=500e-6, time_bw_product=4) # ========= # Readout @@ -43,9 +39,7 @@ delta_k = 1 / fov k_width = Nx * delta_k readout_time = 3.5e-3 -gx = pp.make_trapezoid( - channel="x", system=system, flat_area=k_width, flat_time=readout_time -) +gx = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time) adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time) # ========= @@ -55,29 +49,27 @@ phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_k slice_areas = (np.arange(Nz) - (Nz / 2)) * delta_kz -gx_pre = pp.make_trapezoid(channel="x", system=system, area=-gx.area / 2, duration=2e-3) -gy_pre = pp.make_trapezoid( - channel="y", system=system, area=phase_areas[-1], duration=2e-3 -) +gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=2e-3) +gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[-1], duration=2e-3) # ========= # Spoilers # ========= pre_time = 6.4e-4 gx_spoil = pp.make_trapezoid( - channel="x", + channel='x', system=system, area=(4 * np.pi) / (42.576e6 * delta_k * 1e-3) * 42.576e6, duration=pre_time * 6, ) gy_spoil = pp.make_trapezoid( - channel="y", + channel='y', system=system, area=(4 * np.pi) / (42.576e6 * delta_k * 1e-3) * 42.576e6, duration=pre_time * 6, ) gz_spoil = pp.make_trapezoid( - channel="z", + channel='z', system=system, area=(4 * np.pi) / (42.576e6 * delta_kz * 1e-3) * 42.576e6, duration=pre_time * 6, @@ -86,9 +78,7 @@ # ========= # Extended trapezoids: gx, gx_spoil # ========= -t_gx_extended = np.array( - [0, gx.rise_time, gx.flat_time, (gx.rise_time * 2) + gx.flat_time + gx.fall_time] -) +t_gx_extended = np.array([0, gx.rise_time, gx.flat_time, (gx.rise_time * 2) + gx.flat_time + gx.fall_time]) amp_gx_extended = np.array([0, gx.amplitude, gx.amplitude, gx_spoil.amplitude]) t_gx_spoil_extended = np.array( [ @@ -99,45 +89,27 @@ ) amp_gx_spoil_extended = np.array([gx_spoil.amplitude, gx_spoil.amplitude, 0]) -gx_extended = pp.make_extended_trapezoid( - channel="x", times=t_gx_extended, amplitudes=amp_gx_extended -) -gx_spoil_extended = pp.make_extended_trapezoid( - channel="x", times=t_gx_spoil_extended, amplitudes=amp_gx_spoil_extended -) +gx_extended = pp.make_extended_trapezoid(channel='x', times=t_gx_extended, amplitudes=amp_gx_extended) +gx_spoil_extended = pp.make_extended_trapezoid(channel='x', times=t_gx_spoil_extended, amplitudes=amp_gx_spoil_extended) # ========= # Delays # ========= TE, TI, TR, T_recovery = 4e-3, 140e-3, 10e-3, 1e-3 -delay_TE = ( - TE - pp.calc_duration(rf) / 2 - pp.calc_duration(gx_pre) - pp.calc_duration(gx) / 2 -) +delay_TE = TE - pp.calc_duration(rf) / 2 - pp.calc_duration(gx_pre) - pp.calc_duration(gx) / 2 delay_TI = TI - pp.calc_duration(rf_prep) / 2 - pp.calc_duration(gx_spoil) -delay_TR = ( - TR - - pp.calc_duration(rf) - - pp.calc_duration(gx_pre) - - pp.calc_duration(gx) - - pp.calc_duration(gx_spoil) -) +delay_TR = TR - pp.calc_duration(rf) - pp.calc_duration(gx_pre) - pp.calc_duration(gx) - pp.calc_duration(gx_spoil) for i in range(Ny): - gy_pre = pp.make_trapezoid( - channel="y", system=system, area=phase_areas[i], duration=2e-3 - ) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[i], duration=2e-3) seq.add_block(rf_prep) seq.add_block(gx_spoil, gy_spoil, gz_spoil) seq.add_block(pp.make_delay(delay_TI)) for j in range(Nz): - gz_pre = pp.make_trapezoid( - channel="z", system=system, area=slice_areas[j], duration=2e-3 - ) - gz_reph = pp.make_trapezoid( - channel="z", system=system, area=-slice_areas[j], duration=2e-3 - ) + gz_pre = pp.make_trapezoid(channel='z', system=system, area=slice_areas[j], duration=2e-3) + gz_reph = pp.make_trapezoid(channel='z', system=system, area=-slice_areas[j], duration=2e-3) seq.add_block(rf) seq.add_block(gx_pre, gy_pre, gz_pre) @@ -149,7 +121,7 @@ seq.add_block(pp.make_delay(T_recovery)) -seq.set_definition(key="Name", value="3D T1 MPRAGE") +seq.set_definition(key='Name', value='3D T1 MPRAGE') -seq.write("256_3d_t1_mprage_pypulseq.seq") +seq.write('256_3d_t1_mprage_pypulseq.seq') # seq.plot(time_range=(0, TI + TR + 2e-3)) diff --git a/pypulseq/seq_examples/scripts/write_epi.py b/pypulseq/seq_examples/scripts/write_epi.py index ba33be5..fc1fab0 100644 --- a/pypulseq/seq_examples/scripts/write_epi.py +++ b/pypulseq/seq_examples/scripts/write_epi.py @@ -7,7 +7,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "epi_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'epi_pypulseq.seq'): # ====== # SETUP # ====== @@ -22,9 +22,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_pypulseq.seq"): # Set system limits system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, ) @@ -50,7 +50,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_pypulseq.seq"): readout_time = Nx * dwell_time flat_time = np.ceil(readout_time * 1e5) * 1e-5 # round-up to the gradient raster gx = pp.make_trapezoid( - channel="x", + channel='x', system=system, amplitude=k_width / readout_time, flat_time=flat_time, @@ -63,19 +63,13 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_pypulseq.seq"): # Pre-phasing gradients pre_time = 8e-4 - gx_pre = pp.make_trapezoid( - channel="x", system=system, area=-gx.area / 2, duration=pre_time - ) - gz_reph = pp.make_trapezoid( - channel="z", system=system, area=-gz.area / 2, duration=pre_time - ) - gy_pre = pp.make_trapezoid( - channel="y", system=system, area=-Ny / 2 * delta_k, duration=pre_time - ) + gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=pre_time) + gz_reph = pp.make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=pre_time) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=-Ny / 2 * delta_k, duration=pre_time) # Phase blip in the shortest possible time dur = np.ceil(2 * np.sqrt(delta_k / system.max_slew) / 10e-6) * 10e-6 - gy = pp.make_trapezoid(channel="y", system=system, area=delta_k, duration=dur) + gy = pp.make_trapezoid(channel='y', system=system, area=delta_k, duration=dur) # ====== # CONSTRUCT SEQUENCE @@ -92,9 +86,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_pypulseq.seq"): ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed! Error listing follows:") + print('Timing check failed! Error listing follows:') print(error_report) # ====== @@ -110,5 +104,5 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_pypulseq.seq"): seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_epi_label.py b/pypulseq/seq_examples/scripts/write_epi_label.py index 19b8605..4712ebe 100644 --- a/pypulseq/seq_examples/scripts/write_epi_label.py +++ b/pypulseq/seq_examples/scripts/write_epi_label.py @@ -10,7 +10,7 @@ from pypulseq import calc_rf_center -def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'epi_label_pypulseq.seq'): # ====== # SETUP # ====== @@ -26,9 +26,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.se # Set system limits system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, ) @@ -48,7 +48,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.se ) # Define trigger - trig = pp.make_trigger(channel="physio1", duration=2000e-6) + trig = pp.make_trigger(channel='physio1', duration=2000e-6) # Define other gradients and ADC events delta_k = 1 / fov @@ -57,7 +57,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.se readout_time = Nx * dwell_time flat_time = np.ceil(readout_time * 1e5) * 1e-5 # Round-up to the gradient raster gx = pp.make_trapezoid( - channel="x", + channel='x', system=system, amplitude=k_width / readout_time, flat_time=flat_time, @@ -70,28 +70,22 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.se # Pre-phasing gradients pre_time = 8e-4 - gx_pre = pp.make_trapezoid( - channel="x", system=system, area=-gx.area / 2, duration=pre_time - ) - gz_reph = pp.make_trapezoid( - channel="z", system=system, area=-gz.area / 2, duration=pre_time - ) - gy_pre = pp.make_trapezoid( - channel="y", system=system, area=Ny / 2 * delta_k, duration=pre_time - ) + gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=pre_time) + gz_reph = pp.make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=pre_time) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=Ny / 2 * delta_k, duration=pre_time) # Phase blip in the shortest possible time dur = np.ceil(2 * np.sqrt(delta_k / system.max_slew) / 10e-6) * 10e-6 - gy = pp.make_trapezoid(channel="y", system=system, area=-delta_k, duration=dur) + gy = pp.make_trapezoid(channel='y', system=system, area=-delta_k, duration=dur) - gz_spoil = pp.make_trapezoid(channel="z", system=system, area=delta_k * Nx * 4) + gz_spoil = pp.make_trapezoid(channel='z', system=system, area=delta_k * Nx * 4) # ====== # CONSTRUCT SEQUENCE # ====== # Define sequence blocks for r in range(n_reps): - seq.add_block(trig, pp.make_label(type="SET", label="SLC", value=0)) + seq.add_block(trig, pp.make_label(type='SET', label='SLC', value=0)) for s in range(n_slices): rf.freq_offset = gz.amplitude * slice_thickness * (s - (n_slices - 1) / 2) # Compensate for the slide-offset induced phase @@ -100,16 +94,16 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.se seq.add_block( gx_pre, gz_reph, - pp.make_label(type="SET", label="NAV", value=1), - pp.make_label(type="SET", label="LIN", value=np.round(Ny / 2)), + pp.make_label(type='SET', label='NAV', value=1), + pp.make_label(type='SET', label='LIN', value=np.round(Ny / 2)), ) for n in range(navigator): seq.add_block( gx, adc, - pp.make_label(type="SET", label="REV", value=gx.amplitude < 0), - pp.make_label(type="SET", label="SEG", value=gx.amplitude < 0), - pp.make_label(type="SET", label="AVG", value=n + 1 == 3), + pp.make_label(type='SET', label='REV', value=gx.amplitude < 0), + pp.make_label(type='SET', label='SEG', value=gx.amplitude < 0), + pp.make_label(type='SET', label='AVG', value=n + 1 == 3), ) if n + 1 != navigator: # Dummy blip pulse to maintain identical RO gradient timing and the corresponding eddy currents @@ -120,55 +114,53 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_lable_pypulseq.se # Reset lin/nav/avg seq.add_block( gy_pre, - pp.make_label(type="SET", label="LIN", value=0), - pp.make_label(type="SET", label="NAV", value=0), - pp.make_label(type="SET", label="AVG", value=0), + pp.make_label(type='SET', label='LIN', value=0), + pp.make_label(type='SET', label='NAV', value=0), + pp.make_label(type='SET', label='AVG', value=0), ) for i in range(Ny): seq.add_block( - pp.make_label(type="SET", label="REV", value=gx.amplitude < 0), - pp.make_label(type="SET", label="SEG", value=gx.amplitude < 0), + pp.make_label(type='SET', label='REV', value=gx.amplitude < 0), + pp.make_label(type='SET', label='SEG', value=gx.amplitude < 0), ) seq.add_block(gx, adc) # Read one line of k-space # Phase blip - seq.add_block(gy, pp.make_label(type="INC", label="LIN", value=1)) + seq.add_block(gy, pp.make_label(type='INC', label='LIN', value=1)) gx.amplitude = -gx.amplitude # Reverse polarity of read gradient seq.add_block( gz_spoil, pp.make_delay(0.1), - pp.make_label(type="INC", label="SLC", value=1), + pp.make_label(type='INC', label='SLC', value=1), ) if np.remainder(navigator + Ny, 2) != 0: gx.amplitude = -gx.amplitude - seq.add_block(pp.make_label(type="INC", label="REP", value=1)) + seq.add_block(pp.make_label(type='INC', label='REP', value=1)) ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed! Error listing follows:") + print('Timing check failed! Error listing follows:') print(error_report) # ====== # VISUALIZATION # ====== if plot: - seq.plot( - time_range=(0, 0.1), time_disp="ms", label="SEG, LIN, SLC" - ) # Plot sequence waveforms + seq.plot(time_range=(0, 0.1), time_disp='ms', label='SEG, LIN, SLC') # Plot sequence waveforms # ========= # WRITE .SEQ # ========= if write_seq: # Prepare sequence report - seq.set_definition(key="FOV", value=[fov, fov, slice_thickness * n_slices]) - seq.set_definition(key="Name", value="epi_lbl") + seq.set_definition(key='FOV', value=[fov, fov, slice_thickness * n_slices]) + seq.set_definition(key='Name', value='epi_lbl') seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_epi_se.py b/pypulseq/seq_examples/scripts/write_epi_se.py index cb399c6..ce43c1c 100644 --- a/pypulseq/seq_examples/scripts/write_epi_se.py +++ b/pypulseq/seq_examples/scripts/write_epi_se.py @@ -5,7 +5,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'epi_se_pypulseq.seq'): # ====== # SETUP # ====== @@ -17,9 +17,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_pypulseq.seq") # Set system limits system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=20e-6, @@ -43,43 +43,27 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_pypulseq.seq") delta_k = 1 / fov k_width = Nx * delta_k readout_time = 3.2e-4 - gx = pp.make_trapezoid( - channel="x", system=system, flat_area=k_width, flat_time=readout_time - ) - adc = pp.make_adc( - num_samples=Nx, system=system, duration=gx.flat_time, delay=gx.rise_time - ) + gx = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time) + adc = pp.make_adc(num_samples=Nx, system=system, duration=gx.flat_time, delay=gx.rise_time) # Pre-phasing gradients pre_time = 8e-4 - gz_reph = pp.make_trapezoid( - channel="z", system=system, area=-gz.area / 2, duration=pre_time - ) + gz_reph = pp.make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=pre_time) # Do not need minus for in-plane prephasers because of the spin-echo (position reflection in k-space) - gx_pre = pp.make_trapezoid( - channel="x", system=system, area=gx.area / 2 - delta_k / 2, duration=pre_time - ) - gy_pre = pp.make_trapezoid( - channel="y", system=system, area=Ny / 2 * delta_k, duration=pre_time - ) + gx_pre = pp.make_trapezoid(channel='x', system=system, area=gx.area / 2 - delta_k / 2, duration=pre_time) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=Ny / 2 * delta_k, duration=pre_time) # Phase blip in shortest possible time dur = math.ceil(2 * math.sqrt(delta_k / system.max_slew) / 10e-6) * 10e-6 - gy = pp.make_trapezoid(channel="y", system=system, area=delta_k, duration=dur) + gy = pp.make_trapezoid(channel='y', system=system, area=delta_k, duration=dur) # Refocusing pulse with spoiling gradients - rf180 = pp.make_block_pulse( - flip_angle=np.pi, system=system, duration=500e-6, use="refocusing" - ) - gz_spoil = pp.make_trapezoid( - channel="z", system=system, area=gz.area * 2, duration=3 * pre_time - ) + rf180 = pp.make_block_pulse(flip_angle=np.pi, system=system, duration=500e-6, use='refocusing') + gz_spoil = pp.make_trapezoid(channel='z', system=system, area=gz.area * 2, duration=3 * pre_time) # Calculate delay time TE = 60e-3 - duration_to_center = (Nx / 2 + 0.5) * pp.calc_duration( - gx - ) + Ny / 2 * pp.calc_duration(gy) + duration_to_center = (Nx / 2 + 0.5) * pp.calc_duration(gx) + Ny / 2 * pp.calc_duration(gy) rf_center_incl_delay = rf.delay + pp.calc_rf_center(rf)[0] rf180_center_incl_delay = rf180.delay + pp.calc_rf_center(rf180)[0] delay_TE1 = ( @@ -91,11 +75,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_pypulseq.seq") - rf180_center_incl_delay ) delay_TE2 = ( - TE / 2 - - pp.calc_duration(rf180) - + rf180_center_incl_delay - - pp.calc_duration(gz_spoil) - - duration_to_center + TE / 2 - pp.calc_duration(rf180) + rf180_center_incl_delay - pp.calc_duration(gz_spoil) - duration_to_center ) # ====== @@ -117,9 +97,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_pypulseq.seq") ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed! Error listing follows:") + print('Timing check failed! Error listing follows:') print(error_report) # ====== @@ -135,5 +115,5 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_pypulseq.seq") seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_epi_se_rs.py b/pypulseq/seq_examples/scripts/write_epi_se_rs.py index 153cd6f..d0e7b94 100644 --- a/pypulseq/seq_examples/scripts/write_epi_se_rs.py +++ b/pypulseq/seq_examples/scripts/write_epi_se_rs.py @@ -1,7 +1,8 @@ """ -This is an experimental high-performance EPI sequence which uses split gradients to overlap blips with the readout +This is an experimental high-performance EPI sequence which uses split gradients to overlap blips with the readout gradients combined with ramp-sampling. """ + import math import numpy as np @@ -9,7 +10,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'epi_se_rs_pypulseq.seq'): # ====== # SETUP # ====== @@ -34,9 +35,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se # Set system limits system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, ) @@ -55,9 +56,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se bandwidth=np.abs(sat_freq), freq_offset=sat_freq, ) - gz_fs = pp.make_trapezoid( - channel="z", system=system, delay=pp.calc_duration(rf_fs), area=1 / 1e-4 - ) + gz_fs = pp.make_trapezoid(channel='z', system=system, delay=pp.calc_duration(rf_fs), area=1 / 1e-4) # Create 90 degree slice selection pulse and gradient rf, gz, gz_reph = pp.make_sinc_pulse( @@ -79,18 +78,18 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se apodization=0.5, time_bw_product=4, phase_offset=np.pi / 2, - use="refocusing", + use='refocusing', return_gz=True, ) _, gzr1_t, gzr1_a = pp.make_extended_trapezoid_area( - channel="z", + channel='z', grad_start=0, grad_end=gz180.amplitude, area=spoil_factor * gz.area, system=system, ) _, gzr2_t, gzr2_a = pp.make_extended_trapezoid_area( - channel="z", + channel='z', grad_start=gz180.amplitude, grad_end=0, area=-gz_reph.area + spoil_factor * gz.area, @@ -101,14 +100,14 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se else: rf180.delay += (gzr1_t[3] - gz180.rise_time) - gz180.delay gz180n = pp.make_extended_trapezoid( - channel="z", + channel='z', system=system, times=np.array([*gzr1_t, *gzr1_t[3] + gz180.flat_time + gzr2_t]) + gz180.delay, amplitudes=np.array([*gzr1_a, *gzr2_a]), ) # Define the output trigger to play out with every slice excitation - trig = pp.make_digital_output_pulse(channel="osc0", duration=100e-6) + trig = pp.make_digital_output_pulse(channel='osc0', duration=100e-6) # Define other gradients and ADC events delta_k = 1 / fov @@ -116,31 +115,22 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se # Phase blip in shortest possible time # Round up the duration to 2x gradient raster time - blip_duration = ( - np.ceil(2 * np.sqrt(delta_k / system.max_slew) / 10e-6 / 2) * 10e-6 * 2 - ) + blip_duration = np.ceil(2 * np.sqrt(delta_k / system.max_slew) / 10e-6 / 2) * 10e-6 * 2 # Use negative blips to save one k-space line on our way to center of k-space - gy = pp.make_trapezoid( - channel="y", system=system, area=-delta_k, duration=blip_duration - ) + gy = pp.make_trapezoid(channel='y', system=system, area=-delta_k, duration=blip_duration) # Readout gradient is a truncated trapezoid with dead times at the beginning and at the end each equal to a half of # blip duration. The area between the blips should be defined by k_width. We do a two-step calculation: we first # increase the area assuming maximum slew rate and then scale down the amplitude to fix the area extra_area = blip_duration / 2 * blip_duration / 2 * system.max_slew gx = pp.make_trapezoid( - channel="x", + channel='x', system=system, area=k_width + extra_area, duration=readout_time + blip_duration, ) - actual_area = ( - gx.area - - gx.amplitude / gx.rise_time * blip_duration / 2 * blip_duration / 2 / 2 - ) - actual_area -= ( - gx.amplitude / gx.fall_time * blip_duration / 2 * blip_duration / 2 / 2 - ) + actual_area = gx.area - gx.amplitude / gx.rise_time * blip_duration / 2 * blip_duration / 2 / 2 + actual_area -= gx.amplitude / gx.fall_time * blip_duration / 2 * blip_duration / 2 / 2 gx.amplitude = gx.amplitude / actual_area * k_width gx.area = gx.amplitude * (gx.flat_time + gx.rise_time / 2 + gx.fall_time / 2) gx.flat_area = gx.amplitude * gx.flat_time @@ -163,9 +153,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se # real hardware this misalignment is much stronger anyways due to the gradient delays # Split the blip into two halves and produce a combined synthetic gradient - gy_parts = pp.split_gradient_at( - grad=gy, time_point=blip_duration / 2, system=system - ) + gy_parts = pp.split_gradient_at(grad=gy, time_point=blip_duration / 2, system=system) gy_blipup, gy_blipdown, _ = pp.align(right=gy_parts[0], left=[gy_parts[1], gx]) gy_blipdownup = pp.add_gradients((gy_blipdown, gy_blipup), system=system) @@ -182,14 +170,12 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se Ny_meas = Ny_pre + Ny_post # Pre-phasing gradients - gx_pre = pp.make_trapezoid(channel="x", system=system, area=-gx.area / 2) - gy_pre = pp.make_trapezoid(channel="y", system=system, area=Ny_pre * delta_k) + gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2) + gy_pre = pp.make_trapezoid(channel='y', system=system, area=Ny_pre * delta_k) gx_pre, gy_pre = pp.align(right=gx_pre, left=gy_pre) # Relax the PE prephaser to reduce stimulation - gy_pre = pp.make_trapezoid( - "y", system=system, area=gy_pre.area, duration=pp.calc_duration(gx_pre, gy_pre) - ) + gy_pre = pp.make_trapezoid('y', system=system, area=gy_pre.area, duration=pp.calc_duration(gx_pre, gy_pre)) gy_pre.amplitude = gy_pre.amplitude * pe_enable # Calculate delay times @@ -198,24 +184,14 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se rf180_center_incl_delay = rf180.delay + pp.calc_rf_center(rf180)[0] delay_TE1 = ( math.ceil( - ( - TE / 2 - - pp.calc_duration(rf, gz) - + rf_center_incl_delay - - rf180_center_incl_delay - ) + (TE / 2 - pp.calc_duration(rf, gz) + rf_center_incl_delay - rf180_center_incl_delay) / system.grad_raster_time ) * system.grad_raster_time ) delay_TE2 = ( math.ceil( - ( - TE / 2 - - pp.calc_duration(rf180, gz180n) - + rf180_center_incl_delay - - duration_to_center - ) + (TE / 2 - pp.calc_duration(rf180, gz180n) + rf180_center_incl_delay - duration_to_center) / system.grad_raster_time ) * system.grad_raster_time @@ -256,9 +232,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se # Check whether the timing of the sequence is correct ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed. Error listing follows:") + print('Timing check failed. Error listing follows:') [print(e) for e in error_report] # Very optional slow step, but useful for testing during development e.g. for the real TE, TR or for staying within @@ -277,11 +253,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "epi_se_rs_pypulseq.se # ========= if write_seq: # Prepare the sequence output for the scanner - seq.set_definition(key="FOV", value=[fov, fov, slice_thickness]) - seq.set_definition(key="Name", value="epi") + seq.set_definition(key='FOV', value=[fov, fov, slice_thickness]) + seq.set_definition(key='Name', value='epi') seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_gre.py b/pypulseq/seq_examples/scripts/write_gre.py index 8457485..12e39f7 100644 --- a/pypulseq/seq_examples/scripts/write_gre.py +++ b/pypulseq/seq_examples/scripts/write_gre.py @@ -5,7 +5,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "gre_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'gre_pypulseq.seq'): # ====== # SETUP # ====== @@ -23,9 +23,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_pypulseq.seq"): system = pp.Opts( max_grad=28, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=150, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -45,47 +45,27 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_pypulseq.seq"): ) # Define other gradients and ADC events delta_k = 1 / fov - gx = pp.make_trapezoid( - channel="x", flat_area=Nx * delta_k, flat_time=3.2e-3, system=system - ) - adc = pp.make_adc( - num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system - ) - gx_pre = pp.make_trapezoid( - channel="x", area=-gx.area / 2, duration=1e-3, system=system - ) - gz_reph = pp.make_trapezoid( - channel="z", area=-gz.area / 2, duration=1e-3, system=system - ) + gx = pp.make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=3.2e-3, system=system) + adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system) + gx_pre = pp.make_trapezoid(channel='x', area=-gx.area / 2, duration=1e-3, system=system) + gz_reph = pp.make_trapezoid(channel='z', area=-gz.area / 2, duration=1e-3, system=system) phase_areas = (np.arange(Ny) - Ny / 2) * delta_k # gradient spoiling - gx_spoil = pp.make_trapezoid(channel="x", area=2 * Nx * delta_k, system=system) - gz_spoil = pp.make_trapezoid(channel="z", area=4 / slice_thickness, system=system) + gx_spoil = pp.make_trapezoid(channel='x', area=2 * Nx * delta_k, system=system) + gz_spoil = pp.make_trapezoid(channel='z', area=4 / slice_thickness, system=system) # Calculate timing delay_TE = ( np.ceil( - ( - TE - - pp.calc_duration(gx_pre) - - gz.fall_time - - gz.flat_time / 2 - - pp.calc_duration(gx) / 2 - ) + (TE - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration(gx) / 2) / seq.grad_raster_time ) * seq.grad_raster_time ) delay_TR = ( np.ceil( - ( - TR - - pp.calc_duration(gz) - - pp.calc_duration(gx_pre) - - pp.calc_duration(gx) - - delay_TE - ) + (TR - pp.calc_duration(gz) - pp.calc_duration(gx_pre) - pp.calc_duration(gx) - delay_TE) / seq.grad_raster_time ) * seq.grad_raster_time @@ -109,7 +89,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_pypulseq.seq"): seq.add_block(rf, gz) gy_pre = pp.make_trapezoid( - channel="y", + channel='y', area=phase_areas[i], duration=pp.calc_duration(gx_pre), system=system, @@ -123,9 +103,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_pypulseq.seq"): # Check whether the timing of the sequence is correct ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed. Error listing follows:") + print('Timing check failed. Error listing follows:') [print(e) for e in error_report] # ====== @@ -146,11 +126,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_pypulseq.seq"): # ========= if write_seq: # Prepare the sequence output for the scanner - seq.set_definition(key="FOV", value=[fov, fov, slice_thickness]) - seq.set_definition(key="Name", value="gre") + seq.set_definition(key='FOV', value=[fov, fov, slice_thickness]) + seq.set_definition(key='Name', value='gre') seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=False, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_gre_label.py b/pypulseq/seq_examples/scripts/write_gre_label.py index c801f37..82f9754 100644 --- a/pypulseq/seq_examples/scripts/write_gre_label.py +++ b/pypulseq/seq_examples/scripts/write_gre_label.py @@ -5,7 +5,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'gre_label_pypulseq.seq'): # ====== # SETUP # ====== @@ -25,9 +25,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.se # Set system limits system = pp.Opts( max_grad=28, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=150, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -49,47 +49,27 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.se # Define other gradients and ADC events delta_k = 1 / fov - gx = pp.make_trapezoid( - channel="x", flat_area=Nx * delta_k, flat_time=ro_duration, system=system - ) - adc = pp.make_adc( - num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system - ) - gx_pre = pp.make_trapezoid( - channel="x", area=-gx.area / 2, duration=1e-3, system=system - ) - gz_reph = pp.make_trapezoid( - channel="z", area=-gz.area / 2, duration=1e-3, system=system - ) + gx = pp.make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=ro_duration, system=system) + adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system) + gx_pre = pp.make_trapezoid(channel='x', area=-gx.area / 2, duration=1e-3, system=system) + gz_reph = pp.make_trapezoid(channel='z', area=-gz.area / 2, duration=1e-3, system=system) phase_areas = -(np.arange(Ny) - Ny / 2) * delta_k # Gradient spoiling - gx_spoil = pp.make_trapezoid(channel="x", area=2 * Nx * delta_k, system=system) - gz_spoil = pp.make_trapezoid(channel="z", area=4 / slice_thickness, system=system) + gx_spoil = pp.make_trapezoid(channel='x', area=2 * Nx * delta_k, system=system) + gz_spoil = pp.make_trapezoid(channel='z', area=4 / slice_thickness, system=system) # Calculate timing delay_TE = ( math.ceil( - ( - TE - - pp.calc_duration(gx_pre) - - gz.fall_time - - gz.flat_time / 2 - - pp.calc_duration(gx) / 2 - ) + (TE - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration(gx) / 2) / seq.grad_raster_time ) * seq.grad_raster_time ) delay_TR = ( math.ceil( - ( - TR - - pp.calc_duration(gz) - - pp.calc_duration(gx_pre) - - pp.calc_duration(gx) - - delay_TE - ) + (TR - pp.calc_duration(gz) - pp.calc_duration(gx_pre) - pp.calc_duration(gx) - delay_TE) / seq.grad_raster_time ) * seq.grad_raster_time @@ -100,7 +80,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.se rf_phase = 0 rf_inc = 0 - seq.add_block(pp.make_label(label="REV", type="SET", value=1)) + seq.add_block(pp.make_label(label='REV', type='SET', value=1)) # ====== # CONSTRUCT SEQUENCE @@ -117,7 +97,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.se seq.add_block(rf, gz) gy_pre = pp.make_trapezoid( - channel="y", + channel='y', area=phase_areas[i], duration=pp.calc_duration(gx_pre), system=system, @@ -128,14 +108,12 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.se gy_pre.amplitude = -gy_pre.amplitude spoil_block_contents = [pp.make_delay(delay_TR), gx_spoil, gy_pre, gz_spoil] if i != Ny - 1: - spoil_block_contents.append( - pp.make_label(type="INC", label="LIN", value=1) - ) + spoil_block_contents.append(pp.make_label(type='INC', label='LIN', value=1)) else: spoil_block_contents.extend( [ - pp.make_label(type="SET", label="LIN", value=0), - pp.make_label(type="INC", label="SLC", value=1), + pp.make_label(type='SET', label='LIN', value=0), + pp.make_label(type='INC', label='SLC', value=1), ] ) seq.add_block(*spoil_block_contents) @@ -143,27 +121,27 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_label_pypulseq.se ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed. Error listing follows:") + print('Timing check failed. Error listing follows:') [print(e) for e in error_report] # ====== # VISUALIZATION # ====== if plot: - seq.plot(label="lin", time_range=np.array([0, 32]) * TR, time_disp="ms") + seq.plot(label='lin', time_range=np.array([0, 32]) * TR, time_disp='ms') # ========= # WRITE .SEQ # ========= if write_seq: # Prepare the sequence output for the scanner - seq.set_definition(key="FOV", value=[fov, fov, slice_thickness * n_slices]) - seq.set_definition(key="Name", value="gre_label") + seq.set_definition(key='FOV', value=[fov, fov, slice_thickness * n_slices]) + seq.set_definition(key='Name', value='gre_label') seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_haste.py b/pypulseq/seq_examples/scripts/write_haste.py index 049ef40..a052cf0 100644 --- a/pypulseq/seq_examples/scripts/write_haste.py +++ b/pypulseq/seq_examples/scripts/write_haste.py @@ -13,7 +13,7 @@ from pypulseq.opts import Opts -def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'haste_pypulseq.seq'): # ====== # SETUP # ====== @@ -22,9 +22,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): # Set system limits system = Opts( max_grad=30, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=170, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=100e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -73,7 +73,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): return_gz=True, ) GS_ex = make_trapezoid( - channel="z", + channel='z', system=system, amplitude=gz.amplitude, flat_time=t_ex_wd, @@ -89,11 +89,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): apodization=0.5, time_bw_product=4, phase_offset=rfref_phase, - use="refocusing", + use='refocusing', return_gz=True, ) GS_ref = make_trapezoid( - channel="z", + channel='z', system=system, amplitude=GS_ex.amplitude, flat_time=tf_ref_wd, @@ -102,32 +102,28 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): AGS_ex = GS_ex.area / 2 GS_spr = make_trapezoid( - channel="z", + channel='z', system=system, area=AGS_ex * (1 + fspS), duration=t_sp, rise_time=dG, ) - GS_spex = make_trapezoid( - channel="z", system=system, area=AGS_ex * fspS, duration=t_sp_ex, rise_time=dG - ) + GS_spex = make_trapezoid(channel='z', system=system, area=AGS_ex * fspS, duration=t_sp_ex, rise_time=dG) delta_k = 1 / fov k_width = Nx * delta_k GR_acq = make_trapezoid( - channel="x", + channel='x', system=system, flat_area=k_width, flat_time=readout_time, rise_time=dG, ) adc = make_adc(num_samples=Nx, duration=sampling_time, delay=system.adc_dead_time) - GR_spr = make_trapezoid( - channel="x", system=system, area=GR_acq.area * fspR, duration=t_sp, rise_time=dG - ) + GR_spr = make_trapezoid(channel='x', system=system, area=GR_acq.area * fspR, duration=t_sp, rise_time=dG) GR_spex = make_trapezoid( - channel="x", + channel='x', system=system, area=GR_acq.area * (1 + fspR), duration=t_sp_ex, @@ -136,9 +132,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): AGR_spr = GR_spr.area AGR_preph = GR_acq.area / 2 + AGR_spr - GR_preph = make_trapezoid( - channel="x", system=system, area=AGR_preph, duration=t_sp_ex, rise_time=dG - ) + GR_preph = make_trapezoid(channel='x', system=system, area=AGR_preph, duration=t_sp_ex, rise_time=dG) n_ex = 1 PE_order = np.arange(-Ny_pre, Ny + 1).T @@ -147,11 +141,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): # Split gradients and recombine into blocks GS1_times = np.array([0, GS_ex.rise_time]) GS1_amp = np.array([0, GS_ex.amplitude]) - GS1 = make_extended_trapezoid(channel="z", times=GS1_times, amplitudes=GS1_amp) + GS1 = make_extended_trapezoid(channel='z', times=GS1_times, amplitudes=GS1_amp) GS2_times = np.array([0, GS_ex.flat_time]) GS2_amp = np.array([GS_ex.amplitude, GS_ex.amplitude]) - GS2 = make_extended_trapezoid(channel="z", times=GS2_times, amplitudes=GS2_amp) + GS2 = make_extended_trapezoid(channel='z', times=GS2_times, amplitudes=GS2_amp) GS3_times = np.array( [ @@ -161,14 +155,12 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): GS_spex.rise_time + GS_spex.flat_time + GS_spex.fall_time, ] ) - GS3_amp = np.array( - [GS_ex.amplitude, GS_spex.amplitude, GS_spex.amplitude, GS_ref.amplitude] - ) - GS3 = make_extended_trapezoid(channel="z", times=GS3_times, amplitudes=GS3_amp) + GS3_amp = np.array([GS_ex.amplitude, GS_spex.amplitude, GS_spex.amplitude, GS_ref.amplitude]) + GS3 = make_extended_trapezoid(channel='z', times=GS3_times, amplitudes=GS3_amp) GS4_times = np.array([0, GS_ref.flat_time]) GS4_amp = np.array([GS_ref.amplitude, GS_ref.amplitude]) - GS4 = make_extended_trapezoid(channel="z", times=GS4_times, amplitudes=GS4_amp) + GS4 = make_extended_trapezoid(channel='z', times=GS4_times, amplitudes=GS4_amp) GS5_times = np.array( [ @@ -179,7 +171,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): ] ) GS5_amp = np.array([GS_ref.amplitude, GS_spr.amplitude, GS_spr.amplitude, 0]) - GS5 = make_extended_trapezoid(channel="z", times=GS5_times, amplitudes=GS5_amp) + GS5 = make_extended_trapezoid(channel='z', times=GS5_times, amplitudes=GS5_amp) GS7_times = np.array( [ @@ -190,7 +182,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): ] ) GS7_amp = np.array([0, GS_spr.amplitude, GS_spr.amplitude, GS_ref.amplitude]) - GS7 = make_extended_trapezoid(channel="z", times=GS7_times, amplitudes=GS7_amp) + GS7 = make_extended_trapezoid(channel='z', times=GS7_times, amplitudes=GS7_amp) # Readout gradient GR3 = GR_preph @@ -204,11 +196,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): ] ) GR5_amp = np.array([0, GR_spr.amplitude, GR_spr.amplitude, GR_acq.amplitude]) - GR5 = make_extended_trapezoid(channel="x", times=GR5_times, amplitudes=GR5_amp) + GR5 = make_extended_trapezoid(channel='x', times=GR5_times, amplitudes=GR5_amp) GR6_times = np.array([0, readout_time]) GR6_amp = np.array([GR_acq.amplitude, GR_acq.amplitude]) - GR6 = make_extended_trapezoid(channel="x", times=GR6_times, amplitudes=GR6_amp) + GR6 = make_extended_trapezoid(channel='x', times=GR6_times, amplitudes=GR6_amp) GR7_times = np.array( [ @@ -219,7 +211,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): ] ) GR7_amp = np.array([GR_acq.amplitude, GR_spr.amplitude, GR_spr.amplitude, 0]) - GR7 = make_extended_trapezoid(channel="x", times=GR7_times, amplitudes=GR7_amp) + GR7 = make_extended_trapezoid(channel='x', times=GR7_times, amplitudes=GR7_amp) # Fill-times tex = GS1.shape_dur + GS2.shape_dur + GS3.shape_dur @@ -231,11 +223,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): TR_fill = system.grad_raster_time * round(TR_fill / system.grad_raster_time) if TR_fill < 0: TR_fill = 1e-3 - warnings.warn( - f"TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms" - ) + warnings.warn(f'TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms') else: - print(f"TR fill: {1000 * TR_fill} ms") + print(f'TR fill: {1000 * TR_fill} ms') delay_TR = make_delay(TR_fill) delay_end = make_delay(5) @@ -245,19 +235,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): # Define sequence blocks for k_ex in range(n_ex): for s in range(n_slices): - rfex.freq_offset = ( - GS_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - ) - rfref.freq_offset = ( - GS_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - ) + rfex.freq_offset = GS_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) + rfref.freq_offset = GS_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) # Align the phase for off-center slices - rfex.phase_offset = ( - rfex_phase - 2 * math.pi * rfex.freq_offset * calc_rf_center(rfex)[0] - ) - rfref.phase_offset = ( - rfref_phase - 2 * math.pi * rfref.freq_offset * calc_rf_center(rfref)[0] - ) + rfex.phase_offset = rfex_phase - 2 * math.pi * rfex.freq_offset * calc_rf_center(rfex)[0] + rfref.phase_offset = rfref_phase - 2 * math.pi * rfref.freq_offset * calc_rf_center(rfref)[0] seq.add_block(GS1) seq.add_block(GS2, rfex) @@ -270,14 +252,14 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): phase_area = 0 GP_pre = make_trapezoid( - channel="y", + channel='y', system=system, area=phase_area, duration=t_sp, rise_time=dG, ) GP_rew = make_trapezoid( - channel="y", + channel='y', system=system, area=-phase_area, duration=t_sp, @@ -303,9 +285,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): # Check whether the timing of the sequence is correct ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed. Error listing follows:") + print('Timing check failed. Error listing follows:') [print(e) for e in error_report] # ====== @@ -322,5 +304,5 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "haste_pypulseq.seq"): # SETUPeq") -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_MPRAGE.py b/pypulseq/seq_examples/scripts/write_mprage.py similarity index 75% rename from pypulseq/seq_examples/scripts/write_MPRAGE.py rename to pypulseq/seq_examples/scripts/write_mprage.py index 5c07bb4..31c0841 100644 --- a/pypulseq/seq_examples/scripts/write_MPRAGE.py +++ b/pypulseq/seq_examples/scripts/write_mprage.py @@ -5,7 +5,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'mprage_pypulseq.seq'): # ====== # SETUP # ====== @@ -14,9 +14,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") # Set system limits system = pp.Opts( max_grad=24, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=100, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -35,29 +35,24 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") fov = np.array([192, 240, 256]) * 1e-3 # Define FOV and resolution N = [192, 240, 256] - ax.d1 = "z" # Fastest dimension (readout) - ax.d2 = "x" # Second-fastest dimension (inner phase-encoding loop) - xyz = ["x", "y", "z"] + ax.d1 = 'z' # Fastest dimension (readout) + ax.d2 = 'x' # Second-fastest dimension (inner phase-encoding loop) + xyz = ['x', 'y', 'z'] ax.d3 = np.setdiff1d(xyz, [ax.d1, ax.d2])[0] ax.n1 = xyz.index(ax.d1) ax.n2 = xyz.index(ax.d2) ax.n3 = xyz.index(ax.d3) # Create alpha-degree hard pulse and gradient - rf = pp.make_block_pulse( - flip_angle=alpha * np.pi / 180, system=system, duration=rf_len - ) - rf180 = pp.make_adiabatic_pulse( - pulse_type="hypsec", system=system, duration=10.24e-3, dwell=1e-5 - ) + rf = pp.make_block_pulse(flip_angle=alpha * np.pi / 180, system=system, duration=rf_len) + rf180 = pp.make_adiabatic_pulse(pulse_type='hypsec', system=system, duration=10.24e-3, dwell=1e-5) # Define other gradients and ADC events deltak = 1 / fov gro = pp.make_trapezoid( channel=ax.d1, amplitude=N[ax.n1] * deltak[ax.n1] / ro_dur, - flat_time=np.ceil((ro_dur + system.adc_dead_time) / system.grad_raster_time) - * system.grad_raster_time, + flat_time=np.ceil((ro_dur + system.adc_dead_time) / system.grad_raster_time) * system.grad_raster_time, system=system, ) adc = pp.make_adc( @@ -69,25 +64,16 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") # First 0.5 is necessary to account for the Siemens sampling in the center of the dwell periods gro_pre = pp.make_trapezoid( channel=ax.d1, - area=-gro.amplitude - * (adc.dwell * (adc.num_samples / 2 + 0.5) + 0.5 * gro.rise_time), + area=-gro.amplitude * (adc.dwell * (adc.num_samples / 2 + 0.5) + 0.5 * gro.rise_time), system=system, ) - gpe1 = pp.make_trapezoid( - channel=ax.d2, area=-deltak[ax.n2] * (N[ax.n2] / 2), system=system - ) # Maximum PE1 gradient - gpe2 = pp.make_trapezoid( - channel=ax.d3, area=-deltak[ax.n3] * (N[ax.n3] / 2), system=system - ) # Maximum PE2 gradient + gpe1 = pp.make_trapezoid(channel=ax.d2, area=-deltak[ax.n2] * (N[ax.n2] / 2), system=system) # Maximum PE1 gradient + gpe2 = pp.make_trapezoid(channel=ax.d3, area=-deltak[ax.n3] * (N[ax.n3] / 2), system=system) # Maximum PE2 gradient # Spoil with 4x cycles per voxel - gsl_sp = pp.make_trapezoid( - channel=ax.d3, area=np.max(deltak * N) * 4, duration=10e-3, system=system - ) + gsl_sp = pp.make_trapezoid(channel=ax.d3, area=np.max(deltak * N) * 4, duration=10e-3, system=system) # We cut the RO gradient into two parts for the optimal spoiler timing - gro1, gro_Sp = pp.split_gradient_at( - grad=gro, time_point=gro.rise_time + gro.flat_time - ) + gro1, gro_Sp = pp.split_gradient_at(grad=gro, time_point=gro.rise_time + gro.flat_time) # Gradient spoiling if ro_spoil > 0: gro_Sp = pp.make_extended_trapezoid_area( @@ -100,7 +86,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") # Calculate timing of the fast loop. We will have two blocks in the inner loop: # 1: spoilers/rewinders + RF - # 2: prewinder, phase enconding + readout + # 2: prewinder, phase encoding + readout rf.delay = pp.calc_duration(gro_Sp, gpe1, gpe2) gro_pre, _, _ = pp.align(right=[gro_pre, gpe1, gpe2]) gro1.delay = pp.calc_duration(gro_pre) @@ -128,9 +114,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") # All LABELS / counters an flags are automatically initialized to 0 in the beginning, no need to define initial 0's # so we will just increment LIN after the ADC event (e.g. during the spoiler) - label_inc_lin = pp.make_label(type="INC", label="LIN", value=1) - label_inc_par = pp.make_label(type="INC", label="PAR", value=1) - label_reset_par = pp.make_label(type="SET", label="PAR", value=0) + label_inc_lin = pp.make_label(type='INC', label='LIN', value=1) + label_inc_par = pp.make_label(type='INC', label='PAR', value=1) + label_reset_par = pp.make_label(type='SET', label='PAR', value=0) # Pre-register objects that do not change while looping result = seq.register_grad_event(gsl_sp) @@ -172,18 +158,14 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") gpe2jr, label_inc_par, ) - seq.add_block( - adc, gro1, pp.scale_grad(grad=gpe1, scale=pe1_steps[i]), gpe2je - ) - seq.add_block( - gro_Sp, pp.make_delay(TR_out_delay), label_reset_par, label_inc_lin - ) + seq.add_block(adc, gro1, pp.scale_grad(grad=gpe1, scale=pe1_steps[i]), gpe2je) + seq.add_block(gro_Sp, pp.make_delay(TR_out_delay), label_reset_par, label_inc_lin) # ====== # VISUALIZATION # ====== if plot: - seq.plot(time_range=[0, TR_out * 2], label="PAR") + seq.plot(time_range=[0, TR_out * 2], label='PAR') # ========= # WRITE .SEQ @@ -192,5 +174,5 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "mprage_pypulseq.seq") seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_radial_gre.py b/pypulseq/seq_examples/scripts/write_radial_gre.py index 8d84294..5cdf67a 100644 --- a/pypulseq/seq_examples/scripts/write_radial_gre.py +++ b/pypulseq/seq_examples/scripts/write_radial_gre.py @@ -3,7 +3,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "gre_radial_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'gre_radial_pypulseq.seq'): # ====== # SETUP # ====== @@ -23,9 +23,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_radial_pypulseq.s # Set system limits system = pp.Opts( max_grad=28, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=120, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -47,45 +47,25 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_radial_pypulseq.s # Define other gradients and ADC events deltak = 1 / fov - gx = pp.make_trapezoid( - channel="x", flat_area=Nx * deltak, flat_time=6.4e-3 / 5, system=system - ) - adc = pp.make_adc( - num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system - ) - gx_pre = pp.make_trapezoid( - channel="x", area=-gx.area / 2 - deltak / 2, duration=2e-3, system=system - ) - gz_reph = pp.make_trapezoid( - channel="z", area=-gz.area / 2, duration=2e-3, system=system - ) + gx = pp.make_trapezoid(channel='x', flat_area=Nx * deltak, flat_time=6.4e-3 / 5, system=system) + adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system) + gx_pre = pp.make_trapezoid(channel='x', area=-gx.area / 2 - deltak / 2, duration=2e-3, system=system) + gz_reph = pp.make_trapezoid(channel='z', area=-gz.area / 2, duration=2e-3, system=system) # Gradient spoiling - gx_spoil = pp.make_trapezoid(channel="x", area=0.5 * Nx * deltak, system=system) - gz_spoil = pp.make_trapezoid(channel="z", area=4 / slice_thickness, system=system) + gx_spoil = pp.make_trapezoid(channel='x', area=0.5 * Nx * deltak, system=system) + gz_spoil = pp.make_trapezoid(channel='z', area=4 / slice_thickness, system=system) # Calculate timing delay_TE = ( np.ceil( - ( - TE - - pp.calc_duration(gx_pre) - - gz.fall_time - - gz.flat_time / 2 - - pp.calc_duration(gx) / 2 - ) + (TE - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration(gx) / 2) / seq.grad_raster_time ) * seq.grad_raster_time ) delay_TR = ( np.ceil( - ( - TR - - pp.calc_duration(gx_pre) - - pp.calc_duration(gz) - - pp.calc_duration(gx) - - delay_TE - ) + (TR - pp.calc_duration(gx_pre) - pp.calc_duration(gz) - pp.calc_duration(gx) - delay_TE) / seq.grad_raster_time ) * seq.grad_raster_time @@ -106,21 +86,19 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_radial_pypulseq.s seq.add_block(rf, gz) phi = delta * (i - 1) - seq.add_block(*pp.rotate(gx_pre, gz_reph, angle=phi, axis="z")) + seq.add_block(*pp.rotate(gx_pre, gz_reph, angle=phi, axis='z')) seq.add_block(pp.make_delay(delay_TE)) if i > 0: - seq.add_block(*pp.rotate(gx, adc, angle=phi, axis="z")) + seq.add_block(*pp.rotate(gx, adc, angle=phi, axis='z')) else: - seq.add_block(*pp.rotate(gx, angle=phi, axis="z")) - seq.add_block( - *pp.rotate(gx_spoil, gz_spoil, pp.make_delay(delay_TR), angle=phi, axis="z") - ) + seq.add_block(*pp.rotate(gx, angle=phi, axis='z')) + seq.add_block(*pp.rotate(gx_spoil, gz_spoil, pp.make_delay(delay_TR), angle=phi, axis='z')) ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed! Error listing follows:") + print('Timing check failed! Error listing follows:') print(error_report) # ====== @@ -133,10 +111,10 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "gre_radial_pypulseq.s # WRITE .SEQ # ========= if write_seq: - seq.set_definition(key="FOV", value=[fov, fov, slice_thickness]) - seq.set_definition(key="Name", value="gre_rad") + seq.set_definition(key='FOV', value=[fov, fov, slice_thickness]) + seq.set_definition(key='Name', value='gre_rad') seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_tse.py b/pypulseq/seq_examples/scripts/write_tse.py index 5ca4adf..aef9a39 100644 --- a/pypulseq/seq_examples/scripts/write_tse.py +++ b/pypulseq/seq_examples/scripts/write_tse.py @@ -6,7 +6,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'tse_pypulseq.seq'): # ====== # SETUP # ====== @@ -15,9 +15,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): # Set system limits system = pp.Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=100e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -64,7 +64,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): return_gz=True, ) gs_ex = pp.make_trapezoid( - channel="z", + channel='z', system=system, amplitude=gz.amplitude, flat_time=t_exwd, @@ -80,11 +80,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): apodization=0.5, time_bw_product=4, phase_offset=rf_ref_phase, - use="refocusing", + use='refocusing', return_gz=True, ) gs_ref = pp.make_trapezoid( - channel="z", + channel='z', system=system, amplitude=gs_ex.amplitude, flat_time=t_refwd, @@ -93,31 +93,27 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): ags_ex = gs_ex.area / 2 gs_spr = pp.make_trapezoid( - channel="z", + channel='z', system=system, area=ags_ex * (1 + fsp_s), duration=t_sp, rise_time=dG, ) - gs_spex = pp.make_trapezoid( - channel="z", system=system, area=ags_ex * fsp_s, duration=t_spex, rise_time=dG - ) + gs_spex = pp.make_trapezoid(channel='z', system=system, area=ags_ex * fsp_s, duration=t_spex, rise_time=dG) delta_k = 1 / fov k_width = Nx * delta_k gr_acq = pp.make_trapezoid( - channel="x", + channel='x', system=system, flat_area=k_width, flat_time=readout_time, rise_time=dG, ) - adc = pp.make_adc( - num_samples=Nx, duration=sampling_time, delay=system.adc_dead_time - ) + adc = pp.make_adc(num_samples=Nx, duration=sampling_time, delay=system.adc_dead_time) gr_spr = pp.make_trapezoid( - channel="x", + channel='x', system=system, area=gr_acq.area * fsp_r, duration=t_sp, @@ -126,26 +122,24 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): agr_spr = gr_spr.area agr_preph = gr_acq.area / 2 + agr_spr - gr_preph = pp.make_trapezoid( - channel="x", system=system, area=agr_preph, duration=t_spex, rise_time=dG - ) + gr_preph = pp.make_trapezoid(channel='x', system=system, area=agr_preph, duration=t_spex, rise_time=dG) # Phase-encoding n_ex = math.floor(Ny / n_echo) pe_steps = np.arange(1, n_echo * n_ex + 1) - 0.5 * n_echo * n_ex - 1 if divmod(n_echo, 2)[1] == 0: pe_steps = np.roll(pe_steps, [0, int(-np.round(n_ex / 2))]) - pe_order = pe_steps.reshape((n_ex, n_echo), order="F").T + pe_order = pe_steps.reshape((n_ex, n_echo), order='F').T phase_areas = pe_order * delta_k # Split gradients and recombine into blocks gs1_times = np.array([0, gs_ex.rise_time]) gs1_amp = np.array([0, gs_ex.amplitude]) - gs1 = pp.make_extended_trapezoid(channel="z", times=gs1_times, amplitudes=gs1_amp) + gs1 = pp.make_extended_trapezoid(channel='z', times=gs1_times, amplitudes=gs1_amp) gs2_times = np.array([0, gs_ex.flat_time]) gs2_amp = np.array([gs_ex.amplitude, gs_ex.amplitude]) - gs2 = pp.make_extended_trapezoid(channel="z", times=gs2_times, amplitudes=gs2_amp) + gs2 = pp.make_extended_trapezoid(channel='z', times=gs2_times, amplitudes=gs2_amp) gs3_times = np.array( [ @@ -155,14 +149,12 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): gs_spex.rise_time + gs_spex.flat_time + gs_spex.fall_time, ] ) - gs3_amp = np.array( - [gs_ex.amplitude, gs_spex.amplitude, gs_spex.amplitude, gs_ref.amplitude] - ) - gs3 = pp.make_extended_trapezoid(channel="z", times=gs3_times, amplitudes=gs3_amp) + gs3_amp = np.array([gs_ex.amplitude, gs_spex.amplitude, gs_spex.amplitude, gs_ref.amplitude]) + gs3 = pp.make_extended_trapezoid(channel='z', times=gs3_times, amplitudes=gs3_amp) gs4_times = np.array([0, gs_ref.flat_time]) gs4_amp = np.array([gs_ref.amplitude, gs_ref.amplitude]) - gs4 = pp.make_extended_trapezoid(channel="z", times=gs4_times, amplitudes=gs4_amp) + gs4 = pp.make_extended_trapezoid(channel='z', times=gs4_times, amplitudes=gs4_amp) gs5_times = np.array( [ @@ -173,7 +165,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): ] ) gs5_amp = np.array([gs_ref.amplitude, gs_spr.amplitude, gs_spr.amplitude, 0]) - gs5 = pp.make_extended_trapezoid(channel="z", times=gs5_times, amplitudes=gs5_amp) + gs5 = pp.make_extended_trapezoid(channel='z', times=gs5_times, amplitudes=gs5_amp) gs7_times = np.array( [ @@ -184,7 +176,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): ] ) gs7_amp = np.array([0, gs_spr.amplitude, gs_spr.amplitude, gs_ref.amplitude]) - gs7 = pp.make_extended_trapezoid(channel="z", times=gs7_times, amplitudes=gs7_amp) + gs7 = pp.make_extended_trapezoid(channel='z', times=gs7_times, amplitudes=gs7_amp) # Readout gradient gr3 = gr_preph @@ -198,11 +190,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): ] ) gr5_amp = np.array([0, gr_spr.amplitude, gr_spr.amplitude, gr_acq.amplitude]) - gr5 = pp.make_extended_trapezoid(channel="x", times=gr5_times, amplitudes=gr5_amp) + gr5 = pp.make_extended_trapezoid(channel='x', times=gr5_times, amplitudes=gr5_amp) gr6_times = np.array([0, readout_time]) gr6_amp = np.array([gr_acq.amplitude, gr_acq.amplitude]) - gr6 = pp.make_extended_trapezoid(channel="x", times=gr6_times, amplitudes=gr6_amp) + gr6 = pp.make_extended_trapezoid(channel='x', times=gr6_times, amplitudes=gr6_amp) gr7_times = np.array( [ @@ -213,16 +205,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): ] ) gr7_amp = np.array([gr_acq.amplitude, gr_spr.amplitude, gr_spr.amplitude, 0]) - gr7 = pp.make_extended_trapezoid(channel="x", times=gr7_times, amplitudes=gr7_amp) + gr7 = pp.make_extended_trapezoid(channel='x', times=gr7_times, amplitudes=gr7_amp) # Fill-times t_ex = pp.calc_duration(gs1) + pp.calc_duration(gs2) + pp.calc_duration(gs3) - t_ref = ( - pp.calc_duration(gs4) - + pp.calc_duration(gs5) - + pp.calc_duration(gs7) - + readout_time - ) + t_ref = pp.calc_duration(gs4) + pp.calc_duration(gs5) + pp.calc_duration(gs7) + readout_time t_end = pp.calc_duration(gs4) + pp.calc_duration(gs5) TE_train = t_ex + n_echo * t_ref + t_end @@ -231,11 +218,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): TR_fill = system.grad_raster_time * np.round(TR_fill / system.grad_raster_time) if TR_fill < 0: TR_fill = 1e-3 - warnings.warn( - f"TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms" - ) + warnings.warn(f'TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms') else: - print(f"TR fill: {1000 * TR_fill} ms") + print(f'TR fill: {1000 * TR_fill} ms') delay_TR = pp.make_delay(TR_fill) # ====== @@ -243,20 +228,10 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): # ====== for k_ex in range(n_ex + 1): for s in range(n_slices): - rf_ex.freq_offset = ( - gs_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - ) - rf_ref.freq_offset = ( - gs_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - ) - rf_ex.phase_offset = ( - rf_ex_phase - - 2 * np.pi * rf_ex.freq_offset * pp.calc_rf_center(rf_ex)[0] - ) - rf_ref.phase_offset = ( - rf_ref_phase - - 2 * np.pi * rf_ref.freq_offset * pp.calc_rf_center(rf_ref)[0] - ) + rf_ex.freq_offset = gs_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) + rf_ref.freq_offset = gs_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) + rf_ex.phase_offset = rf_ex_phase - 2 * np.pi * rf_ex.freq_offset * pp.calc_rf_center(rf_ex)[0] + rf_ref.phase_offset = rf_ref_phase - 2 * np.pi * rf_ref.freq_offset * pp.calc_rf_center(rf_ref)[0] seq.add_block(gs1) seq.add_block(gs2, rf_ex) @@ -269,14 +244,14 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): phase_area = 0.0 # 0.0 and not 0 because -phase_area should successfully result in negative zero gp_pre = pp.make_trapezoid( - channel="y", + channel='y', system=system, area=phase_area, duration=t_sp, rise_time=dG, ) gp_rew = pp.make_trapezoid( - channel="y", + channel='y', system=system, area=-phase_area, duration=t_sp, @@ -300,9 +275,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): error_report, ) = seq.check_timing() # Check whether the timing of the sequence is correct if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed. Error listing follows:") + print('Timing check failed. Error listing follows:') [print(e) for e in error_report] # ====== @@ -318,5 +293,5 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "tse_pypulseq.seq"): seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/seq_examples/scripts/write_ute.py b/pypulseq/seq_examples/scripts/write_ute.py index 57007c4..2f0010b 100644 --- a/pypulseq/seq_examples/scripts/write_ute.py +++ b/pypulseq/seq_examples/scripts/write_ute.py @@ -1,6 +1,7 @@ """ A very basic UTE-like sequence, without ramp-sampling, ramp-RF. Achieves TE in the range of 300-400 us """ + from copy import copy import numpy as np @@ -9,7 +10,7 @@ import pypulseq as pp -def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): +def main(plot: bool, write_seq: bool, seq_filename: str = 'ute_pypulseq.seq'): # ====== # SETUP # ====== @@ -18,7 +19,7 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): Nx = 256 alpha = 10 # Flip angle slice_thickness = 3e-3 # Slice thickness - TR = 10e-3 # Repetition tme + TR = 10e-3 # Repetition time Nr = 128 # Number of radial spokes delta = 2 * np.pi / Nr # Angular increment ro_duration = 2.56e-3 # Read-out time: controls RO bandwidth and T2-blurring @@ -30,9 +31,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): # Set system limits system = pp.Opts( max_grad=28, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=100, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -60,45 +61,29 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): # Define other gradients and ADC events delta_k = 1 / fov / (1 + ro_asymmetry) ro_area = Nx * delta_k - gx = pp.make_trapezoid( - channel="x", flat_area=ro_area, flat_time=ro_duration, system=system - ) - adc = pp.make_adc( - num_samples=Nxo, duration=gx.flat_time, delay=gx.rise_time, system=system - ) + gx = pp.make_trapezoid(channel='x', flat_area=ro_area, flat_time=ro_duration, system=system) + adc = pp.make_adc(num_samples=Nxo, duration=gx.flat_time, delay=gx.rise_time, system=system) gx_pre = pp.make_trapezoid( - channel="x", - area=-(gx.area - ro_area) / 2 - - gx.amplitude * adc.dwell / 2 - - ro_area / 2 * (1 - ro_asymmetry), + channel='x', + area=-(gx.area - ro_area) / 2 - gx.amplitude * adc.dwell / 2 - ro_area / 2 * (1 - ro_asymmetry), system=system, ) # Gradient spoiling - gx_spoil = pp.make_trapezoid(channel="x", area=0.2 * Nx * delta_k, system=system) + gx_spoil = pp.make_trapezoid(channel='x', area=0.2 * Nx * delta_k, system=system) # Calculate timing - TE = ( - gz.fall_time - + pp.calc_duration(gx_pre, gz_reph) - + gx.rise_time - + adc.dwell * Nxo / 2 * (1 - ro_asymmetry) - ) + TE = gz.fall_time + pp.calc_duration(gx_pre, gz_reph) + gx.rise_time + adc.dwell * Nxo / 2 * (1 - ro_asymmetry) delay_TR = ( np.ceil( - ( - TR - - pp.calc_duration(gx_pre, gz_reph) - - pp.calc_duration(gz) - - pp.calc_duration(gx) - ) + (TR - pp.calc_duration(gx_pre, gz_reph) - pp.calc_duration(gz) - pp.calc_duration(gx)) / seq.grad_raster_time ) * seq.grad_raster_time ) assert np.all(delay_TR >= pp.calc_duration(gx_spoil)) - print(f"TE = {TE * 1e6:.0f} us") + print(f'TE = {TE * 1e6:.0f} us') if pp.calc_duration(gz_reph) > pp.calc_duration(gx_pre): gx_pre.delay = pp.calc_duration(gz_reph) - pp.calc_duration(gx_pre) @@ -126,19 +111,19 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): gps = copy(gx_pre) gpc.amplitude = gx_pre.amplitude * np.cos(phi) gps.amplitude = gx_pre.amplitude * np.sin(phi) - gps.channel = "y" + gps.channel = 'y' grc = copy(gx) grs = copy(gx) grc.amplitude = gx.amplitude * np.cos(phi) grs.amplitude = gx.amplitude * np.sin(phi) - grs.channel = "y" + grs.channel = 'y' gsc = copy(gx_spoil) gss = copy(gx_spoil) gsc.amplitude = gx_spoil.amplitude * np.cos(phi) gss.amplitude = gx_spoil.amplitude * np.sin(phi) - gss.channel = "y" + gss.channel = 'y' seq.add_block(gpc, gps, gz_reph) seq.add_block(grc, grs, adc) @@ -147,9 +132,9 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): # Check whether the timing of the sequence is correct ok, error_report = seq.check_timing() if ok: - print("Timing check passed successfully") + print('Timing check passed successfully') else: - print("Timing check failed. Error listing follows:") + print('Timing check failed. Error listing follows:') [print(e) for e in error_report] # ====== @@ -170,11 +155,11 @@ def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"): # ========= if write_seq: # Prepare the sequence output for the scanner - seq.set_definition(key="FOV", value=[fov, fov, slice_thickness]) - seq.set_definition(key="Name", value="UTE") + seq.set_definition(key='FOV', value=[fov, fov, slice_thickness]) + seq.set_definition(key='Name', value='UTE') seq.write(seq_filename) -if __name__ == "__main__": +if __name__ == '__main__': main(plot=True, write_seq=True) diff --git a/pypulseq/sigpy_pulse_opts.py b/pypulseq/sigpy_pulse_opts.py index ddc7aad..fd3af5f 100644 --- a/pypulseq/sigpy_pulse_opts.py +++ b/pypulseq/sigpy_pulse_opts.py @@ -1,26 +1,26 @@ class SigpyPulseOpts: def __init__( self, - pulse_type: str = "slr", - ptype: str = "st", - ftype: str = "ls", + pulse_type: str = 'slr', + ptype: str = 'st', + ftype: str = 'ls', d1: float = 0.01, d2: float = 0.01, cancel_alpha_phs: bool = False, n_bands: int = 3, band_sep: int = 20, - phs_0_pt: str = "None", + phs_0_pt: str = 'None', ): self.pulse_type = pulse_type - if pulse_type == "slr": + if pulse_type == 'slr': self.ptype = ptype self.ftype = ftype self.d1 = d1 self.d2 = d2 self.cancel_alpha_phs = cancel_alpha_phs - if pulse_type == "sms": + if pulse_type == 'sms': self.ptype = ptype self.ftype = ftype self.d1 = d1 @@ -31,11 +31,11 @@ def __init__( self.phs_0_pt = phs_0_pt def __str__(self) -> str: - s = "Pulse options:" - s += "\nptype: " + str(self.ptype) - s += "\nftype: " + str(self.ftype) - s += "\nd1: " + str(self.d1) - s += "\nd2: " + str(self.d2) - s += "\ncancel_alpha_phs: " + str(self.cancel_alpha_phs) + s = 'Pulse options:' + s += '\nptype: ' + str(self.ptype) + s += '\nftype: ' + str(self.ftype) + s += '\nd1: ' + str(self.d1) + s += '\nd2: ' + str(self.d2) + s += '\ncancel_alpha_phs: ' + str(self.cancel_alpha_phs) return s diff --git a/pypulseq/split_gradient.py b/pypulseq/split_gradient.py index 4b045e5..8808ba2 100644 --- a/pypulseq/split_gradient.py +++ b/pypulseq/split_gradient.py @@ -6,7 +6,7 @@ from pypulseq.calc_duration import calc_duration from pypulseq.make_extended_trapezoid import make_extended_trapezoid from pypulseq.opts import Opts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def split_gradient( @@ -17,7 +17,8 @@ def split_gradient( flat top and slew down) as extended trapezoid gradient objects. The delays in the individual gradient events are adapted such that addGradients(...) produces an gradient equivalent to 'grad'. - See also: + See Also + -------- - `pypulseq.split_gradient()` - `pypulseq.make_extended_trapezoid()` - `pypulseq.make_trapezoid()` @@ -44,11 +45,11 @@ def split_gradient( """ if system is None: system = Opts.default - + grad_raster_time = system.grad_raster_time total_length = calc_duration(grad) - if grad.type == "trap": + if grad.type == 'trap': channel = grad.channel grad.delay = round(grad.delay / grad_raster_time) * grad_raster_time grad.rise_time = round(grad.rise_time / grad_raster_time) * grad_raster_time @@ -77,10 +78,9 @@ def split_gradient( ) ramp_down.delay = total_length - grad.fall_time - times = np.array([0, grad.flat_time]) amplitudes = np.array([grad.amplitude, grad.amplitude]) - + flat_top = make_extended_trapezoid( channel=channel, system=system, @@ -97,7 +97,7 @@ def split_gradient( ramp_down.trace = t return ramp_up, flat_top, ramp_down - elif grad.type == "grad": - raise ValueError("Splitting of arbitrary gradients is not implemented yet.") + elif grad.type == 'grad': + raise ValueError('Splitting of arbitrary gradients is not implemented yet.') else: - raise ValueError("Splitting of unsupported event.") + raise ValueError('Splitting of unsupported event.') diff --git a/pypulseq/split_gradient_at.py b/pypulseq/split_gradient_at.py index 31dc7e3..85cdb27 100644 --- a/pypulseq/split_gradient_at.py +++ b/pypulseq/split_gradient_at.py @@ -4,10 +4,9 @@ import numpy as np -from pypulseq import eps from pypulseq.make_extended_trapezoid import make_extended_trapezoid from pypulseq.opts import Opts -from pypulseq.utils.tracing import trace_enabled, trace +from pypulseq.utils.tracing import trace, trace_enabled def split_gradient_at( @@ -19,7 +18,8 @@ def split_gradient_at( trapezoids, for 'arb' as arbitrary gradient objects. The delays in the individual gradient events are adapted such that add_gradients(...) produces a gradient equivalent to 'grad'. - See also: + See Also + -------- - `pypulseq.split_gradient()` - `pypulseq.make_extended_trapezoid()` - `pypulseq.make_trapezoid()` @@ -58,7 +58,7 @@ def split_gradient_at( time_point = round(time_index * grad_raster_time, 6) channel = grad.channel - if grad.type == "grad": + if grad.type == 'grad': # Check if we have an arbitrary gradient or an extended trapezoid if abs(grad.tt[-1] - 0.5 * grad_raster_time) < 1e-10 and np.all( abs(grad.tt[1:] - grad.tt[:-1] - grad_raster_time) < 1e-10 @@ -70,9 +70,7 @@ def split_gradient_at( else: grad1 = grad grad2 = grad - grad1.last = 0.5 * ( - grad.waveform[time_index - 1] + grad.waveform[time_index] - ) + grad1.last = 0.5 * (grad.waveform[time_index - 1] + grad.waveform[time_index]) grad2.first = grad1.last grad2.delay = grad.delay + grad.t[time_index] grad1.t = grad.t[:time_index] @@ -90,7 +88,7 @@ def split_gradient_at( # Extended trapezoid times = grad.tt amplitudes = grad.waveform - elif grad.type == "trap": + elif grad.type == 'trap': grad.delay = round(grad.delay / grad_raster_time) * grad_raster_time grad.rise_time = round(grad.rise_time / grad_raster_time) * grad_raster_time grad.flat_time = round(grad.flat_time / grad_raster_time) * grad_raster_time @@ -109,13 +107,11 @@ def split_gradient_at( ] amplitudes = [0, grad.amplitude, grad.amplitude, 0] else: - raise ValueError("Splitting of unsupported event.") + raise ValueError('Splitting of unsupported event.') # If the split line is behind the gradient, there is no second gradient to create if time_point >= grad.delay + times[-1]: - raise ValueError( - "Splitting of gradient at time point after the end of gradient." - ) + raise ValueError('Splitting of gradient at time point after the end of gradient.') # If the split line goes through the delay if time_point < grad.delay: diff --git a/pypulseq/supported_labels_rf_use.py b/pypulseq/supported_labels_rf_use.py index d38980f..4296d15 100644 --- a/pypulseq/supported_labels_rf_use.py +++ b/pypulseq/supported_labels_rf_use.py @@ -1,9 +1,7 @@ from typing import Tuple -def get_supported_labels() -> Tuple[ - str, str, str, str, str, str, str, str, str, str, str, str, str -]: +def get_supported_labels() -> Tuple[str, str, str, str, str, str, str, str, str, str, str, str, str]: """ Returns ------- @@ -11,24 +9,27 @@ def get_supported_labels() -> Tuple[ Supported labels. """ return ( - "SLC", - "SEG", - "REP", - "AVG", - "SET", - "ECO", - "PHS", - "LIN", - "PAR", - "NAV", - "REV", - "SMS", - "REF", "IMA", # For parallel imaging - "NOISE", # noise adjust scan, for iPAT acceleration - "PMC", # for MoCo/PMC Pulseq version to recognize blocks that can be prospectively corrected for motion - "NOROT", "NOPOS", "NOSCL", # instruct the interpreter to ignore the position, rotation or scaling of the FOV specified on the UI - "ONCE", # a 3-state flag that instructs the interpreter to alter the sequence when executing multiple repeats as follows: blocks with ONCE==0 are executed on every repetition; ONCE==1: only the first repetition; ONCE==2: only the last repetition - "TRID", # an integer ID of the TR (sequence segment) used by the GE interpreter to optimize the execution on the scanner + 'SLC', + 'SEG', + 'REP', + 'AVG', + 'SET', + 'ECO', + 'PHS', + 'LIN', + 'PAR', + 'NAV', + 'REV', + 'SMS', + 'REF', + 'IMA', # For parallel imaging + 'NOISE', # noise adjust scan, for iPAT acceleration + 'PMC', # for MoCo/PMC Pulseq version to recognize blocks that can be prospectively corrected for motion + 'NOROT', + 'NOPOS', + 'NOSCL', # instruct the interpreter to ignore the position, rotation or scaling of the FOV specified on the UI + 'ONCE', # a 3-state flag that instructs the interpreter to alter the sequence when executing multiple repeats as follows: blocks with ONCE==0 are executed on every repetition; ONCE==1: only the first repetition; ONCE==2: only the last repetition + 'TRID', # an integer ID of the TR (sequence segment) used by the GE interpreter to optimize the execution on the scanner ) @@ -39,4 +40,4 @@ def get_supported_rf_uses() -> Tuple[str, str, str, str, str]: tuple Supported RF use labels. """ - return "excitation", "refocusing", "inversion", "saturation", "preparation" + return 'excitation', 'refocusing', 'inversion', 'saturation', 'preparation' diff --git a/pypulseq/tests/base.py b/pypulseq/tests/base.py index a1084f3..b75417a 100644 --- a/pypulseq/tests/base.py +++ b/pypulseq/tests/base.py @@ -1,6 +1,6 @@ +from pathlib import Path from types import SimpleNamespace -from pathlib import Path import numpy as np import pytest from _pytest.python_api import ApproxBase @@ -9,7 +9,7 @@ def main(script: callable, matlab_seq_filename: str, pypulseq_seq_filename: str): path_here = Path(__file__) # Path of this file pypulseq_seq_filename = path_here.parent / pypulseq_seq_filename # Path to PyPulseq seq - matlab_seq_filename = path_here.parent / "matlab_seqs" / matlab_seq_filename # Path to MATLAB seq + matlab_seq_filename = path_here.parent / 'matlab_seqs' / matlab_seq_filename # Path to MATLAB seq # Run PyPulseq script and write seq file script.main(plot=False, write_seq=True, seq_filename=str(pypulseq_seq_filename)) @@ -63,24 +63,24 @@ def _repr_compare(self, actual): # return [f'Actual and expected types do not match: {type(actual)} != {type(self.expected)}'] if isinstance(self.expected, dict): if set(self.expected.keys()) != set(actual.keys()): - return [f"Actual and expected keys do not match: {set(actual.keys())} != {set(self.expected.keys())}"] + return [f'Actual and expected keys do not match: {set(actual.keys())} != {set(self.expected.keys())}'] r = [] for k in self.expected: approx_obj = Approx(self.expected[k], rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) if actual[k] != approx_obj: - r += [f"{k} does not match:"] - r += [f" {x}" for x in approx_obj._repr_compare(actual[k])] + r += [f'{k} does not match:'] + r += [f' {x}' for x in approx_obj._repr_compare(actual[k])] return r elif isinstance(self.expected, (list, tuple)): if len(self.expected) != len(actual): - return [f"Actual and expected lengths do not match: {len(actual)} != {len(self.expected)}"] + return [f'Actual and expected lengths do not match: {len(actual)} != {len(self.expected)}'] r = [] for i, (e, a) in enumerate(zip(self.expected, actual)): approx_obj = Approx(e, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) if a != approx_obj: - r += [f"Index {i} does not match:"] - r += [f" {x}" for x in approx_obj._repr_compare(a)] + r += [f'Index {i} does not match:'] + r += [f' {x}' for x in approx_obj._repr_compare(a)] return r elif isinstance(self.expected, SimpleNamespace): return Approx(self.expected.__dict__, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)._repr_compare( diff --git a/pypulseq/tests/ruff.toml b/pypulseq/tests/ruff.toml new file mode 100644 index 0000000..a9aea66 --- /dev/null +++ b/pypulseq/tests/ruff.toml @@ -0,0 +1,6 @@ +extend = "../../pyproject.toml" + +lint.extend-ignore = [ + "ARG001", #unused-argument + "E741", #ambiguous-variable-name +] diff --git a/pypulseq/tests/test_block.py b/pypulseq/tests/test_block.py index 32a8fbf..859bce6 100644 --- a/pypulseq/tests/test_block.py +++ b/pypulseq/tests/test_block.py @@ -1,18 +1,20 @@ import pytest + import pypulseq as pp # Gradient definitions used in tests gx_trap = pp.make_trapezoid('x', area=1000, duration=1e-3) -gx_extended = pp.make_extended_trapezoid('x', amplitudes=[0,100000,0], times=[0,1e-4,2e-4]) -gx_extended_delay = pp.make_extended_trapezoid('x', amplitudes=[0,100000,0], times=[1e-4,2e-4,3e-4]) -gx_endshigh = pp.make_extended_trapezoid('x', amplitudes=[0,100000,100000], times=[0,1e-4,2e-4]) -gx_startshigh = pp.make_extended_trapezoid('x', amplitudes=[100000,100000,0], times=[0,1e-4,2e-4]) -gx_startshigh2 = pp.make_extended_trapezoid('x', amplitudes=[200000,100000,0], times=[0,1e-4,2e-4]) -gx_allhigh = pp.make_extended_trapezoid('x', amplitudes=[100000,100000,100000], times=[0,1e-4,2e-4]) +gx_extended = pp.make_extended_trapezoid('x', amplitudes=[0, 100000, 0], times=[0, 1e-4, 2e-4]) +gx_extended_delay = pp.make_extended_trapezoid('x', amplitudes=[0, 100000, 0], times=[1e-4, 2e-4, 3e-4]) +gx_endshigh = pp.make_extended_trapezoid('x', amplitudes=[0, 100000, 100000], times=[0, 1e-4, 2e-4]) +gx_startshigh = pp.make_extended_trapezoid('x', amplitudes=[100000, 100000, 0], times=[0, 1e-4, 2e-4]) +gx_startshigh2 = pp.make_extended_trapezoid('x', amplitudes=[200000, 100000, 0], times=[0, 1e-4, 2e-4]) +gx_allhigh = pp.make_extended_trapezoid('x', amplitudes=[100000, 100000, 100000], times=[0, 1e-4, 2e-4]) delay = pp.make_delay(1e-3) ## Test gradient continuity checks in add_block + def test_gradient_continuity1(): # Trap followed by extended gradient: No error seq = pp.Sequence() @@ -20,18 +22,21 @@ def test_gradient_continuity1(): seq.add_block(gx_extended) seq.add_block(gx_trap) + def test_gradient_continuity2(): # Trap followed by non-zero gradient with pytest.raises(RuntimeError): seq = pp.Sequence() seq.add_block(gx_trap) - seq.add_block(gx_startshigh) # raises + seq.add_block(gx_startshigh) # raises + def test_gradient_continuity3(): # Gradient starts at non-zero in first block with pytest.raises(RuntimeError): seq = pp.Sequence() - seq.add_block(gx_startshigh) # raises + seq.add_block(gx_startshigh) # raises + def test_gradient_continuity4(): # Gradient starts and ends at non-zero @@ -40,17 +45,20 @@ def test_gradient_continuity4(): seq.add_block(delay) seq.add_block(gx_allhigh) + def test_gradient_continuity5(): # Gradient starts at zero and has a delay: No error seq = pp.Sequence() seq.add_block(gx_extended_delay) + def test_gradient_continuity6(): # Gradient starts at non-zero in other blocks with pytest.raises(RuntimeError): seq = pp.Sequence() seq.add_block(delay) - seq.add_block(gx_startshigh) # raises + seq.add_block(gx_startshigh) # raises + def test_gradient_continuity7(): # Gradient ends high and is followed by empty block @@ -59,6 +67,7 @@ def test_gradient_continuity7(): seq.add_block(gx_endshigh) seq.add_block(delay) # raises + def test_gradient_continuity8(): # Gradient ends high and is followed by trapezoid with pytest.raises(RuntimeError): @@ -66,17 +75,20 @@ def test_gradient_continuity8(): seq.add_block(gx_endshigh) seq.add_block(gx_trap) # raises + def test_gradient_continuity9(): # Gradient ends high and is followed by connecting gradient: No error seq = pp.Sequence() seq.add_block(gx_endshigh) seq.add_block(gx_startshigh) + def test_gradient_continuity10(): # Gradient in last block ends high: No error, this is caught by seq.write() seq = pp.Sequence() seq.add_block(gx_endshigh) + def test_gradient_continuity11(): # Non-zero, but non-connecting gradients with pytest.raises(RuntimeError): @@ -85,9 +97,9 @@ def test_gradient_continuity11(): seq.add_block(gx_startshigh2) - ## Test gradient continuity checks in set_block + def test_gradient_continuity_setblock1(): # Use set_block to insert gradient with pytest.raises(RuntimeError): @@ -98,6 +110,7 @@ def test_gradient_continuity_setblock1(): seq.set_block(1, gx_startshigh) + def test_gradient_continuity_setblock2(): with pytest.raises(RuntimeError): seq = pp.Sequence() @@ -107,6 +120,7 @@ def test_gradient_continuity_setblock2(): seq.set_block(2, gx_startshigh) + def test_gradient_continuity_setblock3(): with pytest.raises(RuntimeError): seq = pp.Sequence() @@ -116,6 +130,7 @@ def test_gradient_continuity_setblock3(): seq.set_block(3, gx_startshigh) + def test_gradient_continuity_setblock4(): # Overwrite valid gradient with empty block with pytest.raises(RuntimeError): @@ -126,6 +141,7 @@ def test_gradient_continuity_setblock4(): seq.set_block(2, delay) + def test_gradient_continuity_setblock5(): # Overwrite valid gradient with gradient that is valid on one side with pytest.raises(RuntimeError): @@ -136,6 +152,7 @@ def test_gradient_continuity_setblock5(): seq.set_block(2, gx_startshigh) + def test_gradient_continuity_setblock6(): # Add new gradient with non-contiguous block numbering with pytest.raises(RuntimeError): @@ -146,6 +163,7 @@ def test_gradient_continuity_setblock6(): seq.set_block(6, gx_startshigh) + def test_gradient_continuity_setblock7(): # Valid sequence with non-contiguous block numbering seq = pp.Sequence() @@ -153,7 +171,7 @@ def test_gradient_continuity_setblock7(): seq.set_block(5, gx_allhigh) seq.set_block(7, gx_startshigh) - assert list(seq.block_events.keys()) == [10,5,7] + assert list(seq.block_events.keys()) == [10, 5, 7] # TODO: Add other block functionality tests diff --git a/pypulseq/tests/test_calc_adc_segments.py b/pypulseq/tests/test_calc_adc_segments.py index 00e770a..7d60acd 100644 --- a/pypulseq/tests/test_calc_adc_segments.py +++ b/pypulseq/tests/test_calc_adc_segments.py @@ -1,30 +1,32 @@ -"""Test function to test calc_adc_segments() and compare its output to the pulseq MATLAB function calcAdcSeg() -""" +"""Test function to test calc_adc_segments() and compare its output to the pulseq MATLAB function calcAdcSeg()""" + import math import os + import numpy as np -from pypulseq import Opts -from pypulseq import calc_adc_segments + +from pypulseq import Opts, calc_adc_segments system = Opts.default system.adc_raster_time = 1e-7 system.grad_raster_time = 1e-5 -dirpath = os.path.dirname(__file__) +dirpath = os.path.dirname(__file__) # noqa: PTH120 (temporary solution) data = np.genfromtxt( dirpath + '/expected_output/pulseq_calcAdcSeg.txt', dtype=[ - ('dwell', float), - ('num_samples', int), - ('adc_limit', int), - ('adc_divisor', int), - ('mode', int), - ('res_num_seg', int), - ('res_num_samples_seg', int) - ] + ('dwell', float), + ('num_samples', int), + ('adc_limit', int), + ('adc_divisor', int), + ('mode', int), + ('res_num_seg', int), + ('res_num_samples_seg', int), + ], ) + def test_calc_adc_segments(): # Accessing structured data for i, row in enumerate(data): @@ -35,42 +37,35 @@ def test_calc_adc_segments(): res_num_seg = row['res_num_seg'] res_num_samples_seg = row['res_num_samples_seg'] n_mode = row['mode'] - mode = 'shorten' if n_mode==1 else 'lengthen' - + mode = 'shorten' if n_mode == 1 else 'lengthen' + # Similar processing as before... system.adc_samples_limit = adc_limit system.adc_samples_divisor = adc_divisor - - num_seg, num_samples_seg = calc_adc_segments( - num_samples=num_samples, dwell=dwell, - system=system, mode=mode) - + + num_seg, num_samples_seg = calc_adc_segments(num_samples=num_samples, dwell=dwell, system=system, mode=mode) + # Check if output is identical to matlab pulseq - assert num_seg==res_num_seg - assert num_samples_seg==res_num_samples_seg - - # Chek if segment samples are below sample limit + assert num_seg == res_num_seg + assert num_samples_seg == res_num_samples_seg + + # Check if segment samples are below sample limit assert num_samples_seg <= adc_limit - + seg_duration = num_samples_seg * dwell adc_duration = seg_duration * num_seg - + # Check if each segment is on grad raster time - assert ( math.isclose( - round(seg_duration / system.grad_raster_time), - seg_duration / system.grad_raster_time) - ) + assert math.isclose(round(seg_duration / system.grad_raster_time), seg_duration / system.grad_raster_time) # Check if total adc is on grad raster time - assert ( math.isclose( - round(adc_duration / system.grad_raster_time), - adc_duration / system.grad_raster_time) - ) - + assert math.isclose(round(adc_duration / system.grad_raster_time), adc_duration / system.grad_raster_time) + # Print progress - if i % 1000 == 0 or i == (data.shape[0]-1): - print(round(i/data.shape[0] * 100, 2), '%') + if i % 1000 == 0 or i == (data.shape[0] - 1): + print(round(i / data.shape[0] * 100, 2), '%') + # Can be run with "pytest " or "python " -if __name__=='__main__': +if __name__ == '__main__': test_calc_adc_segments() diff --git a/pypulseq/tests/test_calc_duration.py b/pypulseq/tests/test_calc_duration.py index 8074b82..29d16e7 100644 --- a/pypulseq/tests/test_calc_duration.py +++ b/pypulseq/tests/test_calc_duration.py @@ -1,7 +1,8 @@ -import pypulseq as pp +from itertools import combinations_with_replacement + import pytest -from itertools import combinations_with_replacement +import pypulseq as pp """ Tests calc_duration by feeding it some sample events with known durations. @@ -14,43 +15,42 @@ def test_no_event(): known_duration_event_zoo = [ - ("trapz_amp1", pp.make_trapezoid("x", amplitude=1, duration=1), 1), - ("trapz_amp1_delayed1", pp.make_trapezoid("x", amplitude=1, duration=1, delay=1), 2), - ("delay1", pp.make_delay(1), 1), - ("delay0", pp.make_delay(0), 0), - ("rf0_block1", pp.make_block_pulse(flip_angle=0, duration=1), 1), - ("rf10_block1", pp.make_block_pulse(flip_angle=10, duration=1), 1), - ("rf10_block1_delay1", pp.make_block_pulse(flip_angle=10, duration=1, delay=1), 2), - ("adc3", pp.make_adc(duration=3, num_samples=1), 3), - ("adc3_delayed", pp.make_adc(duration=3, delay=1, num_samples=1), 4), - ("outputOsc042", pp.make_digital_output_pulse("osc0", duration=42), 42), - ("outputOsc142_delay3", pp.make_digital_output_pulse("osc1", duration=42, delay=1), 43), - ("outputExt42_delay9", pp.make_digital_output_pulse("osc1", duration=42, delay=9), 51), - ("triggerPhysio159", pp.make_trigger("physio1", duration=59), 59), - ("triggerPhysio259_delay1", pp.make_trigger("physio2", duration=59, delay=1), 60), - ("label0", pp.make_label(label="SLC", type="SET", value=0), 0) + ('trapz_amp1', pp.make_trapezoid('x', amplitude=1, duration=1), 1), + ('trapz_amp1_delayed1', pp.make_trapezoid('x', amplitude=1, duration=1, delay=1), 2), + ('delay1', pp.make_delay(1), 1), + ('delay0', pp.make_delay(0), 0), + ('rf0_block1', pp.make_block_pulse(flip_angle=0, duration=1), 1), + ('rf10_block1', pp.make_block_pulse(flip_angle=10, duration=1), 1), + ('rf10_block1_delay1', pp.make_block_pulse(flip_angle=10, duration=1, delay=1), 2), + ('adc3', pp.make_adc(duration=3, num_samples=1), 3), + ('adc3_delayed', pp.make_adc(duration=3, delay=1, num_samples=1), 4), + ('outputOsc042', pp.make_digital_output_pulse('osc0', duration=42), 42), + ('outputOsc142_delay3', pp.make_digital_output_pulse('osc1', duration=42, delay=1), 43), + ('outputExt42_delay9', pp.make_digital_output_pulse('osc1', duration=42, delay=9), 51), + ('triggerPhysio159', pp.make_trigger('physio1', duration=59), 59), + ('triggerPhysio259_delay1', pp.make_trigger('physio2', duration=59, delay=1), 60), + ('label0', pp.make_label(label='SLC', type='SET', value=0), 0), ] -@pytest.mark.parametrize("name,event,expected_dur", known_duration_event_zoo) +@pytest.mark.parametrize('name,event,expected_dur', known_duration_event_zoo) def test_single_events(name, event, expected_dur): assert pp.calc_duration(event) == expected_dur def known_duration_event_zoo_combos(num_to_combine): for combo in combinations_with_replacement(known_duration_event_zoo, num_to_combine): - name = ",".join(event[0] for event in combo) + name = ','.join(event[0] for event in combo) expected_dur = max(event[2] for event in combo) events = (event[1] for event in combo) yield name, tuple(events), expected_dur -@pytest.mark.parametrize("name,events,expected_total_dur", known_duration_event_zoo_combos(2)) +@pytest.mark.parametrize('name,events,expected_total_dur', known_duration_event_zoo_combos(2)) def test_event_combinations2(name, events, expected_total_dur): assert pp.calc_duration(*events) == expected_total_dur -@pytest.mark.parametrize("name,events,expected_total_dur", known_duration_event_zoo_combos(3)) +@pytest.mark.parametrize('name,events,expected_total_dur', known_duration_event_zoo_combos(3)) def test_event_combinations3(name, events, expected_total_dur): assert pp.calc_duration(*events) == expected_total_dur - diff --git a/pypulseq/tests/test_check_timing.py b/pypulseq/tests/test_check_timing.py index 910cbef..cb4a268 100644 --- a/pypulseq/tests/test_check_timing.py +++ b/pypulseq/tests/test_check_timing.py @@ -3,9 +3,9 @@ # System settings system = pp.Opts( max_grad=28, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=200, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6, @@ -14,28 +14,24 @@ # System with ringdown and dead times set to 0 to introduce timing errors system_broken = pp.Opts( max_grad=28, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=200, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=0e-6, rf_dead_time=0e-6, adc_dead_time=0e-6, ) + # Check whether there are no errors in the timing error report for the given blocks def blocks_not_in_error_report(error_report, blocks): - for error in error_report: - if error.block in blocks: - return False - return True + return all(error.block not in blocks for error in error_report) + # Check whether a given timing error exists in the report def exists_in_error_report(error_report, block, event, field, error_type): for error in error_report: - if (error.block == block and - error.event == event and - error.field == field and - error.error_type == error_type): + if error.block == block and error.event == event and error.field == field and error.error_type == error_type: return True return False @@ -46,31 +42,31 @@ def test_check_timing(): # Add events with possible timing errors rf = pp.make_sinc_pulse(flip_angle=1, duration=1e-3, delay=system.rf_dead_time, system=system) - seq.add_block(rf) # Block 1: No error + seq.add_block(rf) # Block 1: No error rf = pp.make_sinc_pulse(flip_angle=1, duration=1e-3, system=system_broken) - seq.add_block(rf) # Block 2: RF_DEAD_TIME, RF_RINGDOWN_TIME, BLOCK_DURATION_MISMATCH + seq.add_block(rf) # Block 2: RF_DEAD_TIME, RF_RINGDOWN_TIME, BLOCK_DURATION_MISMATCH adc = pp.make_adc(num_samples=100, duration=1e-3, delay=system.adc_dead_time, system=system) - seq.add_block(adc) # Block 3: No error + seq.add_block(adc) # Block 3: No error adc = pp.make_adc(num_samples=123, duration=1e-3, delay=system.adc_dead_time, system=system) - seq.add_block(adc) # Block 4: RASTER + seq.add_block(adc) # Block 4: RASTER adc = pp.make_adc(num_samples=100, duration=1e-3, system=system_broken) - seq.add_block(adc) # Block 5: ADC_DEAD_TIME, POST_ADC_DEAD_TIME + seq.add_block(adc) # Block 5: ADC_DEAD_TIME, POST_ADC_DEAD_TIME - gx = pp.make_trapezoid(channel="x", area=1, duration=1, system=system) - seq.add_block(gx) # Block 6: No error + gx = pp.make_trapezoid(channel='x', area=1, duration=1, system=system) + seq.add_block(gx) # Block 6: No error - gx = pp.make_trapezoid(channel="x", area=1, duration=1.00001e-3, system=system) - seq.add_block(gx) # Block 7: RASTER + gx = pp.make_trapezoid(channel='x', area=1, duration=1.00001e-3, system=system) + seq.add_block(gx) # Block 7: RASTER - gx = pp.make_trapezoid(channel="x", area=1, rise_time=1e-6, flat_time=1e-3, fall_time=3e-6, system=system) - seq.add_block(gx) # Block 8: RASTER + gx = pp.make_trapezoid(channel='x', area=1, rise_time=1e-6, flat_time=1e-3, fall_time=3e-6, system=system) + seq.add_block(gx) # Block 8: RASTER - gx = pp.make_trapezoid(channel="x", area=1, duration=1e-3, delay=-1e-5, system=system) - seq.add_block(gx) # Block 9: NEGATIVE_DELAY + gx = pp.make_trapezoid(channel='x', area=1, duration=1e-3, delay=-1e-5, system=system) + seq.add_block(gx) # Block 9: NEGATIVE_DELAY # Check timing errors ok, error_report = seq.check_timing() @@ -80,7 +76,9 @@ def test_check_timing(): assert exists_in_error_report(error_report, 2, event='rf', field='delay', error_type='RF_DEAD_TIME') assert exists_in_error_report(error_report, 2, event='rf', field='duration', error_type='RF_RINGDOWN_TIME') - assert exists_in_error_report(error_report, 2, event='block', field='duration', error_type='BLOCK_DURATION_MISMATCH') + assert exists_in_error_report( + error_report, 2, event='block', field='duration', error_type='BLOCK_DURATION_MISMATCH' + ) assert exists_in_error_report(error_report, 4, event='adc', field='dwell', error_type='RASTER') diff --git a/pypulseq/tests/test_epi.py b/pypulseq/tests/test_epi.py index 53ddd1c..c14c7e1 100644 --- a/pypulseq/tests/test_epi.py +++ b/pypulseq/tests/test_epi.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_epi from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestEPI(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "epi_matlab.seq" - pypulseq_seq_filename = "epi_pypulseq.seq" + matlab_seq_filename = 'epi_matlab.seq' + pypulseq_seq_filename = 'epi_pypulseq.seq' base.main( script=write_epi, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_epi_label.py b/pypulseq/tests/test_epi_label.py index 91cad99..ea8a1da 100644 --- a/pypulseq/tests/test_epi_label.py +++ b/pypulseq/tests/test_epi_label.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_epi_label from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestEPILabel(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "epi_label_matlab.seq" - pypulseq_seq_filename = "epi_label_pypulseq.seq" + matlab_seq_filename = 'epi_label_matlab.seq' + pypulseq_seq_filename = 'epi_label_pypulseq.seq' base.main( script=write_epi_label, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_epi_se.py b/pypulseq/tests/test_epi_se.py index 4c8ddf7..f9762c1 100644 --- a/pypulseq/tests/test_epi_se.py +++ b/pypulseq/tests/test_epi_se.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_epi_se from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestEPISpinEcho(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "epi_se_matlab.seq" - pypulseq_seq_filename = "epi_se_pypulseq.seq" + matlab_seq_filename = 'epi_se_matlab.seq' + pypulseq_seq_filename = 'epi_se_pypulseq.seq' base.main( script=write_epi_se, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_epi_se_rs.py b/pypulseq/tests/test_epi_se_rs.py index 3705026..3db2f30 100644 --- a/pypulseq/tests/test_epi_se_rs.py +++ b/pypulseq/tests/test_epi_se_rs.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_epi_se_rs from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestEPISpinEchoRS(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "epi_se_rs_matlab.seq" - pypulseq_seq_filename = "epi_se_rs_pypulseq.seq" + matlab_seq_filename = 'epi_se_rs_matlab.seq' + pypulseq_seq_filename = 'epi_se_rs_pypulseq.seq' base.main( script=write_epi_se_rs, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_gre.py b/pypulseq/tests/test_gre.py index 5bd6aba..bdd2692 100644 --- a/pypulseq/tests/test_gre.py +++ b/pypulseq/tests/test_gre.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_gre from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestGRE(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "gre_matlab.seq" - pypulseq_seq_filename = "gre_pypulseq.seq" + matlab_seq_filename = 'gre_matlab.seq' + pypulseq_seq_filename = 'gre_pypulseq.seq' base.main( script=write_gre, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_gre_label.py b/pypulseq/tests/test_gre_label.py index 640d008..9be383a 100644 --- a/pypulseq/tests/test_gre_label.py +++ b/pypulseq/tests/test_gre_label.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_gre_label from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestGRELabel(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "gre_label_matlab.seq" - pypulseq_seq_filename = "gre_label_pypulseq.seq" + matlab_seq_filename = 'gre_label_matlab.seq' + pypulseq_seq_filename = 'gre_label_pypulseq.seq' base.main( script=write_gre_label, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_gre_radial.py b/pypulseq/tests/test_gre_radial.py index 878032c..91dc06b 100644 --- a/pypulseq/tests/test_gre_radial.py +++ b/pypulseq/tests/test_gre_radial.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_radial_gre from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestEPISpinEchoRS(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "gre_radial_matlab.seq" - pypulseq_seq_filename = "gre_radial_pypulseq.seq" + matlab_seq_filename = 'gre_radial_matlab.seq' + pypulseq_seq_filename = 'gre_radial_pypulseq.seq' base.main( script=write_radial_gre, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_haste.py b/pypulseq/tests/test_haste.py index dbd1e4e..e5f8667 100644 --- a/pypulseq/tests/test_haste.py +++ b/pypulseq/tests/test_haste.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_haste from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestHASTE(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "haste_matlab.seq" - pypulseq_seq_filename = "haste_pypulseq.seq" + matlab_seq_filename = 'haste_matlab.seq' + pypulseq_seq_filename = 'haste_pypulseq.seq' base.main( script=write_haste, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_make_adiabatic_pulse.py b/pypulseq/tests/test_make_adiabatic_pulse.py index 9a05af1..68b6507 100644 --- a/pypulseq/tests/test_make_adiabatic_pulse.py +++ b/pypulseq/tests/test_make_adiabatic_pulse.py @@ -3,13 +3,13 @@ Will Clarke, University of Oxford, 2024 """ -import pytest import itertools import numpy as np +import pytest -from pypulseq.supported_labels_rf_use import get_supported_rf_uses from pypulseq import make_adiabatic_pulse +from pypulseq.supported_labels_rf_use import get_supported_rf_uses def test_pulse_select(): @@ -19,53 +19,35 @@ def test_pulse_select(): # Check all use and valid pulse combinations return a sensible object # with default parameters. for pulse_type, use_label in itertools.product(valid_pulse_types, valid_rf_use_labels): - rf_obj = make_adiabatic_pulse( - pulse_type=pulse_type, - use=use_label - ) + rf_obj = make_adiabatic_pulse(pulse_type=pulse_type, use=use_label) assert rf_obj.type == 'rf' assert rf_obj.use == use_label # Check the appropriate errors are raised if we specify nonsense - with pytest.raises( - ValueError, - match="Invalid type parameter. Must be one of "): - make_adiabatic_pulse(pulse_type="not a pulse type") - - with pytest.raises( - ValueError, - match="Invalid type parameter. Must be one of "): - make_adiabatic_pulse(pulse_type="") - - with pytest.raises( - ValueError, - match="Invalid use parameter. Must be one of "): - make_adiabatic_pulse( - pulse_type="hypsec", - use='not a use') + with pytest.raises(ValueError, match='Invalid type parameter. Must be one of '): + make_adiabatic_pulse(pulse_type='not a pulse type') + + with pytest.raises(ValueError, match='Invalid type parameter. Must be one of '): + make_adiabatic_pulse(pulse_type='') + + with pytest.raises(ValueError, match='Invalid use parameter. Must be one of '): + make_adiabatic_pulse(pulse_type='hypsec', use='not a use') # Default use case - rf_obj = make_adiabatic_pulse(pulse_type="hypsec") - assert rf_obj.use == "inversion" + rf_obj = make_adiabatic_pulse(pulse_type='hypsec') + assert rf_obj.use == 'inversion' def test_option_requirements(): - # Require non-zero slice thickness if grad requested - with pytest.raises( - ValueError, - match="Slice thickness must be provided"): - make_adiabatic_pulse( - pulse_type="hypsec", - return_gz=True) - - _, gz, gzr = make_adiabatic_pulse( - pulse_type="hypsec", - return_gz=True, - slice_thickness=1) + with pytest.raises(ValueError, match='Slice thickness must be provided'): + make_adiabatic_pulse(pulse_type='hypsec', return_gz=True) + + _, gz, gzr = make_adiabatic_pulse(pulse_type='hypsec', return_gz=True, slice_thickness=1) assert gz.type == 'trap' assert gzr.type == 'trap' + # My intention was to test that the rephase gradient area is appropriate, # but this doesn't pass and I'm highly suspicious of the calculation in # the code @@ -79,20 +61,12 @@ def test_returned_grads(): def test_hypsec_options(): - pobj = make_adiabatic_pulse( - pulse_type="hypsec", - beta=700, - mu=6, - duration=0.05) + pobj = make_adiabatic_pulse(pulse_type='hypsec', beta=700, mu=6, duration=0.05) assert np.isclose(pobj.shape_dur, 0.05) def test_wurst_options(): - pobj = make_adiabatic_pulse( - pulse_type="wurst", - n_fac=25, - bandwidth=30000, - duration=0.05) + pobj = make_adiabatic_pulse(pulse_type='wurst', n_fac=25, bandwidth=30000, duration=0.05) assert np.isclose(pobj.shape_dur, 0.05) diff --git a/pypulseq/tests/test_make_block_pulse.py b/pypulseq/tests/test_make_block_pulse.py index 232328b..c7c0eb9 100644 --- a/pypulseq/tests/test_make_block_pulse.py +++ b/pypulseq/tests/test_make_block_pulse.py @@ -5,46 +5,32 @@ from types import SimpleNamespace -import pytest import numpy as np +import pytest from pypulseq import make_block_pulse def test_invalid_use_error(): - - with pytest.raises( - ValueError, - match=r"Invalid use parameter."): - make_block_pulse(flip_angle=np.pi, duration=1E-3, use='foo') + with pytest.raises(ValueError, match=r'Invalid use parameter.'): + make_block_pulse(flip_angle=np.pi, duration=1e-3, use='foo') def test_bandwidth_and_duration_error(): - - with pytest.raises( - ValueError, - match=r"One of bandwidth or duration must be defined, but not both."): - make_block_pulse(flip_angle=np.pi, duration=1E-3, bandwidth=1000) + with pytest.raises(ValueError, match=r'One of bandwidth or duration must be defined, but not both.'): + make_block_pulse(flip_angle=np.pi, duration=1e-3, bandwidth=1000) def test_invalid_bandwidth_and_duration_error(): + with pytest.raises(ValueError, match=r'One of bandwidth or duration must be defined and be > 0.'): + make_block_pulse(flip_angle=np.pi, duration=-1e-3) - with pytest.raises( - ValueError, - match=r"One of bandwidth or duration must be defined and be > 0."): - make_block_pulse(flip_angle=np.pi, duration=-1E-3) - - with pytest.raises( - ValueError, - match=r"One of bandwidth or duration must be defined and be > 0."): - make_block_pulse(flip_angle=np.pi, bandwidth=-1E3) + with pytest.raises(ValueError, match=r'One of bandwidth or duration must be defined and be > 0.'): + make_block_pulse(flip_angle=np.pi, bandwidth=-1e3) def test_default_duration_warning(): - - with pytest.warns( - UserWarning, - match=r'Using default 4 ms duration for block pulse.'): + with pytest.warns(UserWarning, match=r'Using default 4 ms duration for block pulse.'): make_block_pulse(flip_angle=np.pi) @@ -56,34 +42,33 @@ def test_generation_methods(): - bandwidth - bandwidth + time_bw_product """ - # Capture expected warning for default case with pytest.warns(UserWarning): case1 = make_block_pulse(flip_angle=np.pi) assert isinstance(case1, SimpleNamespace) - assert case1.shape_dur == 4E-3 + assert case1.shape_dur == 4e-3 - case2 = make_block_pulse(flip_angle=np.pi, duration=1E-3) + case2 = make_block_pulse(flip_angle=np.pi, duration=1e-3) assert isinstance(case2, SimpleNamespace) - assert case2.shape_dur == 1E-3 + assert case2.shape_dur == 1e-3 - case3 = make_block_pulse(flip_angle=np.pi, bandwidth=1E3) + case3 = make_block_pulse(flip_angle=np.pi, bandwidth=1e3) assert isinstance(case3, SimpleNamespace) - assert case3.shape_dur == 1 / (4 * 1E3) + assert case3.shape_dur == 1 / (4 * 1e3) - case4 = make_block_pulse(flip_angle=np.pi, bandwidth=1E3, time_bw_product=5) + case4 = make_block_pulse(flip_angle=np.pi, bandwidth=1e3, time_bw_product=5) assert isinstance(case4, SimpleNamespace) - assert case4.shape_dur == 5 / 1E3 + assert case4.shape_dur == 5 / 1e3 def test_amp_calculation(): # A 1 ms 180 degree pulse requires 500 Hz gamma B1 - pulse = make_block_pulse(duration=1E-3, flip_angle=np.pi) + pulse = make_block_pulse(duration=1e-3, flip_angle=np.pi) assert np.isclose(pulse.signal.max(), 500) - pulse = make_block_pulse(duration=1E-3, flip_angle=np.pi/2) + pulse = make_block_pulse(duration=1e-3, flip_angle=np.pi / 2) assert np.isclose(pulse.signal.max(), 250) - pulse = make_block_pulse(duration=2E-3, flip_angle=np.pi/2) + pulse = make_block_pulse(duration=2e-3, flip_angle=np.pi / 2) assert np.isclose(pulse.signal.max(), 125) diff --git a/pypulseq/tests/test_make_extended_trapezoid_area.py b/pypulseq/tests/test_make_extended_trapezoid_area.py index bb04fbe..6415c52 100644 --- a/pypulseq/tests/test_make_extended_trapezoid_area.py +++ b/pypulseq/tests/test_make_extended_trapezoid_area.py @@ -1,9 +1,10 @@ -import pytest -import numpy as np -import random import math +import random -from pypulseq import Opts, make_extended_trapezoid_area, make_extended_trapezoid, calc_duration +import numpy as np +import pytest + +from pypulseq import Opts, calc_duration, make_extended_trapezoid, make_extended_trapezoid_area from pypulseq.utils.cumsum import cumsum system = Opts() @@ -23,14 +24,14 @@ (0, 1000, -100), (-1000, 1000, -100), (-1000, 0, -100), - (0, system.max_grad*0.99, 10000), - (0, system.max_grad*0.99, -10000), - (0, -system.max_grad*0.99, 1000), - (0, -system.max_grad*0.99, -1000), - (system.max_grad*0.99, 0, 100), - (system.max_grad*0.99, 0, -100), - (-system.max_grad*0.99, 0, 1), - (-system.max_grad*0.99, 0, -1), + (0, system.max_grad * 0.99, 10000), + (0, system.max_grad * 0.99, -10000), + (0, -system.max_grad * 0.99, 1000), + (0, -system.max_grad * 0.99, -1000), + (system.max_grad * 0.99, 0, 100), + (system.max_grad * 0.99, 0, -100), + (-system.max_grad * 0.99, 0, 1), + (-system.max_grad * 0.99, 0, -1), (0, 100000, 1), (0, 100000, -1), (0, -100000, 1), @@ -39,99 +40,123 @@ (0, 90000, -0.45), (0, -90000, 0.45), (0, -90000, -0.45), - (0, 10000, 0.5 * (10000)**2 / (system.max_slew*0.99)), - (0, system.max_grad*0.99, 0.5 * (system.max_grad*0.99)**2 / (system.max_slew*0.99)), - (system.max_grad*0.99, system.max_grad*0.99, 1), - (system.max_grad*0.99, system.max_grad*0.99, -1), - ] + (0, 10000, 0.5 * (10000) ** 2 / (system.max_slew * 0.99)), + (0, system.max_grad * 0.99, 0.5 * (system.max_grad * 0.99) ** 2 / (system.max_slew * 0.99)), + (system.max_grad * 0.99, system.max_grad * 0.99, 1), + (system.max_grad * 0.99, system.max_grad * 0.99, -1), +] -@pytest.mark.parametrize("grad_start,grad_end,area", test_zoo) +@pytest.mark.parametrize('grad_start,grad_end,area', test_zoo) def test_make_extended_trapezoid_area(grad_start, grad_end, area): - g,_,_ = make_extended_trapezoid_area(channel='x', grad_start=grad_start, grad_end=grad_end, area=area, system=system) - + g, _, _ = make_extended_trapezoid_area( + channel='x', grad_start=grad_start, grad_end=grad_end, area=area, system=system + ) + grad_ok = all(abs(g.waveform) <= system.max_grad) slew_ok = all(abs(np.diff(g.waveform) / np.diff(g.tt)) <= system.max_slew) - + assert pytest.approx(g.area) == area, 'Result area is not correct' assert grad_ok, 'Maximum gradient strength violated' assert slew_ok, 'Maximum slew rate violated' random.seed(0) -test_zoo_random = [((random.random()-0.5) * 2 * system.max_grad * 0.99, - (random.random()-0.5) * 2 * system.max_grad * 0.99, - (random.random()-0.5) * 10000) for _ in range(100)] +test_zoo_random = [ + ( + (random.random() - 0.5) * 2 * system.max_grad * 0.99, + (random.random() - 0.5) * 2 * system.max_grad * 0.99, + (random.random() - 0.5) * 10000, + ) + for _ in range(100) +] -@pytest.mark.parametrize("grad_start,grad_end,area", test_zoo_random) + +@pytest.mark.parametrize('grad_start,grad_end,area', test_zoo_random) def test_make_extended_trapezoid_area_random_cases(grad_start, grad_end, area): - g,_,_ = make_extended_trapezoid_area(channel='x', grad_start=grad_start, grad_end=grad_end, area=area, system=system) - + g, _, _ = make_extended_trapezoid_area( + channel='x', grad_start=grad_start, grad_end=grad_end, area=area, system=system + ) + grad_ok = all(abs(g.waveform) <= system.max_grad) slew_ok = all(abs(np.diff(g.waveform) / np.diff(g.tt)) <= system.max_slew) - + assert pytest.approx(g.area) == area, 'Result area is not correct' assert grad_ok, 'Maximum gradient strength violated' assert slew_ok, 'Maximum slew rate violated' random.seed(0) -test_zoo_random = [((random.random()-0.5) * 3 * system.max_grad, - (random.random()-0.5) * 3 * system.max_grad, - (random.random()-0.5) * 10 * system.max_grad) for _ in range(100)] +test_zoo_random = [ + ( + (random.random() - 0.5) * 3 * system.max_grad, + (random.random() - 0.5) * 3 * system.max_grad, + (random.random() - 0.5) * 10 * system.max_grad, + ) + for _ in range(100) +] + -@pytest.mark.parametrize("grad_start,grad_end,grad_amp", test_zoo_random) +@pytest.mark.parametrize('grad_start,grad_end,grad_amp', test_zoo_random) def test_make_extended_trapezoid_area_recreate(grad_start, grad_end, grad_amp): def _to_raster(time: float) -> float: return np.ceil(time / system.grad_raster_time) * system.grad_raster_time - + def _calc_ramp_time(grad_amp, slew_rate, grad_start): return _to_raster(abs(grad_amp - grad_start) / slew_rate) - + grad_start = np.clip(grad_start, -system.max_grad, system.max_grad) grad_end = np.clip(grad_end, -system.max_grad, system.max_grad) - + # Construct extended gradient based on start, intermediate, and end gradient # strengths, assuming maximum slew rates. - + # If grad_amp > max_grad, convert to max_grad, and keep total area # approximately equal - if abs(grad_amp) > system.max_grad*0.99: - grad_amp_new = math.copysign(system.max_grad*0.99, grad_amp) - - flat_time = _to_raster(_calc_ramp_time(grad_amp_new, system.max_slew*0.99, grad_amp) * abs(grad_amp_new + grad_amp) / abs(grad_amp_new)) + if abs(grad_amp) > system.max_grad * 0.99: + grad_amp_new = math.copysign(system.max_grad * 0.99, grad_amp) + + flat_time = _to_raster( + _calc_ramp_time(grad_amp_new, system.max_slew * 0.99, grad_amp) + * abs(grad_amp_new + grad_amp) + / abs(grad_amp_new) + ) grad_amp = grad_amp_new else: flat_time = 0 - + # Construct extended gradient if flat_time == 0: - times = cumsum(0, - _calc_ramp_time(grad_amp, system.max_slew*0.99, grad_start), - _calc_ramp_time(grad_amp, system.max_slew*0.99, grad_end), - ) + times = cumsum( + 0, + _calc_ramp_time(grad_amp, system.max_slew * 0.99, grad_start), + _calc_ramp_time(grad_amp, system.max_slew * 0.99, grad_end), + ) amplitudes = (grad_start, grad_amp, grad_end) else: - times = cumsum(0, - _calc_ramp_time(grad_amp, system.max_slew*0.99, grad_start), - _to_raster(flat_time), - _calc_ramp_time(grad_amp, system.max_slew*0.99, grad_end), - ) + times = cumsum( + 0, + _calc_ramp_time(grad_amp, system.max_slew * 0.99, grad_start), + _to_raster(flat_time), + _calc_ramp_time(grad_amp, system.max_slew * 0.99, grad_end), + ) amplitudes = (grad_start, grad_amp, grad_amp, grad_end) - + g_true = make_extended_trapezoid(channel='x', amplitudes=amplitudes, times=times) # Recreate gradient using make_extended_trapezoid_area - g,_,_ = make_extended_trapezoid_area(channel='x', grad_start=grad_start, grad_end=grad_end, area=g_true.area, system=system) + g, _, _ = make_extended_trapezoid_area( + channel='x', grad_start=grad_start, grad_end=grad_end, area=g_true.area, system=system + ) grad_ok = all(abs(g.waveform) <= system.max_grad) slew_ok = all(abs(np.diff(g.waveform) / np.diff(g.tt)) <= system.max_slew) assert pytest.approx(g.area) == g_true.area, 'Result area is not correct' assert grad_ok, 'Maximum gradient strength violated' assert slew_ok, 'Maximum slew rate violated' - + # Check that new gradient duration is smaller or equal to original gradient duration - d1 = calc_duration(g) + d1 = calc_duration(g) d2 = calc_duration(g_true) - + assert pytest.approx(d1) == d2 or d1 < d2 diff --git a/pypulseq/tests/test_make_gauss_pulse.py b/pypulseq/tests/test_make_gauss_pulse.py index 4313a14..9560b8a 100644 --- a/pypulseq/tests/test_make_gauss_pulse.py +++ b/pypulseq/tests/test_make_gauss_pulse.py @@ -12,10 +12,7 @@ def test_use(): - - with pytest.raises( - ValueError, - match=r"Invalid use parameter. Must be one of"): + with pytest.raises(ValueError, match=r'Invalid use parameter. Must be one of'): make_gauss_pulse(flip_angle=1, use='invalid') for use in get_supported_rf_uses(): diff --git a/pypulseq/tests/test_make_trapezoid.py b/pypulseq/tests/test_make_trapezoid.py index e157d2c..e76a151 100644 --- a/pypulseq/tests/test_make_trapezoid.py +++ b/pypulseq/tests/test_make_trapezoid.py @@ -11,71 +11,58 @@ def test_channel_error(): - - with pytest.raises( - ValueError, - match=r"Invalid channel. Must be one of `x`, `y` or `z`. Passed:"): + with pytest.raises(ValueError, match=r'Invalid channel. Must be one of `x`, `y` or `z`. Passed:'): make_trapezoid(channel='p') def test_falltime_risetime_error(): with pytest.raises( - ValueError, - match=r"Invalid arguments. Must always supply `rise_time` if `fall_time` is specified explicitly."): + ValueError, match=r'Invalid arguments. Must always supply `rise_time` if `fall_time` is specified explicitly.' + ): make_trapezoid(channel='x', fall_time=10) def test_area_flatarea_amplitude_error(): - with pytest.raises( - ValueError, - match=r"Must supply either 'area', 'flat_area' or 'amplitude'."): + with pytest.raises(ValueError, match=r"Must supply either 'area', 'flat_area' or 'amplitude'."): make_trapezoid(channel='x') def test_flat_time_error(): - errstr = "When `flat_time` is provided, either `flat_area`, "\ - "or `amplitude`, or `rise_time` and `area` must be provided as well." + errstr = ( + 'When `flat_time` is provided, either `flat_area`, ' + 'or `amplitude`, or `rise_time` and `area` must be provided as well.' + ) - with pytest.raises( - ValueError, - match=errstr): + with pytest.raises(ValueError, match=errstr): make_trapezoid(channel='x', flat_time=10, area=10) def test_area_too_large_error(): - errstr = "Requested area is too large for this gradient. Minimum required duration is" + errstr = 'Requested area is too large for this gradient. Minimum required duration is' - with pytest.raises( - AssertionError, - match=errstr): - make_trapezoid(channel='x', area=1E6, duration=1E-6) + with pytest.raises(AssertionError, match=errstr): + make_trapezoid(channel='x', area=1e6, duration=1e-6) def test_area_too_large_error_rise_time(): - errstr = "Requested area is too large for this gradient. Probably amplitude is violated" + errstr = 'Requested area is too large for this gradient. Probably amplitude is violated' - with pytest.raises( - AssertionError, - match=errstr): - make_trapezoid(channel='x', area=1E6, duration=1E-6, rise_time=1E-7) + with pytest.raises(AssertionError, match=errstr): + make_trapezoid(channel='x', area=1e6, duration=1e-6, rise_time=1e-7) def test_no_area_no_duration_error(): - errstr = "Must supply area or duration." + errstr = 'Must supply area or duration.' - with pytest.raises( - ValueError, - match=errstr): - make_trapezoid(channel='x', amplitude=1) + with pytest.raises(ValueError, match=errstr): + make_trapezoid(channel='x', amplitude=1) def test_amplitude_too_large_error(): - errstr = r"Refined amplitude \(\d+ Hz/m\) is larger than max \(\d+ Hz/m\)." + errstr = r'Refined amplitude \(\d+ Hz/m\) is larger than max \(\d+ Hz/m\).' - with pytest.raises( - AssertionError, - match=errstr): - make_trapezoid(channel='x', amplitude=1E10, duration=1) + with pytest.raises(AssertionError, match=errstr): + make_trapezoid(channel='x', amplitude=1e10, duration=1) def test_generation_methods(): @@ -87,23 +74,12 @@ def test_generation_methods(): - flat_time and amplitude - flat_time, area and rise_time """ + assert isinstance(make_trapezoid(channel='x', area=1), SimpleNamespace) - assert isinstance( - make_trapezoid(channel='x', area=1), - SimpleNamespace) - - assert isinstance( - make_trapezoid(channel='x', amplitude=1, duration=1), - SimpleNamespace) + assert isinstance(make_trapezoid(channel='x', amplitude=1, duration=1), SimpleNamespace) - assert isinstance( - make_trapezoid(channel='x', flat_time=1, flat_area=1), - SimpleNamespace) + assert isinstance(make_trapezoid(channel='x', flat_time=1, flat_area=1), SimpleNamespace) - assert isinstance( - make_trapezoid(channel='x', flat_time=1, amplitude=1), - SimpleNamespace) + assert isinstance(make_trapezoid(channel='x', flat_time=1, amplitude=1), SimpleNamespace) - assert isinstance( - make_trapezoid(channel='x', flat_time=0.5, area=1, rise_time=0.1), - SimpleNamespace) + assert isinstance(make_trapezoid(channel='x', flat_time=0.5, area=1, rise_time=0.1), SimpleNamespace) diff --git a/pypulseq/tests/test_MPRAGE.py b/pypulseq/tests/test_mprage.py similarity index 59% rename from pypulseq/tests/test_MPRAGE.py rename to pypulseq/tests/test_mprage.py index 22c11ec..182374d 100644 --- a/pypulseq/tests/test_MPRAGE.py +++ b/pypulseq/tests/test_mprage.py @@ -1,22 +1,22 @@ import unittest -from pypulseq.seq_examples.scripts import write_MPRAGE -from pypulseq.tests import base - import pytest +from pypulseq.seq_examples.scripts import write_mprage +from pypulseq.tests import base + @pytest.mark.matlab_seq_comp class TestMPRAGE(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "mprage_matlab.seq" - pypulseq_seq_filename = "mprage_pypulseq.seq" + matlab_seq_filename = 'mprage_matlab.seq' + pypulseq_seq_filename = 'mprage_pypulseq.seq' base.main( - script=write_MPRAGE, + script=write_mprage, matlab_seq_filename=matlab_seq_filename, pypulseq_seq_filename=pypulseq_seq_filename, ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_sequence.py b/pypulseq/tests/test_sequence.py index 462a321..62f2028 100644 --- a/pypulseq/tests/test_sequence.py +++ b/pypulseq/tests/test_sequence.py @@ -1,17 +1,15 @@ -import os import math +import os from pathlib import Path +from unittest.mock import patch import pytest -from unittest.mock import patch +import pypulseq as pp from pypulseq import Sequence from pypulseq.tests.base import Approx, compare_seq_file -import pypulseq as pp - - -expected_output_path = Path(__file__).parent / "expected_output" +expected_output_path = Path(__file__).parent / 'expected_output' # Sequence which contains only a gauss pulse @@ -28,12 +26,12 @@ def seq_make_gauss_pulse(): def seq1(): seq = Sequence() seq.add_block(pp.make_block_pulse(math.pi / 4, duration=1e-3)) - seq.add_block(pp.make_trapezoid("x", area=1000)) - seq.add_block(pp.make_trapezoid("y", area=-500.00001)) - seq.add_block(pp.make_trapezoid("z", area=100)) - seq.add_block(pp.make_trapezoid("x", area=-1000), pp.make_trapezoid("y", area=500)) - seq.add_block(pp.make_trapezoid("y", area=-500), pp.make_trapezoid("z", area=1000)) - seq.add_block(pp.make_trapezoid("x", area=-1000), pp.make_trapezoid("z", area=1000.00001)) + seq.add_block(pp.make_trapezoid('x', area=1000)) + seq.add_block(pp.make_trapezoid('y', area=-500.00001)) + seq.add_block(pp.make_trapezoid('z', area=100)) + seq.add_block(pp.make_trapezoid('x', area=-1000), pp.make_trapezoid('y', area=500)) + seq.add_block(pp.make_trapezoid('y', area=-500), pp.make_trapezoid('z', area=1000)) + seq.add_block(pp.make_trapezoid('x', area=-1000), pp.make_trapezoid('z', area=1000.00001)) return seq @@ -42,11 +40,11 @@ def seq1(): def seq2(): seq = Sequence() seq.add_block(pp.make_block_pulse(math.pi / 2, duration=1e-3)) - seq.add_block(pp.make_trapezoid("x", area=1000)) - seq.add_block(pp.make_trapezoid("x", area=-1000)) + seq.add_block(pp.make_trapezoid('x', area=1000)) + seq.add_block(pp.make_trapezoid('x', area=-1000)) seq.add_block(pp.make_block_pulse(math.pi, duration=1e-3)) - seq.add_block(pp.make_trapezoid("x", area=-500)) - seq.add_block(pp.make_trapezoid("x", area=1000, duration=10e-3), pp.make_adc(num_samples=100, duration=10e-3)) + seq.add_block(pp.make_trapezoid('x', area=-500)) + seq.add_block(pp.make_trapezoid('x', area=1000, duration=10e-3), pp.make_adc(num_samples=100, duration=10e-3)) return seq @@ -57,13 +55,13 @@ def seq3(): for i in range(10): seq.add_block(pp.make_block_pulse(math.pi / 8, duration=1e-3)) - seq.add_block(pp.make_trapezoid("x", area=1000)) - seq.add_block(pp.make_trapezoid("y", area=-500 + i * 100)) - seq.add_block(pp.make_trapezoid("x", area=-500)) + seq.add_block(pp.make_trapezoid('x', area=1000)) + seq.add_block(pp.make_trapezoid('y', area=-500 + i * 100)) + seq.add_block(pp.make_trapezoid('x', area=-500)) seq.add_block( - pp.make_trapezoid("x", area=1000, duration=10e-3), + pp.make_trapezoid('x', area=1000, duration=10e-3), pp.make_adc(num_samples=100, duration=10e-3), - pp.make_label(label="LIN", type="INC", value=1), + pp.make_label(label='LIN', type='INC', value=1), ) return seq @@ -75,13 +73,13 @@ def seq4(): for i in range(10): seq.add_block(pp.make_block_pulse(math.pi / 8, duration=1e-3)) - seq.add_block(pp.make_trapezoid("x", area=1000)) - seq.add_block(pp.make_trapezoid("y", area=-500 + i * 100)) - seq.add_block(pp.make_trapezoid("x", area=-500)) + seq.add_block(pp.make_trapezoid('x', area=1000)) + seq.add_block(pp.make_trapezoid('y', area=-500 + i * 100)) + seq.add_block(pp.make_trapezoid('x', area=-500)) seq.add_block( - pp.make_trapezoid("x", area=1000, duration=10e-3), + pp.make_trapezoid('x', area=1000, duration=10e-3), pp.make_adc(num_samples=100, duration=10e-3), - pp.make_label(label="LIN", type="SET", value=i), + pp.make_label(label='LIN', type='SET', value=i), ) return seq @@ -95,19 +93,19 @@ def seq4(): # This "test" rewrites the expected .seq output files when SAVE_EXPECTED is # set in the environment variables. # E.g. in a unix-based system, run: SAVE_EXPECTED=1 pytest test_sequence.py -@pytest.mark.skipif(not os.environ.get("SAVE_EXPECTED"), reason="Only save sequence files when requested") -@pytest.mark.parametrize("seq_func", sequence_zoo) +@pytest.mark.skipif(not os.environ.get('SAVE_EXPECTED'), reason='Only save sequence files when requested') +@pytest.mark.parametrize('seq_func', sequence_zoo) def test_sequence_save_expected(seq_func): seq_name = str(seq_func.__name__) # Generate sequence and write to file seq = seq_func() - seq.write(expected_output_path / (seq_name + ".seq")) + seq.write(expected_output_path / (seq_name + '.seq')) # Test whether a sequence can be plotted. -@pytest.mark.parametrize("seq_func", sequence_zoo) -@patch("matplotlib.pyplot.show") +@pytest.mark.parametrize('seq_func', sequence_zoo) +@patch('matplotlib.pyplot.show') def test_plot(mock_show, seq_func): seq = seq_func() @@ -117,10 +115,10 @@ def test_plot(mock_show, seq_func): # Test whether the sequence is the approximately the same after writing a .seq # file and reading it back in. -@pytest.mark.parametrize("seq_func", sequence_zoo) +@pytest.mark.parametrize('seq_func', sequence_zoo) def test_sequence_writeread(seq_func, tmp_path): seq_name = str(seq_func.__name__) - output_filename = tmp_path / (seq_name + ".seq") + output_filename = tmp_path / (seq_name + '.seq') # Generate sequence seq = seq_func() @@ -129,7 +127,7 @@ def test_sequence_writeread(seq_func, tmp_path): seq.write(output_filename) # Check if written sequence file matches expected sequence file - compare_seq_file(output_filename, expected_output_path / (seq_name + ".seq")) + compare_seq_file(output_filename, expected_output_path / (seq_name + '.seq')) # Read written sequence file back in seq2 = pp.Sequence(system=seq.system) @@ -143,14 +141,14 @@ def test_sequence_writeread(seq_func, tmp_path): for block_counter in seq.block_events: assert seq2.get_block(block_counter) == Approx( seq.get_block(block_counter), abs=1e-6, rel=1e-5 - ), f"Block {block_counter} does not match" + ), f'Block {block_counter} does not match' # Test for approximate equality of all gradient waveforms for a, b in zip(seq2.get_gradients(), seq.get_gradients()): if a is None and b is None: continue if a is None or b is None: - assert False + raise AssertionError() assert a.x == Approx(b.x, abs=1e-3, rel=1e-3) assert a.c == Approx(b.c, abs=1e-3, rel=1e-3) @@ -159,19 +157,19 @@ def test_sequence_writeread(seq_func, tmp_path): assert seq2.calculate_kspace() == Approx(seq.calculate_kspace(), abs=1e-2, nan_ok=True) # Test whether labels are the same - labels_seq = seq.evaluate_labels(evolution="blocks") - labels_seq2 = seq2.evaluate_labels(evolution="blocks") + labels_seq = seq.evaluate_labels(evolution='blocks') + labels_seq2 = seq2.evaluate_labels(evolution='blocks') - assert labels_seq.keys() == labels_seq2.keys(), "Sequences do not contain the same set of labels" + assert labels_seq.keys() == labels_seq2.keys(), 'Sequences do not contain the same set of labels' for label in labels_seq: - assert (labels_seq[label] == labels_seq2[label]).all(), f"Label {label} does not match" + assert (labels_seq[label] == labels_seq2[label]).all(), f'Label {label} does not match' # Test whether the sequence is approximately the same after recreating it by # getting all blocks with get_block and inserting them into a new sequence # with add_block. -@pytest.mark.parametrize("seq_func", sequence_zoo) +@pytest.mark.parametrize('seq_func', sequence_zoo) def test_sequence_recreate(seq_func, tmp_path): # Generate sequence seq = seq_func() @@ -186,14 +184,14 @@ def test_sequence_recreate(seq_func, tmp_path): for block_counter in seq.block_events: assert seq2.get_block(block_counter) == Approx( seq.get_block(block_counter), abs=1e-6, rel=1e-5 - ), f"Block {block_counter} does not match" + ), f'Block {block_counter} does not match' # Test for approximate equality of all gradient waveforms for a, b in zip(seq2.get_gradients(), seq.get_gradients()): if a is None and b is None: continue if a is None or b is None: - assert False + raise AssertionError() assert a.x == Approx(b.x, abs=1e-4, rel=1e-4) assert a.c == Approx(b.c, abs=1e-4, rel=1e-4) @@ -202,10 +200,10 @@ def test_sequence_recreate(seq_func, tmp_path): assert seq2.calculate_kspace() == Approx(seq.calculate_kspace(), abs=1e-6, nan_ok=True) # Test whether labels are the same - labels_seq = seq.evaluate_labels(evolution="blocks") - labels_seq2 = seq2.evaluate_labels(evolution="blocks") + labels_seq = seq.evaluate_labels(evolution='blocks') + labels_seq2 = seq2.evaluate_labels(evolution='blocks') - assert labels_seq.keys() == labels_seq2.keys(), "Sequences do not contain the same set of labels" + assert labels_seq.keys() == labels_seq2.keys(), 'Sequences do not contain the same set of labels' for label in labels_seq: - assert (labels_seq[label] == labels_seq2[label]).all(), f"Label {label} does not match" + assert (labels_seq[label] == labels_seq2[label]).all(), f'Label {label} does not match' diff --git a/pypulseq/tests/test_sigpy.py b/pypulseq/tests/test_sigpy.py index 390ee84..e07fa34 100644 --- a/pypulseq/tests/test_sigpy.py +++ b/pypulseq/tests/test_sigpy.py @@ -1,9 +1,10 @@ # sms - check MB # slr - check slice profile -import pytest +import importlib.util import numpy as np +import pytest import pypulseq as pp from pypulseq.opts import Opts @@ -11,19 +12,21 @@ def test_sigpy_import(): - try: - from pypulseq.make_sigpy_pulse import sigpy_n_seq - except (ImportError, ModuleNotFoundError): - with pytest.raises(ModuleNotFoundError, match="SigPy is not installed."): - from pypulseq.make_sigpy_pulse import sigpy_n_seq + if importlib.util.find_spec('pypulseq.make_sigpy_pulse'): + # Attempt to import and ensure no issues + pass + else: + with pytest.raises(ModuleNotFoundError, match='SigPy is not installed.'): + raise ModuleNotFoundError('SigPy is not installed.') @pytest.mark.sigpy def test_slr(): - from pypulseq.make_sigpy_pulse import sigpy_n_seq import sigpy.mri.rf as rf - print("Testing SLR design") + from pypulseq.make_sigpy_pulse import sigpy_n_seq + + print('Testing SLR design') time_bw_product = 4 slice_thickness = 3e-3 # Slice thickness @@ -31,22 +34,22 @@ def test_slr(): # Set system limits system = Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, ) pulse_cfg = SigpyPulseOpts( - pulse_type="slr", - ptype="st", - ftype="ls", + pulse_type='slr', + ptype='st', + ftype='ls', d1=0.01, d2=0.01, cancel_alpha_phs=False, n_bands=3, band_sep=20, - phs_0_pt="None", + phs_0_pt='None', ) rfp, gz, _, pulse = sigpy_n_seq( flip_angle=flip_angle, @@ -67,20 +70,21 @@ def test_slr(): np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000), True, ) - Mxy = 2 * np.multiply(np.conj(a), b) + mag_xy = 2 * np.multiply(np.conj(a), b) # pl.LinePlot(Mxy) # print(np.sum(np.abs(Mxy))) # peaks, dict = sis.find_peaks(np.abs(Mxy),threshold=0.5, plateau_size=40) - plateau_widths = np.sum(np.abs(Mxy) > 0.8) - assert 29 == plateau_widths + plateau_widths = np.sum(np.abs(mag_xy) > 0.8) + assert plateau_widths == 29 @pytest.mark.sigpy def test_sms(): - from pypulseq.make_sigpy_pulse import sigpy_n_seq import sigpy.mri.rf as rf - print("Testing SMS design") + from pypulseq.make_sigpy_pulse import sigpy_n_seq + + print('Testing SMS design') time_bw_product = 4 slice_thickness = 3e-3 # Slice thickness @@ -89,22 +93,22 @@ def test_sms(): # Set system limits system = Opts( max_grad=32, - grad_unit="mT/m", + grad_unit='mT/m', max_slew=130, - slew_unit="T/m/s", + slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, ) pulse_cfg = SigpyPulseOpts( - pulse_type="sms", - ptype="st", - ftype="ls", + pulse_type='sms', + ptype='st', + ftype='ls', d1=0.01, d2=0.01, cancel_alpha_phs=False, n_bands=n_bands, band_sep=20, - phs_0_pt="None", + phs_0_pt='None', ) rfp, gz, _, pulse = sigpy_n_seq( flip_angle=flip_angle, @@ -125,10 +129,10 @@ def test_sms(): np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000), True, ) - Mxy = 2 * np.multiply(np.conj(a), b) + mag_xy = 2 * np.multiply(np.conj(a), b) # pl.LinePlot(Mxy) # print(np.sum(np.abs(Mxy))) # peaks, dict = sis.find_peaks(np.abs(Mxy),threshold=0.5, plateau_size=40) - plateau_widths = np.sum(np.abs(Mxy) > 0.8) + plateau_widths = np.sum(np.abs(mag_xy) > 0.8) # if slr has 29 > 0.8, then sms with MB = n_bands assert (29 * n_bands) == plateau_widths diff --git a/pypulseq/tests/test_tse.py b/pypulseq/tests/test_tse.py index 0a3e412..52fc07e 100644 --- a/pypulseq/tests/test_tse.py +++ b/pypulseq/tests/test_tse.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_tse from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestTSE(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "tse_matlab.seq" - pypulseq_seq_filename = "tse_pypulseq.seq" + matlab_seq_filename = 'tse_matlab.seq' + pypulseq_seq_filename = 'tse_pypulseq.seq' base.main( script=write_tse, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/tests/test_ute.py b/pypulseq/tests/test_ute.py index bf3b7a3..37bbfbd 100644 --- a/pypulseq/tests/test_ute.py +++ b/pypulseq/tests/test_ute.py @@ -1,16 +1,16 @@ import unittest +import pytest + from pypulseq.seq_examples.scripts import write_ute from pypulseq.tests import base -import pytest - @pytest.mark.matlab_seq_comp class TestUTE(unittest.TestCase): def test_write_epi(self): - matlab_seq_filename = "ute_matlab.seq" - pypulseq_seq_filename = "ute_pypulseq.seq" + matlab_seq_filename = 'ute_matlab.seq' + pypulseq_seq_filename = 'ute_pypulseq.seq' base.main( script=write_ute, matlab_seq_filename=matlab_seq_filename, @@ -18,5 +18,5 @@ def test_write_epi(self): ) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/pypulseq/traj_to_grad.py b/pypulseq/traj_to_grad.py index e583b25..0e1d5e2 100644 --- a/pypulseq/traj_to_grad.py +++ b/pypulseq/traj_to_grad.py @@ -1,11 +1,11 @@ -from typing import Tuple +from typing import Tuple, Union import numpy as np from pypulseq.opts import Opts -def traj_to_grad(k: np.ndarray, raster_time: float = None) -> Tuple[np.ndarray, np.ndarray]: +def traj_to_grad(k: np.ndarray, raster_time: Union[float, None] = None) -> Tuple[np.ndarray, np.ndarray]: """ Convert k-space trajectory `k` into gradient waveform in compliance with `raster_time` gradient raster time. diff --git a/pypulseq/utils/safe_pns_prediction.py b/pypulseq/utils/safe_pns_prediction.py index cbc50cf..0fbe9f2 100644 --- a/pypulseq/utils/safe_pns_prediction.py +++ b/pypulseq/utils/safe_pns_prediction.py @@ -37,8 +37,8 @@ from types import SimpleNamespace -import numpy as np import matplotlib.pyplot as plt +import numpy as np def safe_example_hw(): @@ -48,9 +48,9 @@ def safe_example_hw(): # See comments for units. hw = SimpleNamespace() - hw.name = "MP_GPA_EXAMPLE" - hw.checksum = "1234567890" - hw.dependency = "" + hw.name = 'MP_GPA_EXAMPLE' + hw.checksum = '1234567890' + hw.dependency = '' hw.x = SimpleNamespace() hw.x.tau1 = 0.20 # ms @@ -199,10 +199,10 @@ def safe_hw_check(hw): or abs(hw.y.a1 + hw.y.a2 + hw.y.a3 - 1) > 0.001 or abs(hw.z.a1 + hw.z.a2 + hw.z.a3 - 1) > 0.001 ): - raise ValueError("Hardware specification a1+a2+a3 must be equal to 1!") + raise ValueError('Hardware specification a1+a2+a3 must be equal to 1!') - axl = ["x", "y", "z"] - fnl = ["stim_limit", "stim_thresh", "tau1", "tau2", "tau3", "a1", "a2", "a3", "g_scale"] + axl = ['x', 'y', 'z'] + fnl = ['stim_limit', 'stim_thresh', 'tau1', 'tau2', 'tau3', 'a1', 'a2', 'a3', 'g_scale'] for axn in axl: if not hasattr(hw, axn): @@ -324,7 +324,7 @@ def safe_gwf_to_pns(gwf, rf, dt, hw, do_padding=True): pns[:, 1] = safe_pns_model(dgdt[:, 1], dt, hw.y) pns[:, 2] = safe_pns_model(dgdt[:, 2], dt, hw.z) - # Export relevant paramters + # Export relevant parameters res = SimpleNamespace() res.pns = pns res.gwf = gwf @@ -346,48 +346,48 @@ def safe_plot(pns, dt=None, envelope=True, envelope_points=500): # FZ: Added option to plot the moving maximum of pns and pnsnorm to keep # plots for long sequences intelligible if envelope and pns.shape[0] > envelope_points: - N = int(np.ceil(pns.shape[0] / envelope_points)) + n_samples = int(np.ceil(pns.shape[0] / envelope_points)) if dt is not None: - dt *= N + dt *= n_samples - if pns.shape[0] % N != 0: - pns = np.concatenate((pns, np.zeros((N - pns.shape[0] % N, pns.shape[1])))) - pnsnorm = np.concatenate((pnsnorm, np.zeros((N - pnsnorm.shape[0] % N)))) + if pns.shape[0] % n_samples != 0: + pns = np.concatenate((pns, np.zeros((n_samples - pns.shape[0] % n_samples, pns.shape[1])))) + pnsnorm = np.concatenate((pnsnorm, np.zeros((n_samples - pnsnorm.shape[0] % n_samples)))) - pns = pns.reshape(pns.shape[0] // N, N, pns.shape[1]) + pns = pns.reshape(pns.shape[0] // n_samples, n_samples, pns.shape[1]) pns = pns.max(axis=1) - pnsnorm = pnsnorm.reshape(pnsnorm.shape[0] // N, N) + pnsnorm = pnsnorm.reshape(pnsnorm.shape[0] // n_samples, n_samples) pnsnorm = pnsnorm.max(axis=1) if dt is None: ttot = 1 # au - xlabstr = "Time [a.u.]" + xlabstr = 'Time [a.u.]' else: ttot = pns.shape[0] * dt * 1000 # ms - xlabstr = "Time [ms]" + xlabstr = 'Time [ms]' t = np.linspace(0, ttot, pns.shape[0]) - plt.plot(t, pns[:, 0], "r-", t, pns[:, 1], "g-", t, pns[:, 2], "b-", t, pnsnorm, "k-") + plt.plot(t, pns[:, 0], 'r-', t, pns[:, 1], 'g-', t, pns[:, 2], 'b-', t, pnsnorm, 'k-') plt.ylim([0, 120]) plt.xlim([min(t), max(t)]) - plt.title(f"Predicted PNS ({max(pnsnorm):0.0f}%)") + plt.title(f'Predicted PNS ({max(pnsnorm):0.0f}%)') plt.xlabel(xlabstr) - plt.ylabel("Relative stimulation [%]") + plt.ylabel('Relative stimulation [%]') - plt.plot([0, max(t)], [max(pnsnorm), max(pnsnorm)], "k:") + plt.plot([0, max(t)], [max(pnsnorm), max(pnsnorm)], 'k:') plt.legend( [ - f"X ({max(pns[:,0]):0.0f}%)", - f"Y ({max(pns[:,1]):0.0f}%)", - f"Z ({max(pns[:,2]):0.0f}%)", - f"nrm ({max(pnsnorm):0.0f}%)", + f'X ({max(pns[:,0]):0.0f}%)', + f'Y ({max(pns[:,1]):0.0f}%)', + f'Z ({max(pns[:,2]):0.0f}%)', + f'nrm ({max(pnsnorm):0.0f}%)', ], - loc="best", + loc='best', ) @@ -395,7 +395,7 @@ def safe_example(): # Load an exampe gradient waveform [gwf, rf, dt] = safe_example_gwf() - # Load reponse parameters for example hardware + # Load response parameters for example hardware hw = safe_example_hw() # Check if hardware parameters are consistent @@ -411,5 +411,5 @@ def safe_example(): safe_plot(pns, dt) -if __name__ == "__main__": +if __name__ == '__main__': safe_example() diff --git a/pypulseq/utils/siemens/asc_to_hw.py b/pypulseq/utils/siemens/asc_to_hw.py index a38a065..9e2cc0b 100644 --- a/pypulseq/utils/siemens/asc_to_hw.py +++ b/pypulseq/utils/siemens/asc_to_hw.py @@ -1,9 +1,10 @@ from types import SimpleNamespace from typing import List + import numpy as np -def asc_to_acoustic_resonances(asc : dict) -> List[dict]: +def asc_to_acoustic_resonances(asc: dict) -> List[dict]: """ Convert ASC dictionary from readasc to list of acoustic resonances @@ -17,17 +18,17 @@ def asc_to_acoustic_resonances(asc : dict) -> List[dict]: List[dict] List of acoustic resonances (specified by frequency and bandwidth fields). """ - if 'aflGCAcousticResonanceFrequency' in asc: freqs = asc['aflGCAcousticResonanceFrequency'] bw = asc['aflGCAcousticResonanceBandwidth'] else: freqs = asc['asGPAParameters'][0]['sGCParameters']['aflAcousticResonanceFrequency'] bw = asc['asGPAParameters'][0]['sGCParameters']['aflAcousticResonanceBandwidth'] - - return [dict(frequency=f, bandwidth=b) for f,b in zip(freqs.values(), bw.values()) if f != 0] -def asc_to_hw(asc : dict, cardiac_model : bool = False) -> SimpleNamespace: + return [{'frequency': f, 'bandwidth': b} for f, b in zip(freqs.values(), bw.values()) if f != 0] + + +def asc_to_hw(asc: dict, cardiac_model: bool = False) -> SimpleNamespace: """ Convert ASC dictionary from readasc to SAFE hardware description. @@ -45,7 +46,7 @@ def asc_to_hw(asc : dict, cardiac_model : bool = False) -> SimpleNamespace: SAFE hardware description """ hw = SimpleNamespace() - + if 'asCOMP' in asc and 'tName' in asc['asCOMP']: hw.name = asc['asCOMP']['tName'] else: @@ -55,7 +56,7 @@ def asc_to_hw(asc : dict, cardiac_model : bool = False) -> SimpleNamespace: asc_pns = asc['GradPatSup']['Phys']['PNS'] else: asc_pns = asc - + if cardiac_model: if 'GradPatSup' in asc and 'CarNS' in asc['GradPatSup']['Phys']: asc_pns = asc['GradPatSup']['Phys']['CarNS'] @@ -63,43 +64,43 @@ def asc_to_hw(asc : dict, cardiac_model : bool = False) -> SimpleNamespace: return None hw.x = SimpleNamespace() - hw.x.tau1 = asc_pns['flGSWDTauX'][0] # ms - hw.x.tau2 = asc_pns['flGSWDTauX'][1] # ms - hw.x.tau3 = asc_pns['flGSWDTauX'][2] # ms - hw.x.a1 = asc_pns['flGSWDAX'][0] - hw.x.a2 = asc_pns['flGSWDAX'][1] - hw.x.a3 = asc_pns['flGSWDAX'][2] - hw.x.stim_limit = asc_pns['flGSWDStimulationLimitX'] # T/m/s + hw.x.tau1 = asc_pns['flGSWDTauX'][0] # ms + hw.x.tau2 = asc_pns['flGSWDTauX'][1] # ms + hw.x.tau3 = asc_pns['flGSWDTauX'][2] # ms + hw.x.a1 = asc_pns['flGSWDAX'][0] + hw.x.a2 = asc_pns['flGSWDAX'][1] + hw.x.a3 = asc_pns['flGSWDAX'][2] + hw.x.stim_limit = asc_pns['flGSWDStimulationLimitX'] # T/m/s hw.x.stim_thresh = asc_pns['flGSWDStimulationThresholdX'] # T/m/s - + hw.y = SimpleNamespace() - hw.y.tau1 = asc_pns['flGSWDTauY'][0] # ms - hw.y.tau2 = asc_pns['flGSWDTauY'][1] # ms - hw.y.tau3 = asc_pns['flGSWDTauY'][2] # ms - hw.y.a1 = asc_pns['flGSWDAY'][0] - hw.y.a2 = asc_pns['flGSWDAY'][1] - hw.y.a3 = asc_pns['flGSWDAY'][2] - hw.y.stim_limit = asc_pns['flGSWDStimulationLimitY'] # T/m/s + hw.y.tau1 = asc_pns['flGSWDTauY'][0] # ms + hw.y.tau2 = asc_pns['flGSWDTauY'][1] # ms + hw.y.tau3 = asc_pns['flGSWDTauY'][2] # ms + hw.y.a1 = asc_pns['flGSWDAY'][0] + hw.y.a2 = asc_pns['flGSWDAY'][1] + hw.y.a3 = asc_pns['flGSWDAY'][2] + hw.y.stim_limit = asc_pns['flGSWDStimulationLimitY'] # T/m/s hw.y.stim_thresh = asc_pns['flGSWDStimulationThresholdY'] # T/m/s - + hw.z = SimpleNamespace() - hw.z.tau1 = asc_pns['flGSWDTauZ'][0] # ms - hw.z.tau2 = asc_pns['flGSWDTauZ'][1] # ms - hw.z.tau3 = asc_pns['flGSWDTauZ'][2] # ms - hw.z.a1 = asc_pns['flGSWDAZ'][0] - hw.z.a2 = asc_pns['flGSWDAZ'][1] - hw.z.a3 = asc_pns['flGSWDAZ'][2] - hw.z.stim_limit = asc_pns['flGSWDStimulationLimitZ'] # T/m/s + hw.z.tau1 = asc_pns['flGSWDTauZ'][0] # ms + hw.z.tau2 = asc_pns['flGSWDTauZ'][1] # ms + hw.z.tau3 = asc_pns['flGSWDTauZ'][2] # ms + hw.z.a1 = asc_pns['flGSWDAZ'][0] + hw.z.a2 = asc_pns['flGSWDAZ'][1] + hw.z.a3 = asc_pns['flGSWDAZ'][2] + hw.z.stim_limit = asc_pns['flGSWDStimulationLimitZ'] # T/m/s hw.z.stim_thresh = asc_pns['flGSWDStimulationThresholdZ'] # T/m/s - + if 'asGPAParameters' in asc: - hw.x.g_scale = asc['asGPAParameters'][0]['sGCParameters']['flGScaleFactorX'] - hw.y.g_scale = asc['asGPAParameters'][0]['sGCParameters']['flGScaleFactorY'] - hw.z.g_scale = asc['asGPAParameters'][0]['sGCParameters']['flGScaleFactorZ'] + hw.x.g_scale = asc['asGPAParameters'][0]['sGCParameters']['flGScaleFactorX'] + hw.y.g_scale = asc['asGPAParameters'][0]['sGCParameters']['flGScaleFactorY'] + hw.z.g_scale = asc['asGPAParameters'][0]['sGCParameters']['flGScaleFactorZ'] else: print('Warning: Gradient scale factors not in ASC file: assuming 1/pi') - hw.x.g_scale = 1/np.pi - hw.y.g_scale = 1/np.pi - hw.z.g_scale = 1/np.pi - + hw.x.g_scale = 1 / np.pi + hw.y.g_scale = 1 / np.pi + hw.z.g_scale = 1 / np.pi + return hw diff --git a/pypulseq/utils/siemens/readasc.py b/pypulseq/utils/siemens/readasc.py index 339926d..3961845 100644 --- a/pypulseq/utils/siemens/readasc.py +++ b/pypulseq/utils/siemens/readasc.py @@ -22,20 +22,19 @@ def readasc(filename: str) -> Tuple[dict, dict]: extra : dict Dictionary of other fields after "ASCCONV END" """ - asc, extra = {}, {} # Read asc file and convert it into a dictionary structure - with open(filename, "r") as fp: + with open(filename, 'r') as fp: end_of_asc = False for next_line in fp: next_line = next_line.strip() - if next_line == "### ASCCONV END ###": # find end of mrProt in the asc file + if next_line == '### ASCCONV END ###': # find end of mrProt in the asc file end_of_asc = True - if next_line == "" or next_line[0] == "#": + if next_line == '' or next_line[0] == '#': continue # regex wizardry: Matches lines like 'a[0].b[2][3].c = "string" # comment' @@ -58,31 +57,31 @@ def readasc(filename: str) -> Tuple[dict, dict]: assign_to = None # Iterate over every segment of the field name - parts = field_name.split(".") + parts = field_name.split('.') for p in parts: - # Update base so final assignement is like: base[assign_to][p] = value + # Update base so final assignment is like: base[assign_to][p] = value if assign_to is not None and assign_to not in base: base[assign_to] = {} if assign_to is not None: base = base[assign_to] # Iterate over brackets - start = p.find("[") + start = p.find('[') if start != -1: name = p[:start] assign_to = name while start != -1: - stop = p.find("]", start) + stop = p.find(']', start) index = int(p[start + 1 : stop]) - # Update base so final assignement is like: base[assign_to][p][index] = value + # Update base so final assignment is like: base[assign_to][p][index] = value if assign_to not in base: base[assign_to] = {} base = base[assign_to] assign_to = index - start = p.find("[", stop) + start = p.find('[', stop) else: assign_to = p @@ -94,8 +93,8 @@ def readasc(filename: str) -> Tuple[dict, dict]: elif match[5]: base[assign_to] = float(match[5]) else: - raise RuntimeError("This should not be reached") - elif next_line.find("=") != -1: - raise RuntimeError(f"Bug: ASC line with an assignment was not parsed correctly: {next_line}") + raise RuntimeError('This should not be reached') + elif next_line.find('=') != -1: + raise RuntimeError(f'Bug: ASC line with an assignment was not parsed correctly: {next_line}') return asc, extra diff --git a/pypulseq/utils/tracing.py b/pypulseq/utils/tracing.py index 9b7f3dc..59ff86b 100644 --- a/pypulseq/utils/tracing.py +++ b/pypulseq/utils/tracing.py @@ -1,10 +1,11 @@ -import traceback import sys +import traceback # Global variables indicating whether tracing is enabled and how deep the # calls are traced. -_tracing : bool = False -_trace_limit : int = 1 +_tracing: bool = False +_trace_limit: int = 1 + def trace_enabled() -> bool: """ @@ -14,6 +15,7 @@ def trace_enabled() -> bool: global _tracing return _tracing + def enable_trace(limit: int = 1) -> None: """ Enable tracing where sequence events and blocks were created. Note that @@ -31,6 +33,7 @@ def enable_trace(limit: int = 1) -> None: _tracing = True _trace_limit = limit + def disable_trace() -> None: """ Disabling tracing where sequence events and blocks were created. @@ -38,6 +41,7 @@ def disable_trace() -> None: global _tracing _tracing = False + def trace() -> traceback.StackSummary: """ Internal function to fetch a summary of the call stack. @@ -47,9 +51,10 @@ def trace() -> traceback.StackSummary: traceback.StackSummary Call stack summary. """ - f = sys._getframe().f_back.f_back # type: ignore + f = sys._getframe().f_back.f_back # type: ignore return traceback.extract_stack(f, limit=_trace_limit) + def format_trace(trace: traceback.StackSummary, indent: int = 0) -> str: """ Format a stack summary into a string. Optionally adds `indent` spaces @@ -67,4 +72,4 @@ def format_trace(trace: traceback.StackSummary, indent: int = 0) -> str: str Stack summary formatted into a printable string. """ - return '\n'.join(' '*indent + y for x in trace.format() for y in x.splitlines()) + return '\n'.join(' ' * indent + y for x in trace.format() for y in x.splitlines())