Skip to content

Commit

Permalink
Release 1.3.1post1
Browse files Browse the repository at this point in the history
Merge pull request #50 from imr-framework/dev
  • Loading branch information
sravan953 authored May 3, 2021
2 parents 9fe6ad3 + 9e50ed6 commit cc9ccfb
Show file tree
Hide file tree
Showing 19 changed files with 98 additions and 59 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.1
1.3.1post1
2 changes: 1 addition & 1 deletion pypulseq/SAR/SAR_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def calc_SAR(file: Union[str, Path, Sequence]) -> None:

if file.exists() and file.is_file():
seq_obj = Sequence()
seq_obj.read(file)
seq_obj.read(str(file))
seq_obj = seq_obj
else:
raise ValueError('Seq file does not exist.')
Expand Down
4 changes: 2 additions & 2 deletions pypulseq/Sequence/read_seq.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,9 @@ def __read_version(input_file) -> Tuple[int, int, int]:
elif tok[0] == 'minor':
minor = int(tok[1])
elif tok[0] == 'revision':
revision = int(tok[1])
revision = tok[1]
else:
raise RuntimeError()
raise RuntimeError(f'Incompatible version. Expected: {major}{minor}{revision}')
line = __strip_line(input_file)

return major, minor, revision
Expand Down
4 changes: 1 addition & 3 deletions pypulseq/Sequence/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
class Sequence:
version_major: int = major
version_minor: int = minor
version_revision: int = revision
if isinstance(version_revision, str):
version_revision = int(version_revision[0])
version_revision = revision

def __init__(self, system=Opts()):
# =========
Expand Down
2 changes: 1 addition & 1 deletion pypulseq/Sequence/write_seq.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def write(self, file_name: str) -> None:
output_file.write(f'{keys[block_counter]} ')
if isinstance(values[block_counter], str):
output_file.write(values[block_counter] + ' ')
elif isinstance(values[block_counter], float):
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, [FOV, FOV, FOV]
for i in range(len(values[block_counter])):
Expand Down
22 changes: 20 additions & 2 deletions pypulseq/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
from pathlib import Path

import numpy as np

path_version = Path(__file__).parent.parent / 'VERSION'
with open(str(path_version), 'r') as version_file:
version = version_file.read().strip().split('.')
major, minor, revision = [int(v) for v in version]
major, minor, revision = version_file.read().strip().split('.')
major = int(major)
minor = int(minor)


# =========
# BANKER'S ROUNDING FIX
# =========
def round_half_up(n, decimals=0):
"""
Avoid banker's rounding inconsistencies; from https://realpython.com/python-rounding/#rounding-half-up
"""
multiplier = 10 ** decimals
return np.floor(np.abs(n) * multiplier + 0.5) / multiplier


