Source code for pypulseq.make_extended_trapezoid_area

from types import SimpleNamespace
from typing import Tuple, Union

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.cumsum import cumsum
from pypulseq.utils.tracing import trace, trace_enabled


[docs] def make_extended_trapezoid_area( area: float, channel: str, grad_start: float, grad_end: float, convert_to_arbitrary: bool = False, system: Union[Opts, None] = None, ) -> Tuple[SimpleNamespace, np.array, np.array]: """Make the shortest possible extended trapezoid for given area and gradient start and end point. Parameters ---------- area : float Area of extended trapezoid. channel : str Orientation of extended trapezoidal gradient event. Must be one of 'x', 'y' or 'z'. grad_start : float Starting non-zero gradient value. grad_end : float Ending non-zero gradient value. convert_to_arbitrary : bool, default=False Boolean flag to enable converting the extended trapezoid gradient into an arbitrary gradient. system: Opts, optional System limits. Returns ------- grad : SimpleNamespace Extended trapezoid event. times : numpy.ndarray Time points of the extended trapezoid. amplitude : numpy.ndarray Amplitude values of the extended trapezoid. Raises ------ ValueError if no solution was found that satisfies the constraints and the desired area. """ if system is None: system = Opts.default max_slew = system.max_slew * 0.99 max_grad = system.max_grad * 0.99 raster_time = system.grad_raster_time def _to_raster(time: float) -> float: return np.ceil(time / raster_time) * raster_time def _calc_ramp_time(grad_1: float, grad_2: float) -> float: return _to_raster(abs(grad_1 - grad_2) / max_slew) def _find_solution(duration: int) -> Union[None, Tuple[int, int, int, float]]: """Find extended trapezoid gradient waveform for given duration. The function performs a grid search over all possible ramp-up, ramp-down and flat times for the given duration and returns the solution with the lowest slew rate. Parameters ---------- duration duration of the gradient in integer multiples of raster_time Returns ------- 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 = [] # 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 = round(ramp_up_time) # Check if gradient amplitude exceeds max_grad, if so, adjust ramp # times for a trapezoidal gradient with maximum slew rate. if grad_start + ramp_up_time * max_slew * raster_time > max_grad + eps: ramp_up_time = round(_calc_ramp_time(grad_start, max_grad) / raster_time) ramp_down_time = round(_calc_ramp_time(grad_end, max_grad) / raster_time) else: 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: 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 = round(ramp_up_time) # Check if gradient amplitude exceeds -max_grad, if so, adjust ramp # times for a trapezoidal gradient with maximum slew rate. if grad_start - ramp_up_time * max_slew * raster_time < -max_grad - eps: ramp_up_time = round(_calc_ramp_time(grad_start, -max_grad) / raster_time) ramp_down_time = round(_calc_ramp_time(grad_end, -max_grad) / raster_time) else: 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: ramp_up_times.append(ramp_up_time) ramp_down_times.append(ramp_down_time) # Second, try any solution with flat_time == 0 # This appears to be necessary for many cases, but going through all # timings here is probably too conservative still. for ramp_up_time in range(1, duration): ramp_up_times.append(ramp_up_time) ramp_down_times.append(duration - ramp_up_time) time_ramp_up = np.array(ramp_up_times) time_ramp_down = np.array(ramp_down_times) # Calculate corresponding flat times flat_time = duration - time_ramp_up - time_ramp_down # Filter search space for valid timings (flat time >= 0) valid_indices = flat_time >= 0 time_ramp_up = time_ramp_up[valid_indices] time_ramp_down = time_ramp_down[valid_indices] flat_time = flat_time[valid_indices] # Calculate gradient strength for given timing using analytical solution grad_amp = -(time_ramp_up * raster_time * grad_start + time_ramp_down * raster_time * grad_end - 2 * area) / ( (time_ramp_up + 2 * flat_time + time_ramp_down) * raster_time ) # Calculate slew rates for given timings slew_rate1 = abs(grad_start - grad_amp) / (time_ramp_up * raster_time) slew_rate2 = abs(grad_end - grad_amp) / (time_ramp_down * raster_time) # Filter solutions that satisfy max_grad and max_slew constraints valid_indices = ( (abs(grad_amp) <= max_grad + 1e-8) & (slew_rate1 <= max_slew + 1e-8) & (slew_rate2 <= max_slew + 1e-8) ) solutions = np.flatnonzero(valid_indices) # Check if any valid solutions were found if solutions.shape[0] == 0: return None # Find solution with lowest slew rate and return it ind = np.argmin(slew_rate1[valid_indices] + slew_rate2[valid_indices]) ind = solutions[ind] return (int(time_ramp_up[ind]), int(flat_time[ind]), int(time_ramp_down[ind]), float(grad_amp[ind])) # Perform a linear search # This is necessary because there can exist a dead space where solutions # do not exist for some durations longer than the optimal duration. The # binary search below fails to find the optimum in those cases. # TODO: Check if range is sufficient, try to calculate the dead space. min_duration = max(round(_calc_ramp_time(grad_end, grad_start) / raster_time), 2) # Calculate duration needed to ramp down gradient to zero. # 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, ) # Linear search solution = None for duration in range(min_duration, max_duration + 1): solution = _find_solution(duration) if solution: break # Perform a binary search for duration > max_duration if no solution was found if not solution: # First, find the upper limit on duration where a solution exists by # exponentially expanding the duration. while not solution: max_duration *= 2 solution = _find_solution(max_duration) def binary_search(fun, lower_limit, upper_limit): if lower_limit == upper_limit - 1: return fun(upper_limit) test_value = (upper_limit + lower_limit) // 2 if fun(test_value): return binary_search(fun, lower_limit, test_value) else: return binary_search(fun, test_value, upper_limit) solution = binary_search(_find_solution, max_duration // 2, max_duration) # Get timing and gradient amplitude from solution time_ramp_up = solution[0] * raster_time flat_time = solution[1] * raster_time time_ramp_down = solution[2] * raster_time grad_amp = solution[3] # Create extended trapezoid if flat_time > 0: times = cumsum(0, time_ramp_up, flat_time, time_ramp_down) amplitudes = np.array([grad_start, grad_amp, grad_amp, grad_end]) else: times = cumsum(0, time_ramp_up, time_ramp_down) amplitudes = np.array([grad_start, grad_amp, grad_end]) grad = make_extended_trapezoid( channel=channel, amplitudes=amplitudes, convert_to_arbitrary=convert_to_arbitrary, system=system, times=times ) # Overwrite trace if trace_enabled(): grad.trace = trace() if not abs(grad.area - area) < 1e-8: raise ValueError(f'Could not find a solution for area={area}.') return grad, grad.tt, grad.waveform