Source code for pypulseq.make_trapezoid

import math
import warnings
from types import SimpleNamespace
from typing import Literal, Union

from pypulseq import eps
from pypulseq.opts import Opts
from pypulseq.utils.tracing import trace, trace_enabled


def calculate_shortest_params_for_area(area: float, max_slew: float, max_grad: float, grad_raster_time: float):
    """Calculate the shortest possible rise_time, flat_time, and fall_time for a given area."""

    # Calculate initial rise time constrained by max slew rate
    rise_time = math.ceil(math.sqrt(abs(area) / max_slew) / grad_raster_time) * grad_raster_time
    rise_time = max(rise_time, grad_raster_time)

    # Calculate initial amplitude
    amplitude = area / rise_time
    effective_time = rise_time

    # Adjust for max gradient constraint
    if abs(amplitude) > max_grad + eps:
        effective_time = math.ceil(abs(area) / max_grad / grad_raster_time) * grad_raster_time
        amplitude = area / effective_time
        rise_time = math.ceil(abs(amplitude) / max_slew / grad_raster_time) * grad_raster_time
        rise_time = max(rise_time, grad_raster_time)

    # Calculate flat and fall times
    flat_time = effective_time - rise_time
    fall_time = rise_time

    return amplitude, rise_time, flat_time, fall_time


def calculate_shortest_rise_time(amplitude: float, max_slew: float, grad_raster_time: float):
    """Calculate the shortest possible rise / fall time for a given amplitude and slew rate."""

    return math.ceil(max(abs(amplitude) / max_slew, grad_raster_time) / grad_raster_time) * grad_raster_time