# =========
# PACKAGE-LEVEL IMPORTS
# =========
from pypulseq.SAR.SAR_calc import calc_SAR
from pypulseq.Sequence.sequence import Sequence
from pypulseq.add_gradients import add_gradients
Expand Down
23 changes: 5 additions & 18 deletions pypulseq/calc_rf_center.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,10 @@ def calc_rf_center(rf: SimpleNamespace) -> Tuple[float, float]:
id_center : float
Corresponding position of `time_center` in the radio-frequency pulse's envelope.
"""
eps = np.finfo(float).eps
for first, x in enumerate(rf.signal):
if abs(x) > eps:
break

for last, x in enumerate(rf.signal[::-1]):
if abs(x) > eps:
break

# Detect the excitation peak: we traverse over in-place reverse of rf.signal; we want index from the ending
last = len(rf.signal) - last - 1
rf_min = min(abs(rf.signal[first:last + 1]))
rf_max = max(abs(rf.signal[first:last + 1]))
id_center = np.argmax(abs(rf.signal[first:last + 1]))
if rf_max - rf_min <= eps:
id_center = round((last - first + 1) / 2) - 1

time_center = rf.t[first + id_center]
# We detect the excitation peak; if i is a plateau we take its center
rf_max = max(abs(rf.signal))
i_peak = np.where(abs(rf.signal) >= rf_max * 0.99999)[0]
time_center = (rf.t[i_peak[0]] + rf.t[i_peak[-1]]) / 2
id_center = i_peak[round((len(i_peak) - 1) / 2)]

return time_center, id_center
8 changes: 7 additions & 1 deletion pypulseq/check_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def check_timing(system: Opts, *events: SimpleNamespace) -> Tuple[bool, str, flo
raster = system.grad_raster_time

if hasattr(e, 'delay'):
eps = np.finfo(np.float).eps
if e.delay < -eps:
ok = False
if not __div_check(e.delay, raster):
ok = False

Expand Down Expand Up @@ -93,6 +96,9 @@ def check_timing(system: Opts, *events: SimpleNamespace) -> Tuple[bool, str, flo
return is_ok, text_err, total_duration


def __div_check(a, b) -> bool:
def __div_check(a: float, b: float) -> bool:
"""
Checks whether `a` can be divided by `b` to an accuracy of 1e-9.
"""
c = a / b
return abs(c - np.round(c)) < 1e-9
2 changes: 1 addition & 1 deletion pypulseq/make_arbitrary_rf.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def make_arbitrary_rf(signal: np.ndarray, flip_angle: float, bandwidth: float =
signal = np.squeeze(signal)
if signal.ndim > 1:
raise ValueError(f'signal should have ndim=1. Passed ndim={signal.ndim}')
signal = signal / np.sum(signal * system.rf_raster_time) * flip_angle / (2 * np.pi)
signal = signal / bp.abs(np.sum(signal * system.rf_raster_time)) * flip_angle / (2 * np.pi)

N = len(signal)
duration = N * system.rf_raster_time
Expand Down
7 changes: 5 additions & 2 deletions pypulseq/make_block_pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def make_block_pulse(flip_angle: float, bandwidth: float = 0, delay: float = 0,
If `return_gz=True`, and `slice_thickness` is not passed.
"""
valid_use_pulses = ['excitation', 'refocusing', 'inversion']
if use is not None 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}")

