diff --git a/docs/api/sequence.md b/docs/api/sequence.md index 4110923a..7871a565 100644 --- a/docs/api/sequence.md +++ b/docs/api/sequence.md @@ -60,8 +60,6 @@ in-memory sequence, {func}`read_mrd_description` reads those an MRD stream carries ahead of its first acquisition, and {meth}`~blochsim.model.Simulator.from_description` simulates any of them. -Reading Pulseq needs pypulseq: `pip install blochsim[pulseq]`. - | Object | Description | | --- | --- | | {obj}`~blochsim.SequenceDescription` | Event stream and RF resources of one repetition of a sequence. | diff --git a/docs/developer-guide/internals/description.md b/docs/developer-guide/internals/description.md index b46a7c20..79eaa721 100644 --- a/docs/developer-guide/internals/description.md +++ b/docs/developer-guide/internals/description.md @@ -48,8 +48,7 @@ gradient per axis and one ADC window. One block becomes one event: a block with a pulse is an RF event, one with an ADC and no pulse an ADC event, any other a WAIT. Gradients are not events. -The file is parsed and its trajectory computed by pypulseq -(`pip install blochsim[pulseq]`); a sequence in memory with pypulseq's reading +The file is parsed and its trajectory computed by pypulseq; a sequence in memory with pypulseq's reading interface, pypulseq's or pypulseqpp's, is read the same way. The gradients are read for two things only: the k-space trajectory that places each ADC event, and the slice-select amplitude under each pulse's isocentre, the magnitude of diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 3a8bebe1..089e31f4 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -127,9 +127,8 @@ pip install "blochsim[cu13]" # plus the GPU kernels, for a CUDA 13 PyTorch The GPU kernels are compiled ahead of time and need a driver, not the CUDA toolkit. Install the extra that matches PyTorch's CUDA major version; BlochSim -refuses a mismatched one and names the extra to install. Other extras: -`pulseq` reads `.seq` files, and `examples` brings the packages the gallery -imports. +refuses a mismatched one and names the extra to install. The `examples` extra +brings the packages the gallery imports. Check the installation by simulating a fast spin echo for three tissues: diff --git a/examples/01-framework/04-description-based-simulation.py b/examples/01-framework/04-description-based-simulation.py index 3e9425a7..e235e1a1 100644 --- a/examples/01-framework/04-description-based-simulation.py +++ b/examples/01-framework/04-description-based-simulation.py @@ -24,7 +24,7 @@ # .. colab-link:: # :needs_gpu: 0 # -# !pip install "blochsim[pulseq]" matplotlib +# !pip install blochsim matplotlib # !wget --quiet --no-clobber https://raw.githubusercontent.com/pulserver/blochsim/main/docs/figure_style.py # !wget --quiet --no-clobber https://raw.githubusercontent.com/pulserver/blochsim/main/examples/01-framework/fse.seq @@ -136,9 +136,8 @@ # Pulseq file # ----------- # -# ``from_pulseq`` reads the description from a ``.seq`` file; it requires the -# ``pulseq`` extra (``pip install "blochsim[pulseq]"``). The file below is a -# short FSE: +# ``from_pulseq`` reads the description from a ``.seq`` file, parsed by +# pypulseq. The file below is a short FSE: from_file = FSESimulator.from_pulseq(SEQ_FILE, states=20) file_signal = from_file.simulate(T1=1000.0, T2=torch.tensor([80.0, 110.0])) diff --git a/pyproject.toml b/pyproject.toml index c1158de4..8e9f5ee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ dependencies = [ # torch.func (jacrev/jacfwd) and torch.linalg are used by the optimization and # estimator packages. "torch>=2.1", +# The format's reference implementation parses a Pulseq .seq file and +# computes its trajectory. +"pypulseq>=1.4", ] [project.optional-dependencies] @@ -64,15 +67,8 @@ cu12 = [ cu13 = [ "blochsim-cuda13; sys_platform == 'linux' and platform_machine == 'x86_64'", ] -# What it takes to read a Pulseq .seq file. The format's reference -# implementation parses it and computes its trajectory; nothing else in the -# package imports it. -pulseq = [ -"pypulseq>=1.4", -] # What it takes to run the suite. Nothing here is imported by the package. test = [ -"blochsim[pulseq]", "pytest>=7.0", "pytest-cov", "pytest-sugar", @@ -83,9 +79,6 @@ test = [ # import while they execute -- those are the ``examples`` extra, so a # documentation build without the gallery does not drag them in. doc = [ -# The explanation figures and Course lesson 4 read the shipped -# ``.seq`` file, which pypulseq parses. -"blochsim[pulseq]", "sphinx>=7.0", "sphinx_book_theme>=1.1", "sphinx-design>=0.6", @@ -102,7 +95,6 @@ doc = [ # phantoms, the segmentation, the coils, the Fourier encoding and the linear # solvers all come from elsewhere, which is the point several examples make. examples = [ -"blochsim[pulseq]", "brainweb-dl>=0.3", "deepinv>=0.2", "deepmriprep>=0.3", diff --git a/src/blochsim/sequence/_description.py b/src/blochsim/sequence/_description.py index 924c84ac..a20d8e95 100644 --- a/src/blochsim/sequence/_description.py +++ b/src/blochsim/sequence/_description.py @@ -852,8 +852,6 @@ def from_operators( def from_pulseq(cls, source: Any, **settings: Any) -> SequenceDescription: """Read one repetition of a Pulseq sequence. - Requires pypulseq (``pip install blochsim[pulseq]``). - Parameters ---------- source : str, Path or sequence @@ -872,8 +870,6 @@ def from_pulseq(cls, source: Any, **settings: Any) -> SequenceDescription: Raises ------ - ImportError - If pypulseq is not installed. ValueError If the file declares no ``TRSize``, ``tr_index`` is out of range, or the repetitions differ and none was named. diff --git a/src/blochsim/sequence/_pulseq.py b/src/blochsim/sequence/_pulseq.py index 8e2bea31..c602ad3b 100644 --- a/src/blochsim/sequence/_pulseq.py +++ b/src/blochsim/sequence/_pulseq.py @@ -4,8 +4,6 @@ reference implementation of the format. What is added here is the reading a simulation needs and Pulseq does not write down: which pulse shapes the RF rows share, and which ADC sample each readout passes through k = 0 in. - -``pypulseq`` is an optional dependency -- ``pip install blochsim[pulseq]``. """ from __future__ import annotations @@ -45,13 +43,8 @@ def _read(source: Any) -> Any: """Return ``source`` if it is a sequence already, or the file it names parsed.""" if hasattr(source, "block_events"): return source - try: - import pypulseq - except ImportError as missing: # pragma: no cover - exercised by the message - raise ImportError( - "reading a Pulseq file needs pypulseq, which BlochSim does not " - "require otherwise: pip install blochsim[pulseq]" - ) from missing + import pypulseq + sequence = pypulseq.Sequence() sequence.read(str(source)) return sequence @@ -354,8 +347,6 @@ def read_pulseq_description( Raises ------ - ImportError - If pypulseq is not installed. ValueError If the file declares no ``TRSize``, if ``tr_index`` is out of range, or if the repetitions differ and none was named. diff --git a/tests/sequence/test_mrd_reader.py b/tests/sequence/test_mrd_reader.py index 8349a1fc..5a860e14 100644 --- a/tests/sequence/test_mrd_reader.py +++ b/tests/sequence/test_mrd_reader.py @@ -16,6 +16,7 @@ import struct import numpy as np +import pypulseq as pp import pytest import torch @@ -24,8 +25,6 @@ from blochsim.sequence import EventType, RfUse, read_mrd_description from blochsim.simulators import SPGRSimulator -pp = pytest.importorskip("pypulseq", reason="building the sequence needs pypulseq") - FLIP_DEG = 30.0 TR_S = 20e-3 SAMPLES, DWELL_S = 64, 10e-6 diff --git a/tests/sequence/test_pulseq_reader.py b/tests/sequence/test_pulseq_reader.py index 64dd1c43..cf72aa3a 100644 --- a/tests/sequence/test_pulseq_reader.py +++ b/tests/sequence/test_pulseq_reader.py @@ -11,6 +11,7 @@ import math import numpy as np +import pypulseq as pp import pytest import torch @@ -19,8 +20,6 @@ from blochsim.sequence import AdcRole, EventType, RfUse from blochsim.simulators import SPGRSimulator -pp = pytest.importorskip("pypulseq", reason="reading a .seq file needs pypulseq") - FLIP_DEG = 30.0 TR_S = 20e-3 FOV_M, SAMPLES, DWELL_S = 0.22, 64, 10e-6