Source code for pypulseq.make_adc

import itertools
from math import ceil, floor, gcd, isclose, prod
from types import SimpleNamespace
from typing import List, Tuple, Union
from warnings import warn

import numpy as np

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


[docs] def make_adc( num_samples: int, delay: float = 0.0, duration: float = 0.0, dwell: float = 0.0, freq_offset: float = 0.0, phase_offset: float = 0.0, system: Union[Opts, None] = None, freq_ppm: float = 0.0, phase_ppm: float = 0.0, phase_modulation: Union[np.ndarray, None] = None, ) -> SimpleNamespace: """ Create an ADC readout event. Parameters ---------- num_samples: int Number of readout samples. system : Opts, default=Opts() 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 Duration in seconds (s) of ADC readout event with `num_samples` number of samples. delay : float, default=0 Delay in seconds (s) of ADC readout event. freq_offset : float, default=0 Frequency offset of ADC readout event. phase_offset : float, default=0 Phase offset of ADC readout event. freq_ppm : float, default=0 PPM frequency offset of ADC readout event. phase_ppm : float, default=0 PPM phase offset of ADC readout event. phase_modulation : Union[np.ndarray, None], default=None Phase modulation array for FOV shifting. If provided, it must have `num_samples` number of samples. Returns ------- adc : SimpleNamespace ADC readout event. Raises ------ ValueError If neither `dwell` nor `duration` are defined. """ if system is None: system = Opts.default adc = SimpleNamespace() adc.type = 'adc' adc.num_samples = num_samples adc.dwell = dwell adc.delay = delay adc.freq_offset = freq_offset adc.phase_offset = phase_offset adc.freq_ppm = freq_ppm adc.phase_ppm = phase_ppm 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') if phase_modulation is not None and len(phase_modulation) != num_samples: raise ValueError('ADC Phase modulation vector must have the same length as the number of samples') adc.phase_modulation = phase_modulation if duration > 0: adc.dwell = duration / num_samples if dwell > 0: adc.duration = dwell * num_samples 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.', stacklevel=2, ) adc.delay = adc.dead_time if trace_enabled(): adc.trace = trace() return adc
[docs] def calc_adc_segments( num_samples: int, dwell: float, system: Union[Opts, None] = None, mode: str = 'lengthen' ) -> Tuple[int, int]: """Calculate splitting of the ADC in segments with equal samples. Parameters ---------- num_samples : int Initial number of samples to split into segments. dwell : float Dwell time of the ADC in [s] system : Opts, default=None 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. Returns ------- (num_segments, num_samples_seg): int, int Number of segments and number of samples per segment. Notes ----- "On some scanners, notably Siemens, ADC objects that exceed a certain sample length (8192 samples on Siemens) should be splittable to N equal parts, each of which aligned to the gradient raster. Each segment, however, needs to have the number of samples smaller than system.adcSamplesLimit' and divisible by system.adcSamplesDivisor to be executable on the scanner. The optional parameter mode can be either 'shorten' or 'lengthen'." [Matlab implementation of 'calcAdcSeg'. https://github.com/pulseq] Raises ------ ValueError If 'mode' is not 'shorten' or 'lengthen'. ValueError If no suitable segmentation could be found. ValueError If number of segments exceeds 128. """ # Define maximum number of segments for the ADC max_segments = 128 if mode not in ['shorten', 'lengthen']: raise ValueError(f"'mode' must be 'shorten' or 'lengthen' but is {mode}") if system is None: system = Opts.default # Check if single adc is sufficient if system.adc_samples_limit <= 0: return 1, num_samples # Get minimum number of samples for which the adc duration # is a multiple of grad raster time (GRT) and adc raster time (ART) i_gr = round(system.grad_raster_time / system.adc_raster_time) # GRT in units of ART if not isclose(system.grad_raster_time / system.adc_raster_time, i_gr): raise ValueError("System 'grad_raster_time' is not a multiple of 'adc_raster_time'.") i_dwell = round(dwell / system.adc_raster_time) # dwell in units of ART if not isclose(dwell / system.adc_raster_time, i_dwell): raise ValueError("'dwell' is not a multiple of system 'adc_raster_time'.") i_common = _lcm(i_gr, i_dwell) min_samples_segment = int(i_common / i_dwell) # lcm(a,b)/b is always int # Siemens: Number of Samples should be divisible by a divisor gcd_adcdiv = gcd(min_samples_segment, system.adc_samples_divisor) if gcd_adcdiv != system.adc_samples_divisor: min_samples_segment *= system.adc_samples_divisor / gcd_adcdiv # Get segment multiplier if mode == 'shorten': samples_seg_multip = floor(num_samples / min_samples_segment) else: samples_seg_multip = ceil(num_samples / min_samples_segment) while samples_seg_multip > 0 and samples_seg_multip < (2 * num_samples / min_samples_segment): # Get prime factors from segments multiplier adc_seg_primes = _prime_factors(samples_seg_multip) num_segments = 1 if len(adc_seg_primes) > 1: num_segments_candids = set() for k in range(1, len(adc_seg_primes) + 1): 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: 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: break else: # Shorten or lengthen the number of samples per segment samples_seg_multip += 1 if mode == 'lengthen' else -1 # Validate constraints if samples_seg_multip <= 0: 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}') return int(num_segments), int(num_samples_seg)
def _prime_factors(n: int) -> List[int]: """Compute the prime factors of given integer n.""" if n == 1: return [1] else: factors = [] # Dividing n by 2 until it's odd while n % 2 == 0: factors.append(2) n //= 2 # Checking odd factors from 3 upwards i = 3 while i * i <= n: while n % i == 0: factors.append(i) n //= i i += 2 if n > 2: factors.append(n) return factors def _lcm(a, b): """Calculates the least common multiple of a and b.""" return (a * b) // gcd(a, b)