Expand Down Expand Up @@ -118,4 +118,7 @@ def make_block_pulse(flip_angle: float, bandwidth: float = 0, delay: float = 0,
rf.t = np.concatenate((rf.t, (rf.t[-1] + t_fill)))
rf.signal = np.concatenate((rf.signal, np.zeros(len(t_fill))))

return rf, gz if return_gz else rf
if return_gz:
return rf, gz
else:
return rf
3 changes: 2 additions & 1 deletion pypulseq/make_gauss_pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def make_gauss_pulse(flip_angle: float, apodization: float = 0, bandwidth: float

if return_gz:
return rf, gz, gzr
return rf
else:
return rf


def __gauss(x):
Expand Down
7 changes: 5 additions & 2 deletions pypulseq/make_sinc_pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def make_sinc_pulse(flip_angle: float, apodization: float = 0, delay: float = 0,
Maximum slew rate of accompanying slice select trapezoidal event.
phase_offset : float, optional, default=0
Phase offset in Hertz (Hz).
return_gz:bool, default=True
return_gz:bool, default=False
Boolean flag to indicate if slice-selective gradient has to be returned.
slice_thickness : float, optional, default=0
Slice thickness of accompanying slice select trapezoidal event. The slice thickness determines the area of the
Expand Down Expand Up @@ -125,4 +125,7 @@ def make_sinc_pulse(flip_angle: float, apodization: float = 0, delay: float = 0,
negative_zero_indices = np.where(rf.signal == -0.0)
rf.signal[negative_zero_indices] = 0

return rf, gz, gzr if return_gz else rf
if return_gz:
return rf, gz, gzr
else:
return rf
25 changes: 14 additions & 11 deletions pypulseq/make_trap_pulse.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import math
from types import SimpleNamespace

import numpy as np

from pypulseq.opts import Opts


def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay: float = 0, duration: float = 0,
flat_area: float = 0, flat_time: float = 0, max_grad: float = 0, max_slew: float = 0,
flat_area: float = 0, flat_time: float = -1, max_grad: float = 0, max_slew: float = 0,
rise_time: float = 0, system: Opts = Opts()) -> SimpleNamespace:
"""
Creates a trapezoidal gradient event.
Expand All @@ -24,8 +26,8 @@ def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay
Duration in milliseconds (ms).
flat_area : float, optional, default=0
Flat area.
flat_time : float, optional, default=0
Flat duration in milliseconds (ms).
flat_time : float, optional, default=-1
Flat duration in milliseconds (ms). Default is -1 to account for triangular pulses.
max_grad : float, optional, default=0
Maximum gradient strength.
max_slew : float, optional, default=0
Expand All @@ -43,9 +45,10 @@ def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay
Raises
------
ValueError
- If none of `area`, `flat_area` and `amplitude` are passed
- If requested area is too large for this gradient
- Amplitude violation
If none of `area`, `flat_area` and `amplitude` are passed
If requested area is too large for this gradient
If `flat_time`, `duration` and `area` are not supplied.
Amplitude violation
"""
if channel not in ['x', 'y', 'z']:
raise ValueError(f"Invalid channel. Must be one of `x`, `y` or `z`. Passed: {channel}")
Expand All @@ -62,7 +65,7 @@ def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay
if area is None and flat_area == 0 and amplitude == 0:
raise ValueError("Must supply either 'area', 'flat_area' or 'amplitude'.")

if flat_time != 0:
if flat_time != -1:
if amplitude != 0:
amplitude2 = amplitude
else:
Expand Down Expand Up @@ -98,11 +101,11 @@ def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay
if amplitude == 0:
amplitude2 = area / (rise_time / 2 + fall_time / 2 + flat_time)
else:
if area is None:
raise ValueError('Must supply a duration')
if area == 0:
raise ValueError('Must supply a duration.')
else:
rise_time = math.ceil(math.sqrt(abs(area) / max_slew) / system.grad_raster_time) * system.grad_raster_time
amplitude2 = area / rise_time
amplitude2 = np.divide(area, rise_time) # To handle nan
t_eff = rise_time

if abs(amplitude2) > max_grad:
Expand All @@ -115,7 +118,7 @@ def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay
fall_time = rise_time

if abs(amplitude2) > max_grad:
raise ValueError("Amplitude violation")
raise ValueError("Amplitude violation.")

grad = SimpleNamespace()
grad.type = 'trap'
Expand Down
2 changes: 1 addition & 1 deletion pypulseq/seq_examples/scripts/write_epi_se.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
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')
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
Expand Down
9 changes: 8 additions & 1 deletion pypulseq/seq_examples/scripts/write_haste.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
k_width = Nx * delta_k

GR_acq = make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time, rise_time=dG)
adc = make_adc(num_samples=Nx, duration=GR_acq.flat_time - 40e-6, delay=20e-6)
adc = make_adc(num_samples=Nx, duration=GR_acq.flat_time - 2 * system.adc_dead_time, delay=20e-6)
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', system=system, area=GR_acq.area * (1 + fspR), duration=t_sp_ex, rise_time=dG)

Expand Down Expand Up @@ -189,6 +189,13 @@

seq.add_block(delay_end)

ok, error_report = seq.check_timing() # Check whether the timing of the sequence is correct
if ok:
print('Timing check passed successfully')
else:
print('Timing check failed. Error listing follows:')
[print(e) for e in error_report]

# ======
# VISUALIZATION
# ======
Expand Down
9 changes: 8 additions & 1 deletion pypulseq/seq_examples/scripts/write_tse.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
k_width = Nx * delta_k

gr_acq = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time, rise_time=dG)
adc = pp.make_adc(num_samples=Nx, duration=gr_acq.flat_time - 40e-6, delay=20e-6)
adc = pp.make_adc(num_samples=Nx, duration=gr_acq.flat_time - 2 * system.adc_dead_time, delay=20e-6)
gr_spr = pp.make_trapezoid(channel='x', system=system, area=gr_acq.area * fsp_r, duration=t_sp, rise_time=dG)
gr_spex = pp.make_trapezoid(channel='x', system=system, area=gr_acq.area * (1 + fsp_r), duration=t_spex, rise_time=dG)

Expand Down Expand Up @@ -176,6 +176,13 @@
seq.add_block(gs5)
seq.add_block(delay_TR)

ok, error_report = seq.check_timing() # Check whether the timing of the sequence is correct
if ok:
print('Timing check passed successfully')
else:
print('Timing check failed. Error listing follows:')
[print(e) for e in error_report]

# ======
# VISUALIZATION
# ======
Expand Down
16 changes: 11 additions & 5 deletions pypulseq/seq_examples/scripts/write_ute.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
seq = pp.Sequence() # Create a new sequence object
# Define FOV and resolution
fov = 250e-3
Nx = 256
Nx = 250
alpha = 10 # Flip angle
slice_thickness = 3e-3 # Slice thickness
TR = 10e-3 # TR
Nr = 128 # Number of radial spokes
delta = 2 * np.pi / Nr # AAngular increment; try golden angle pi*(3-5^0.5) or 0.5 of i
ro_duration = 2.4e-3 # Read-out time: controls RO bandwidth and T2-blurring
delta = 2 * np.pi / Nr # Angular increment; try golden angle pi*(3-5^0.5) or 0.5 of i
ro_duration = 2.5e-3 # Read-out time: controls RO bandwidth and T2-blurring
ro_os = 2 # Oversampling
ro_asymmetry = 0.97 # 0: Fully symmetric; 1: half-echo
minRF_to_ADC_time = 50e-6 # Defines TE together with the RO asymmetry
Expand All @@ -40,14 +40,13 @@

# Align RO asymmetry to ADC samples
Nxo = np.round(ro_os * Nx)
ro_asymmetry = np.round(ro_asymmetry * Nxo / 2) / Nxo * 2
ro_asymmetry = pp.round_half_up(ro_asymmetry * Nxo / 2) / Nxo * 2 # Avoid banker's rounding

# 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)
adc.delay = adc.delay - 0.5 * adc.dwell # compensate for the 0.5 samples shift
gx_pre = pp.make_trapezoid(channel='x', area=-(gx.area - ro_area) / 2 - ro_area / 2 * (1 - ro_asymmetry), system=system)

# Gradient spoiling
Expand Down Expand Up @@ -105,6 +104,13 @@
seq.add_block(grc, grs, adc)
seq.add_block(gsc, gss, pp.make_delay(delay_TR))

ok, error_report = seq.check_timing() # Check whether the timing of the sequence is correct
if ok:
print('Timing check passed successfully')
else:
print('Timing check failed. Error listing follows:')
[print(e) for e in error_report]

# ======
# VISUALIZATION
# ======
Expand Down
6 changes: 3 additions & 3 deletions pypulseq/split_gradient_at.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ def split_gradient_at(grad: SimpleNamespace, time_point: float,
else:
grad1 = grad
grad2 = grad
grad1.last = grad.waveform[time_index]
grad2.first = 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]
grad1.waveform = grad.waveform[:time_index]
grad2.t = grad.t[time_index:]
grad2.t = grad.t[time_index:] - time_point
grad2.waveform = grad.waveform[time_index:]
return grad1, grad2
else:
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Tuple
from typing import Tuple, Union

import setuptools


def _get_version() -> Tuple[int, int, int]:
def _get_version() -> Tuple[int, int, Union[int, str]]:
"""
Returns version of current PyPulseq release.
Expand Down

0 comments on commit cc9ccfb

Please sign in to comment.