[docs] def make_trapezoid( channel: str, amplitude: Union[float, None] = None, area: Union[float, None] = None, delay: float = 0.0, duration: Union[float, None] = None, fall_time: Union[float, None] = None, flat_area: Union[float, None] = None, flat_time: Union[float, None] = None, max_grad: Union[float, None] = None, max_slew: Union[float, None] = None, rise_time: Union[float, None] = None, system: Union[Opts, None] = None, ) -> SimpleNamespace: """ Create a trapezoidal gradient event. The user must supply any of the following sets of parameters: Area based: - area - area and duration - area and duration and rise_time - flat_time, area and rise_time Amplitude based: - amplitude and duration - amplitude and flat_time Flat area based: - flat_area and flat_time Additional options may be supplied with the above. See Also -------- - `pypulseq.Sequence.sequence.Sequence.add_block()` - `pypulseq.opts.Opts` Parameters ---------- channel : str Orientation of trapezoidal gradient event. Must be one of `x`, `y` or `z`. amplitude : float, default=None Peak amplitude (Hz/m). area : float, default=None Area (1/m). delay : float, default=0 Delay in seconds (s). duration : float, default=None Duration in seconds (s). Duration is defined as rise_time + flat_time + fall_time. fall_time : float, default=None Fall time in seconds (s). flat_area : float, default=None Flat area (1/m). flat_time : float, default=None Flat duration in seconds (s). Default is -1 to allow for triangular pulses. max_grad : float, default=None Maximum gradient strength (Hz/m). max_slew : float, default=None Maximum slew rate (Hz/m/s). rise_time : float, default=0 Rise time in seconds (s). system : Opts, default=Opts() System limits. Returns ------- grad : SimpleNamespace Trapezoidal gradient event created based on the supplied parameters. Raises ------ ValueError 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 NotImplementedError If an input set that might be valid but not implemented is passed. """ 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 max_grad is None: max_grad = system.max_grad if max_slew is None: max_slew = system.max_slew # If either of rise_time or fall_time is not provided, set it to the other. rise_time = rise_time or fall_time fall_time = fall_time or rise_time # Check which one of area, flat_area and amplitude is provided, and determine the calculation path accordingly. calc_path: Literal['area', 'flat_area', 'amplitude'] if area is not None and flat_area is None and amplitude is None: calc_path = 'area' elif area is None and flat_area is not None and amplitude is None: calc_path = 'flat_area' elif area is None and flat_area is None and amplitude is not None: calc_path = 'amplitude' elif area is None and flat_area is not None and amplitude is not None: raise NotImplementedError('Flat Area + Amplitude input pair is not implemented yet.') elif area is not None and flat_area is None and amplitude is not None: raise NotImplementedError('Amplitude + Area input pair is not implemented yet.') else: raise ValueError("Must supply either 'area', 'flat_area' or 'amplitude'.") # Check if sufficient timing parameters are provided. if flat_time is not None and flat_area is None and amplitude is None and (rise_time is None or area is None): raise ValueError( 'When `flat_time` is provided, either `flat_area`, ' 'or `amplitude`, or `rise_time` and `area` must be provided as well.' ) if calc_path == 'area': # We have area and duration. if duration is not None and flat_time is None: if rise_time is None: _, 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' ) 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 duration <= (rise_time + eps): raise ValueError('The `duration` is too short for the given `rise_time`.') if fall_time is None: 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 assert possible, ( f'Requested area is too large for this gradient. Probably amplitude is violated ' f'{round(abs(amplitude2) / max_grad * 100)}' ) flat_time = duration - rise_time - fall_time amplitude2 = area / (rise_time / 2 + fall_time / 2 + flat_time) elif flat_time is not None: if rise_time is None: raise ValueError('Must supply `rise_time` when `area` and `flat_time` is provided.') amplitude2 = area / (rise_time + flat_time) else: if rise_time is not None or fall_time is not None: warnings.warn('Rise time and fall time is ignored when calculating the shortest duration from `area`.') amplitude2, rise_time, flat_time, fall_time = calculate_shortest_params_for_area( area, max_slew, max_grad, system.grad_raster_time ) elif calc_path == 'flat_area': # Check and raise invalid input sets if duration is not None: raise NotImplementedError('Flat Area + Duration input pair is not implemented yet.') elif flat_time is not None: amplitude2 = flat_area / flat_time elif calc_path == 'amplitude': if rise_time is None: rise_time = abs(amplitude) / max_slew rise_time = math.ceil(rise_time / system.grad_raster_time) * system.grad_raster_time if rise_time == 0: rise_time = system.grad_raster_time fall_time = rise_time amplitude2 = amplitude if duration is not None and flat_time is None: flat_time = duration - rise_time - fall_time elif flat_time is not None and duration is None: pass else: raise ValueError('Must supply area or duration.') if rise_time is None and fall_time is None: rise_time = fall_time = calculate_shortest_rise_time(amplitude2, max_slew, system.grad_raster_time) if abs(amplitude2) > max_grad + eps: raise ValueError(f'Refined amplitude ({abs(amplitude2):0.0f} Hz/m) is larger than max ({max_grad:0.0f} Hz/m).') if abs(amplitude2) / rise_time > max_slew * (1 + eps): raise ValueError( f'Refined slew rate ({abs(amplitude2) / rise_time:0.0f} Hz/m/s) for ramp up is larger than max ({max_slew:0.0f} Hz/m/s).' ) if abs(amplitude2) / fall_time > max_slew * (1 + eps): raise ValueError( f'Refined slew rate ({abs(amplitude2) / fall_time:0.0f} Hz/m/s) for ramp down is larger than max ({max_slew:0.0f} Hz/m/s).' ) grad = SimpleNamespace() grad.type = 'trap' grad.channel = channel grad.amplitude = amplitude2 grad.rise_time = rise_time grad.flat_time = flat_time grad.fall_time = fall_time grad.area = amplitude2 * (flat_time + rise_time / 2 + fall_time / 2) grad.flat_area = amplitude2 * flat_time grad.delay = delay grad.first = 0 grad.last = 0 if trace_enabled(): grad.trace = trace() return grad