diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..f3d5c41 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: '' +labels: bug assignees: '' --- diff --git a/.vscode/launch.json b/.vscode/launch.json index 9d49c90..49c27f9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,18 +1,36 @@ +// { +// // Use IntelliSense to learn about possible attributes. +// // Hover to view descriptions of existing attributes. +// // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +// "version": "0.2.0", +// "configurations": [ + +// { +// "name": "Python: Current File", +// "type": "python", +// "env": {"PYTHONPATH": "${workspaceRoot}"}, +// "request": "launch", +// "program": "${file}", +// "console": "integratedTerminal", +// "justMyCode": true +// } +// ] +// } { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "env": {"PYTHONPATH": "${workspaceRoot}"}, + "name": "Python Debugger: Current File", + "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", - "justMyCode": true + "env": { + "PYTHONPATH": "/opt/mri4all/console" + } } ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d99f2f3..892bc75 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,8 @@ { - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}" }, - "python.formatting.provider": "none" + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ] } \ No newline at end of file diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 3b037da..d82671c --- a/README.md +++ b/README.md @@ -1,12 +1,5 @@ # MRI4ALL Console +This fork includes some modifications and new features (ChangeLog snapshot): +1. Transmit frequency edit box under Adjustments for every sequence -This repository contains the console software for the Zeugmatron Z1 MRI scanner that was developed during the MRI4ALL Hackathon 2023. The software has been built solely using open-source components. It runs under the Ubuntu 22.04 operating system and has been written in Python 3 using PyQt5 for the graphical user interface. A development environment with automatic installation is provided. Installation instructions are provided in the [Wiki](https://github.com/mri4all/console/wiki). - -![Screenshot from 2024-02-22 21-09-10](https://github.com/mri4all/console/assets/35747793/2da37f29-bd7a-491e-81ea-2f57ce5ae4b2) - - -## Software Overview and Platform Architecture - -The video below provides an overview & demo of the MRI4ALL Console Software. It also gives a brief introduction to the underlying software architecture and explains how custom sequences and reconstruction techniques can be integrated. - -[![Overview of the MRI4ALL Console Software](https://img.youtube.com/vi/8GNmocJP-14/0.jpg)](https://www.youtube.com/watch?v=8GNmocJP-14) +The software in this repository drives the open-source MR scanner developed during the MRI4ALL Hackathon 2023. It integrates multiple open-source packages to accomplish the functioning of an entire MR console. diff --git a/VERSION b/VERSION old mode 100644 new mode 100755 diff --git a/common/__init__.py b/common/__init__.py old mode 100644 new mode 100755 diff --git a/common/config.py b/common/config.py old mode 100644 new mode 100755 index 739206b..92d410a --- a/common/config.py +++ b/common/config.py @@ -31,7 +31,7 @@ class Configuration(BaseModel): Set description to "hidden" to hide the setting in the UI. """ - scanner_ip: str = Field(default="10.42.0.251", description="Scanner IP (internal)") + scanner_ip: str = Field(default="10.42.0.114", description="Scanner IP (internal)") debug_mode: str = Field(default="False", description="Debug Mode") hardware_simulation: str = Field(default="False", description="Hardware Simulation") dicom_targets: List[DicomTarget] = [] @@ -61,7 +61,7 @@ def save_to_file(self): with open(mri4_all_config_path, "w") as f: f.write(self.model_dump_json(indent=4)) - def update(self, data: Dict): + def update(self, data: Dict) -> "Configuration": update = self.model_dump() update.update(data) for k, v in ( @@ -69,6 +69,3 @@ def update(self, data: Dict): ): setattr(self, k, v) return self - - def is_hardware_simulation(self): - return self.hardware_simulation == "True" diff --git a/common/constants.py b/common/constants.py old mode 100644 new mode 100755 diff --git a/common/helper.py b/common/helper.py old mode 100644 new mode 100755 diff --git a/common/ipc/__init__.py b/common/ipc/__init__.py old mode 100644 new mode 100755 diff --git a/common/ipc/ipc.py b/common/ipc/ipc.py old mode 100644 new mode 100755 index 9844e9d..c15cccc --- a/common/ipc/ipc.py +++ b/common/ipc/ipc.py @@ -159,6 +159,7 @@ def mkfifo(self, FIFO): os.unlink(FIFO) os.mkfifo(FIFO) + def _listen(self): while True: with open(self.in_file) as fifo: diff --git a/common/ipc/messages.py b/common/ipc/messages.py old mode 100644 new mode 100755 diff --git a/common/ipc/test_shim.py b/common/ipc/test_shim.py old mode 100644 new mode 100755 diff --git a/common/logger.py b/common/logger.py old mode 100644 new mode 100755 diff --git a/common/plotting.py b/common/plotting.py old mode 100644 new mode 100755 diff --git a/common/queue.py b/common/queue.py old mode 100644 new mode 100755 diff --git a/common/runtime.py b/common/runtime.py old mode 100644 new mode 100755 diff --git a/common/state.py b/common/state.py old mode 100644 new mode 100755 diff --git a/common/task.py b/common/task.py old mode 100644 new mode 100755 diff --git a/common/types.py b/common/types.py old mode 100644 new mode 100755 index ba29a78..ed7db61 --- a/common/types.py +++ b/common/types.py @@ -114,15 +114,15 @@ class AdjustmentShim(BaseModel): class AdjustmentRF(BaseModel): - larmor_frequency: float = 0.0 - rf_max_amplitude: float = 0.0 - rf_pi2_fraction: float = 0.0 + larmor_frequency: float = 11.464 # MHz for 0.268T + rf_max_amplitude: float = 7661.29 + rf_pi2_fraction: float = 0.6744 class AdjustmentGradients(BaseModel): - gx_max: float = 0.0 - gy_max: float = 0.0 - gz_max: float = 0.0 + gx_max: float = 270000 + gy_max: float = 378000.0 + gz_max: float = 10000000.0 class AdjustmentSettings(BaseModel): diff --git a/common/version.py b/common/version.py old mode 100644 new mode 100755 diff --git a/external/flocra_pulseq/LICENSE b/external/flocra_pulseq/LICENSE old mode 100644 new mode 100755 diff --git a/external/flocra_pulseq/README.md b/external/flocra_pulseq/README.md old mode 100644 new mode 100755 diff --git a/external/flocra_pulseq/__init__.py b/external/flocra_pulseq/__init__.py old mode 100644 new mode 100755 diff --git a/external/flocra_pulseq/interpreter.py b/external/flocra_pulseq/interpreter.py deleted file mode 100644 index ce3c717..0000000 --- a/external/flocra_pulseq/interpreter.py +++ /dev/null @@ -1,836 +0,0 @@ -# -*- coding: utf-8 -*- -# pulseq_assembler.py -# Written by Lincoln Craven-Brightman - -import numpy as np -import logging # For errors - -class PSInterpreter: - """ - Interpret object that can compile a PulSeq file into a FLOCRA update stream array. - Run PSInterpreter.compile to compile a .seq file into a [updates]x[variables] - - Attributes: - out_dict (complex): Output sequence data - readout_number (int): Expected number of readouts - """ - - def __init__(self, rf_center=3e+6, rf_amp_max=5e+3, grad_max=1e+7, - gx_max=None, gy_max=None, gz_max=None, - clk_t=1/122.88, tx_t=123/122.88, grad_t=1229/122.88, - tx_warmup=500, tx_zero_end=True, grad_zero_end=True, - log_file = 'ps_interpreter', log_level = 20): - """ - Create PSInterpreter object for FLOCRA with system parameters. - - Args: - rf_center (float): RF center (local oscillator frequency) in Hz. - rf_amp_max (float): Default 5e+3 -- System RF amplitude max in Hz. - grad_max (float): Default 1e+6 -- System gradient max in Hz/m. - gx_max (float): Default None -- System X-gradient max in Hz/m. If None, defaults to grad_max. - gy_max (float): Default None -- System Y-gradient max in Hz/m. If None, defaults to grad_max. - gz_max (float): Default None -- System Z-gradient max in Hz/m. If None, defaults to grad_max. - clk_t (float): Default 1/122.88 -- System clock period in us. - tx_t (float): Default 123/122.88 -- Transmit raster period in us. - grad_t (float): Default 1229/122.88 -- Gradient raster period in us. - tx_warmup (float): Default 500 -- Warmup time to turn on tx_gate before Tx events in us. - tx_zero_end (bool): Default True -- Force zero at the end of RF shapes - grad_zero_end (bool): Default True -- Force zero at the end of Gradient/Trap shapes - log_file (str): Default 'ps_interpreter' -- File (.log appended) to write run log into. - log_level (int): Default 20 (INFO) -- Logger level, 0 for all, 20 to ignore debug. - """ - # Logging - self._logger = logging.getLogger() - logging.basicConfig(filename = log_file + '.log', filemode = 'w', level = log_level) - - self._clk_t = clk_t # Instruction clock period in us - self._tx_t = tx_t # Transmit sample period in us - self._warning_if(int(tx_t / self._clk_t) * self._clk_t != tx_t, - f"tx_t ({tx_t}) isn't a multiple of clk_t ({clk_t})") - self._grad_t = grad_t # Gradient sample period in us - self._warning_if(int(grad_t / self._clk_t) * self._clk_t != grad_t, - f"grad_t ({(grad_t)}) isn't multiple of clk_t ({clk_t})") - self._rx_div = None - self._rx_t = None - - self._rf_center = rf_center # Hz - self._rf_amp_max = rf_amp_max # Hz - - - # Gradient maxes, Hz/m - self._grad_max = {} - if gx_max is None: self._grad_max['gx'] = grad_max - else: self._grad_max['gx'] = gx_max - if gy_max is None: self._grad_max['gy'] = grad_max - else: self._grad_max['gy'] = gy_max - if gz_max is None: self._grad_max['gz'] = grad_max - else: self._grad_max['gz'] = gz_max - - self._tx_warmup = tx_warmup # us - - self._tx_zero_end = tx_zero_end - self._grad_zero_end = grad_zero_end - - # Interpreter for section names in .seq file - self._pulseq_keys = { - '[VERSION]' : self._read_temp, # Unused - '[DEFINITIONS]' : self._read_defs, - '[BLOCKS]' : self._read_blocks, - '[RF]' : self._read_rf_events, - '[GRADIENTS]' : self._read_grad_events, - '[TRAP]' : self._read_trap_events, - '[ADC]' : self._read_adc_events, - '[DELAYS]' : self._read_delay_events, - '[EXTENSIONS]' : self._read_temp, # Unused - '[SHAPES]' : self._read_shapes - } - - # Defined variable names to output - self._var_names = ('tx0', 'grad_vx', 'grad_vy', 'grad_vz', 'grad_vz2', - 'rx0_en', 'tx_gate') - - # PulSeq dictionary storage - self._blocks = {} - self._rf_events = {} - self._grad_events = {} - self._adc_events = {} - self._delay_events = {} - self._shapes = {} - self._definitions = {} - - # Interpolated and compiled data for output - self._tx_durations = {} # us - self._tx_times = {} # us - self._tx_data = {} # normalized float - self._grad_durations = {} # us - self._grad_times = {} # us - self._grad_data = {} # normalized float - - self.out_data = {} - self.readout_number = 0 - self.is_assembled = False - - # Wrapper for full compilation - def interpret(self, pulseq_file): - """ - Interpret FLOCRA array from PulSeq .seq file - - Args: - pulseq_file (str): PulSeq file to compile from - - Returns: - dict: tuple of numpy.ndarray time and update arrays, with variable name keys - dict: parameter dictionary containing raster times, readout numbers, and any file-defined variables - """ - self._logger.info(f'Interpreting ' + pulseq_file) - if self.is_assembled: - self._logger.info('Re-initializing over old sequence...') - self.__init__(rf_center=self._rf_center, rf_amp_max=self._rf_amp_max, - gx_max=self._grad_max['gx'], gy_max=self._grad_max['gy'], gz_max=self._grad_max['gz'], - clk_t=self._clk_t, tx_t=self._tx_t, grad_t=self._grad_t) - self._read_pulseq(pulseq_file) - self._compile_tx_data() - self._compile_grad_data() - self.out_data, self.readout_number = self._stream_all_blocks() - self.is_assembled = True - param_dict = {'readout_number' : self.readout_number, 'tx_t' : self._tx_t, 'rx_t' : self._rx_t, 'grad_t': self._grad_t} - for key, value in self._definitions.items(): - if key in param_dict: - self._logger.warning(f'Key conflict: overwriting key [{key}], value [{param_dict[key]}] with new value [{value}]') - param_dict[key] = value - return (self.out_data, param_dict) - - # Open file and read in all sections into class storage - def _read_pulseq(self, pulseq_file): - """ - Read PulSeq file into object dict memory - - Args: - pulseq_file (str): PulSeq file to assemble from - """ - # Open file - with open(pulseq_file) as f: - self._logger.info('Opening PulSeq file...') - line = '\n' - next_line = '' - - while True: - if not next_line: - line = f.readline() - else: - line = next_line - next_line = '' - if line == '': break - key = self._simplify(line) - if key in self._pulseq_keys: - next_line = self._pulseq_keys[key](f) - - # Check that all ids are valid - self._logger.info('Validating ids...') - var_names = ('delay', 'rf', 'gx', 'gy', 'gz', 'adc', 'ext') - var_dicts = [self._delay_events, self._rf_events, self._grad_events, self._grad_events, self._grad_events, self._adc_events, {}] - for block in self._blocks.values(): - for i in range(len(var_names)): - id_n = block[var_names[i]] - self._error_if(id_n != 0 and id_n not in var_dicts[i], f'Invalid {var_names[i]} id: {id_n}') - for rf in self._rf_events.values(): - self._error_if(rf['mag_id'] not in self._shapes, f'Invalid magnitude shape id: {rf["mag_id"]}') - self._error_if(rf['phase_id'] not in self._shapes, f'Invalid phase shape id: {rf["phase_id"]}') - for grad in self._grad_events.values(): - if len(grad) == 3: - self._error_if(grad['shape_id'] not in self._shapes, f'Invalid grad shape id: {grad["shape_id"]}') - self._logger.info('Valid ids') - - # Check that all delays are multiples of clk_t - for events in [self._blocks.values(), self._rf_events.values(), self._grad_events.values(), - self._adc_events.values()]: - for event in events: - self._warning_if(int(event['delay'] / self._clk_t) * self._clk_t != event['delay'], - f'Event delay {event["delay"]} is not a multiple of clk_t') - for delay in self._delay_events.values(): - self._warning_if(int(delay / self._clk_t) * self._clk_t != delay, - f'Delay event {delay} is not a multiple of clk_t') - - # Check that RF/ADC (TX/RX) only have one frequency offset -- can't be set within one file. - freq = None - base_id = None - base_str = None - for rf_id, rf in self._rf_events.items(): - if freq is None: - freq = rf['freq'] - base_id = rf_id - base_str = 'RF' - self._error_if(rf['freq'] != freq, f"Frequency offset of RF event {rf_id} ({rf['freq']}) doesn't match that of {base_str} event {base_id} ({freq})") - for adc_id, adc in self._adc_events.items(): - if freq is None: - freq = adc['freq'] - base_id = adc_id - base_str = 'ADC' - self._error_if(adc['freq'] != freq, f"Frequency offset of ADC event {adc_id} ({adc['freq']}) doesn't match that of {base_str} event {base_id} ({freq})") - if freq is not None and freq != 0: - self._rf_center += freq - self._logger.info(f'Adding freq offset {freq} Hz. New center / linear oscillator frequency: {self._rf_center}') - - # Check that ADC has constant dwell time - dwell = None - for adc_id, adc in self._adc_events.items(): - if dwell is None: - dwell = adc['dwell']/1000 - base_id = adc_id - self._error_if(adc['dwell']/1000 != dwell, f"Dwell time of ADC event {adc_id} ({adc['dwell']}) doesn't match that of ADC event {base_id} ({dwell})") - if dwell is not None: - self._rx_div = np.round(dwell / self._clk_t).astype(int) - self._rx_t = self._clk_t * self._rx_div - self._warning_if(self._rx_div * self._clk_t != dwell, - f'Dwell time ({dwell}) rounded to {self._rx_t}, multiple of clk_t ({self._clk_t})') - - self._logger.info('PulSeq file loaded') - - # Compilation into data formats - #region - - # Interpolate and compile tx events - def _compile_tx_data(self): - """ - Compile transmit data from object dict memory into concatenated array - """ - - self._logger.info('Compiling Tx data...') - - # Process each rf event - for tx_id, tx_event in self._rf_events.items(): - # Collect mag/phase shapes - mag_shape = self._shapes[tx_event['mag_id']] - phase_shape = self._shapes[tx_event['phase_id']] - self._error_if(len(mag_shape) != len(phase_shape), f'Tx envelope of RF event {tx_id} has mismatched magnitude ' \ - 'and phase length') - - # Event length and duration, create time points - event_len = len(mag_shape) # unitless - event_duration = event_len * self._tx_t # us - self._error_if(event_len < 1, f"Zero length shape: {tx_event['mag_id']}") - x = np.linspace(0, event_duration, num = event_len, endpoint=False) - - # Scale and convert to complex Tx envelope - mag = mag_shape * tx_event['amp'] / self._rf_amp_max - phase = phase_shape * 2 * np.pi - tx_env = np.exp((phase + tx_event['phase']) * 1j) * mag - - self._error_if(np.any(np.abs(tx_env) > 1.0), f'Magnitude of RF event {tx_id} is too ' \ - f'large relative to RF max {self._rf_amp_max}') - - # Optionally force zero at the end of tx event - if self._tx_zero_end: - x = np.append(x, event_duration) - tx_env = np.append(tx_env, 0) - - # Save tx duration, update times, data - self._tx_durations[tx_id] = event_duration + tx_event['delay'] - self._tx_times[tx_id] = x + tx_event['delay'] - self._tx_data[tx_id] = tx_env - - self._logger.info('Tx data compiled') - - # Interpolate and compile gradient events - def _compile_grad_data(self): - """ - Compile gradient events from object dict memory into array - """ - self._logger.info('Compiling gradient data...') - - # Process each rf event - for grad_id, grad_event in self._grad_events.items(): - - # Collect shapes, create time points - if len(grad_event) == 5: # Trapezoid shape - - # Check for timing issues - for time in ['rise', 'flat', 'fall']: - self._warning_if(grad_event[time] < self._grad_t, f'Trapezoid {grad_id} has {time} ' \ - f"time ({grad_event[time]}) less than raster time ({self._grad_t})") - self._warning_if(int(grad_event[time] / self._grad_t) * self._grad_t != grad_event[time], - f"Trapezoid {grad_id} {time} time ({grad_event[time]}) isn't a multiple of raster time ({self._grad_t})") - - # Raster out rise and fall times, prioritize flat time and zero ending - rise_len = int(grad_event['rise'] / self._grad_t) - fall_len = int(grad_event['fall'] / self._grad_t) - - x_rise = np.linspace(grad_event['rise'] - rise_len * self._grad_t, - grad_event['rise'], - num=rise_len, endpoint=False) - rise = np.flip(np.linspace(grad_event['amp'], 0, num=rise_len, endpoint=False)) - - x_fall = np.linspace(grad_event['rise'] + grad_event['flat'], - grad_event['rise'] + grad_event['flat'] + fall_len * self._grad_t, - num=fall_len, endpoint=False) - fall = np.flip(np.linspace(0, grad_event['amp'], num=fall_len, endpoint=False)) - - # Concatenate times and data - x = np.concatenate((x_rise, x_fall)) - grad = np.concatenate((rise, fall)) - - event_duration = grad_event['rise'] + grad_event['flat'] + grad_event['fall'] # us - else: - # Event length and duration, create time points - shape = self._shapes[grad_event['shape_id']] - event_len = len(shape) # unitless - event_duration = event_len * self._grad_t # us - self._error_if(event_len < 1, f"Zero length shape: {grad_event['shape_id']}") - grad = shape * grad_event['amp'] - x = np.linspace(0, event_duration, num = event_len, endpoint=False) - - # Optionally force zero at the end of gradient event - if self._grad_zero_end: - x = np.append(x, event_duration) - grad = np.append(grad, 0) - - # Save grad duration, update times, data - self._grad_durations[grad_id] = event_duration + grad_event['delay'] - self._grad_times[grad_id] = x + grad_event['delay'] - self._grad_data[grad_id] = grad - - self._logger.info('Gradient data compiled') - - # Encode all blocks - def _stream_all_blocks(self): - """ - Encode all blocks into sequential time updates. - - Returns: - dict: tuples of np.ndarray times, updates with variable name keys - int: number of sequence readout points - """ - # Prep containers, zero at start - out_data = {} - times = {var: [np.zeros(1)] for var in self._var_names} - updates = {var: [np.zeros(1)] for var in self._var_names} - start = 0 - readout_total = 0 - - # Encode all blocks - for block_id in self._blocks.keys(): - var_dict, duration, readout_num = self._stream_block(block_id) - - for var in self._var_names: - times[var].append(var_dict[var][0] + start) - updates[var].append(var_dict[var][1]) - - start += duration - readout_total += readout_num - - # Clean up final arrays - for var in self._var_names: - # Make sure times are ordered, and overwrite duplicates to last inserted update - time_sorted, unique_idx = np.unique(np.flip(np.concatenate(times[var])), return_index=True) - update_sorted = np.flip(np.concatenate(updates[var]))[unique_idx] - - # Compressed repeated values - update_compressed_idx = np.concatenate([[0], np.nonzero(update_sorted[1:] - update_sorted[:-1])[0] + 1]) - update_arr = update_sorted[update_compressed_idx] - time_arr = time_sorted[update_compressed_idx] - - # Zero everything at end - time_arr = np.concatenate((time_arr, np.zeros(1) + start)) - update_arr = np.concatenate((update_arr, np.zeros(1))) - - out_data[var] = (time_arr, update_arr) - - return (out_data, readout_total) - - # Convert individual block into PR commands (duration, gates), TX offset, and GRAD offset - def _stream_block(self, block_id): - """ - Encode block into sequential time updates - - Args: - block_id (int): Block id key for block in object dict memory to be encoded - - Returns: - dict: tuples of np.ndarray times, updates with variable name keys - float: duration of the block - int: readout count for the block - """ - out_dict = {var: [] for var in self._var_names} - readout_num = 0 - duration = 0 - - block = self._blocks[block_id] - # Preset all variables - for var in self._var_names: - out_dict[var] = (np.zeros(0, dtype=int),) * 2 - - # Minimum duration of block - if block['delay'] != 0: - duration = max(duration, self._delay_events[block['delay']]) - - # Tx and Tx gate updates - tx_id = block['rf'] - if tx_id != 0: - out_dict['tx0'] = (self._tx_times[tx_id], self._tx_data[tx_id]) - duration = max(duration, self._tx_durations[tx_id]) - tx_gate_start = self._tx_times[tx_id][0] - self._tx_warmup - self._error_if(tx_gate_start < 0, - f'Tx warmup ({self._tx_warmup}) of RF event {tx_id} is longer than delay ({self._tx_times[tx_id][0]})') - out_dict['tx_gate'] = (np.array([tx_gate_start, self._tx_durations[tx_id]]), - np.array([1, 0])) - - # Gradient updates - for grad_ch in ('gx', 'gy', 'gz'): - grad_id = block[grad_ch] - if grad_id != 0: - grad_var_name = grad_ch[0] + 'rad_v' + grad_ch[1] # To get the correct varname for output g[CH] -> grad_v[CH] - self._error_if(np.any(np.abs(self._grad_data[grad_id] / self._grad_max[grad_ch]) > 1), - f'Gradient event {grad_id} for {grad_ch} in block {block_id} is larger than {grad_ch} max') - out_dict[grad_var_name] = (self._grad_times[grad_id], self._grad_data[grad_id] / self._grad_max[grad_ch]) - duration = max(duration, self._grad_durations[grad_id]) - - # Rx updates - rx_id = block['adc'] - if rx_id != 0: - rx_event = self._adc_events[rx_id] - rx_start = rx_event['delay'] - rx_end = rx_start + rx_event['num'] * self._rx_t - readout_num += rx_event['num'] - out_dict['rx0_en'] = (np.array([rx_start, rx_end]), np.array([1, 0])) - duration = max(duration, rx_end) - - # Return durations for each PR and leading edge values - return (out_dict, duration, int(readout_num)) - #endregion - - # Helper functions for reading sections - #region - - # [BLOCKS] - def _read_blocks(self, f): - """ - Read BLOCKS (event block) section in PulSeq file f to object dict memory. - Event blocks are formatted like: - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - var_names = ('delay', 'rf', 'gx', 'gy', 'gz', 'adc', 'ext') - rline = '' - line = '' - self._logger.info('Blocks: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 8: # - data_line = [int(x) for x in tmp] - self._warning_if(data_line[0] in self._blocks, f'Repeat block ID {data_line[0]}, overwriting') - self._blocks[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - elif len(tmp) == 7: # Spec allows extension ID not included, add it in as 0 - data_line = [int(x) for x in tmp] - data_line.append(0) - self._warning_if(data_line[0] in self._blocks, f'Repeat block ID {data_line[0]}, overwriting') - self._blocks[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - - if len(self._blocks) == 0: self._logger.error('Zero blocks read, nonzero blocks needed') - assert len(self._blocks) > 0, 'Zero blocks read, nonzero blocks needed' - self._logger.info('Blocks: Complete') - - return rline - - # [RF] - def _read_rf_events(self, f): - """ - Read RF (RF event) section in PulSeq file f to object dict memory. - RF events are formatted like: - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - var_names = ('amp', 'mag_id', 'phase_id', 'delay', 'freq', 'phase') - rline = '' - line = '' - self._logger.info('RF: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 7: # - data_line = [int(tmp[0]), float(tmp[1]), int(tmp[2]), int(tmp[3]), int(tmp[4]), float(tmp[5]), float(tmp[6])] - self._warning_if(data_line[0] in self._rf_events, f'Repeat RF ID {data_line[0]}, overwriting') - self._rf_events[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - - self._logger.info('RF: Complete') - - return rline - - # [GRADIENTS] - def _read_grad_events(self, f): - """ - Read GRADIENTS (gradient event) section in PulSeq file f to object dict memory. - Gradient events are formatted like: - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - var_names = ('amp', 'shape_id', 'delay') - rline = '' - line = '' - self._logger.info('Gradients: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 4: # GRAD - data_line = [int(tmp[0]), float(tmp[1]), int(tmp[2]), int(tmp[3])] - self._warning_if(data_line[0] in self._grad_events, f'Repeat gradient ID {data_line[0]} in GRADIENTS, overwriting') - self._grad_events[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - elif len(tmp) == 3: # GRAD NO DELAY - data_line = [int(tmp[0]), float(tmp[1]), int(tmp[2])] - data_line.append(0) - self._warning_if(data_line[0] in self._grad_events, f'Repeat gradient ID {data_line[0]}, in GRADIENTS, overwriting') - self._grad_events[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - - self._logger.info('Gradients: Complete') - - return rline - - # [TRAP] - def _read_trap_events(self, f): - """ - Read TRAP (trapezoid gradient event) section in PulSeq file f to object dict memory. - Trapezoid gradient events are formatted like: - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - var_names = ('amp', 'rise', 'flat', 'fall', 'delay') - rline = '' - line = '' - self._logger.info('Trapezoids: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 6: # TRAP - data_line = [int(tmp[0]), float(tmp[1]), int(tmp[2]), int(tmp[3]), int(tmp[4]), float(tmp[5])] - self._warning_if(data_line[0] in self._grad_events, f'Repeat gradient ID {data_line[0]} in TRAP, overwriting') - self._grad_events[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - elif len(tmp) == 5: # TRAP NO DELAY - data_line = [int(tmp[0]), float(tmp[1]), int(tmp[2]), int(tmp[3]), int(tmp[4])] - data_line.append(0) - self._warning_if(data_line[0] in self._grad_events, f'Repeat gradient ID {data_line[0]} in TRAP, overwriting') - self._grad_events[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - - self._logger.info('Trapezoids: Complete') - - return rline - - # [ADC] - def _read_adc_events(self, f): - """ - Read ADC (ADC/readout event) section in PulSeq file f to object dict memory. - ADC events are formatted like: - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - var_names = ('num', 'dwell', 'delay', 'freq', 'phase') - rline = '' - line = '' - self._logger.info('ADC: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 6: - data_line = [int(tmp[0]), int(tmp[1]), float(tmp[2]), int(tmp[3]), float(tmp[4]), float(tmp[5])] - self._adc_events[data_line[0]] = {var_names[i] : data_line[i+1] for i in range(len(var_names))} - - self._logger.info('ADC: Complete') - - return rline - - # [DELAY] -> single value output - def _read_delay_events(self, f): - """ - Read DELAY (delay event) section in PulSeq file f to object dict memory (stored as a single value, not a dict). - Delay events are formatted like: - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - rline = '' - line = '' - self._logger.info('Delay: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 2: - data_line = [int(x) for x in tmp] - self._warning_if(data_line[0] in self._delay_events, f'Repeat delay ID {data_line[0]}, overwriting') - self._delay_events[data_line[0]] = data_line[1] # Single value, delay - - self._logger.info('Delay: Complete') - - return rline - - # [SHAPES] list of entries, normalized between 0 and 1 - def _read_shapes(self, f): - """ - Read SHAPES (rastered shapes) section in PulSeq file f to object dict memory. - Shapes are formatted with two header lines, followed by lines of single data points in compressed pulseq shape format - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - rline = '' - line = '' - self._logger.info('Shapes: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - if len(rline.split()) == 2 and rline.split()[0].lower() == 'shape_id': - shape_id = int(rline.split()[1]) - n = int(self._simplify(f.readline()).split()[1]) - self._warning_if(shape_id in self._shapes, f'Repeat shape ID {shape_id}, overwriting') - self._shapes[shape_id] = np.zeros(n) - i = 0 - prev = -2 - x = 0 - while i < n: - dx = float(self._simplify(f.readline())) - x += dx - self._warning_if(x > 1 or x < 0, f'Shape {shape_id} entry {i} is {x},' - ' outside of [0, 1], will be capped') - if x > 1: - x = 1 - elif x < 0: - x = 0 - self._shapes[shape_id][i] = x - if dx == prev: - r = int(float(self._simplify(f.readline()))) - for _ in range(0, r): - i += 1 - x += dx - self._warning_if(x > 1 or x < 0, f'Shape {shape_id} entry {i} is {x},' - ' outside of [0, 1], will be capped') - if x > 1: - x = 1 - elif x < 0: - x = 0 - self._shapes[shape_id][i] = x - i += 1 - prev = dx - - self._logger.info('Shapes: Complete') - - return rline - - # [DEFINITIONS] - def _read_defs(self, f): - """ - Read through DEFINITIONS section in PulSeq file f. - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - rline = '' - line = '' - self._logger.info('Definitions: Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - - tmp = rline.split() - if len(tmp) == 2: - varname, value = rline.split() - try: - value = float(value) - except: - pass - - # Automatic raster time reading - if varname == 'tx_t': - self._tx_t = value - self._logger.info(f'Overwriting tx_t to {value} from Definitions') - elif varname == 'grad_t': - self._grad_t = value - self._logger.info(f'Overwriting grad_t to {value} from Definitions') - elif varname == 'tx_warmup': - self._tx_warmup = value - self._logger.info(f'Overwriting tx_warmup to {value} from Definitions') - else: - self._definitions[varname] = value - - self._logger.debug(f'Read in {varname}') - - self._logger.info('Definitions: Complete') - - return rline - - # Unused headers - def _read_temp(self, f): - """ - Read through any unused section in PulSeq file f. - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Raw next line in file after section ends - """ - rline = '' - line = '' - self._logger.info('(Unused): Reading...') - while True: - line = f.readline() - rline = self._simplify(line) - if line == '' or rline in self._pulseq_keys: break - self._logger.debug('Unused line') - - self._logger.info('(Unused): Complete') - - return rline - - # Simplify lines read from pulseq -- remove comments, trailing \n, trailing whitespace, commas - def _simplify(self, line): - """ - Simplify raw line to space-separated values - - Args: - f (_io.TextIOWrapper): File pointer to read from - - Returns: - str: Simplified string - """ - - # Find and remove comments, comma - comment_index = line.find('#') - if comment_index >= 0: - line = line[:comment_index] - - return line.rstrip('\n').strip().replace(',','') - - #endregion - - # Error and warnings - #region - # For crashing and logging errors (may change behavior) - def _error_if(self, err_condition, message): - """ - Throw an error (currently using assert) and log if error condition is met - - Args: - err_condition (bool): Condition on which to throw error - message (str): Message to accompany error in log. - """ - if err_condition: self._logger.error(message) - assert not err_condition, (message) - - # For warnings without crashing - def _warning_if(self, warn_condition, message): - """ - Print warning and log if error condition is met - - Args: - warn_condition (bool): Condition on which to warn - message (str): Message to accompany warning in log. - """ - if warn_condition: self._logger.warning(message) - #endregion - -# Sample usage -if __name__ == '__main__': - ps = PSInterpreter(grad_t=1) - inp_file = '../mgh-flocra/test_sequences/tabletop_radial_v2_2d_pulseq.seq' - out_data, params = ps.interpret(inp_file) - - import matplotlib.pyplot as plt - - names = [' tx', ' gx', ' gy', ' gz', 'adc'] - data = [out_data['tx0'], out_data['grad_vx'], out_data['grad_vy'], out_data['grad_vz'], out_data['tx_gate']] - - for i in range(5): - print(f'{names[i]} minimum entry difference magnitude: {np.min(np.abs(data[i][1][1:] - data[i][1][:-1]))}') - print(f'{names[i]} entries below 1e-6 difference: {np.sum(np.abs(data[i][1][1:] - data[i][1][:-1]) < 1e-6)}') - print(f'{names[i]} entries below 1e-5 difference: {np.sum(np.abs(data[i][1][1:] - data[i][1][:-1]) < 1e-5)}') - print(f'{names[i]} entries below 1e-4 difference: {np.sum(np.abs(data[i][1][1:] - data[i][1][:-1]) < 1e-4)}') - print(f'{names[i]} entries below 1e-3 difference: {np.sum(np.abs(data[i][1][1:] - data[i][1][:-1]) < 1e-3)}') - - print("Completed successfully") diff --git a/external/flocra_pulseq/interpreter_pp.py b/external/flocra_pulseq/interpreter_pp.py new file mode 100755 index 0000000..d3d6886 --- /dev/null +++ b/external/flocra_pulseq/interpreter_pp.py @@ -0,0 +1,288 @@ +import math +import warnings +import numpy as np +import pypulseq as pp +# dependencies from mri4all console project +import common.logger as logger + + +log = logger.get_logger() + +class seq2flocra: + + """ + Returns the flocra dictionary from a seq object or a seq file. + + Parameters + ---------- + seq : object from a Pulseq sequence + + + Returns + ------- + flodict : dictionary for flocra ingestion + + """ + + def __init__(self, seq: pp.Sequence = None, seq_file:str = None, system:pp.Opts = None, + center_freq: float=15.58e6, clk_freq: float = 122.88, + rf_amp_max: float = 5e3, tx_zero_end: bool = True, + debug_log: bool = True): + """ + From Lincoln's object - trying to match structure for compatibility + Args: + rf_center (float): RF center (local oscillator frequency) in Hz. + rf_amp_max (float): Default 5e+3 -- System RF amplitude max in Hz. + grad_max (float): Default 1e+6 -- System gradient max in Hz/m. - from pp.Opts + gx_max (float): Default None -- System X-gradient max in Hz/m. If None, defaults to grad_max. - from pp.Opts + gy_max (float): Default None -- System Y-gradient max in Hz/m. If None, defaults to grad_max. - from pp.Opts + gz_max (float): Default None -- System Z-gradient max in Hz/m. If None, defaults to grad_max. - from pp.Opts + clf_freq (float): Default 122.88 -- System clock frequency in MHz. + clk_t (float): Default 1/122.88 -- System clock period in us. + tx_t (float): Default 123/122.88 -- Transmit raster period in us. - from pp.Opts: rf_raster_time + grad_t (float): Default 1229/122.88 -- Gradient raster period in us. - from pp.Opts: grad_raster_time + tx_warmup (float): Default 500 -- Warmup time to turn on tx_gate before Tx events in us. + tx_zero_end (bool): Default True -- Force zero at the end of RF shapes + grad_zero_end (bool): Default False, If True -- Force zero at the end of Gradient/Trap shapes + """ + # seq world lives in seconds; marcos in us + self._seq = seq # seq object + self._seq_file = seq_file + self._center_freq = center_freq + self._lo_freq = center_freq # this will change based on frequency offset + self._clk_freq = clk_freq * 1e6 # More readily available in spec. - MHz :: Not available in system opts + self._clk_t = 1 / self._clk_freq + self._rf_amp_max = rf_amp_max # Not available in system opts + self._tx_zero_end = tx_zero_end + self._debug_log = debug_log + self._system = system + + if system is not None: + log.info("**System defined**:", self._system) + else: + log.info("**System not defined, using defaults**") + self._system = None + + # This seq system needs to be point of full control - simplifies config significantly; TODO:: simplify config using the pp.Opts() + if self._system is None: + self._system = pp.Opts( + max_grad=1e7, # this should come from the config file + grad_unit="Hz/m", + # max_slew=130, + # slew_unit="T/m/s", + rf_ringdown_time=20e-6, # old one did not include this + rf_dead_time=100e-6, + adc_dead_time=20e-6, + rf_raster_time = 1e-6, # (np.ceil(clk_freq) / self._clk_freq), + grad_raster_time = 10e-6, # (np.ceil(clk_freq * 20) / self._clk_freq), #clk_freq is in MHz, self._clk_freq is in Hz + block_duration_raster=1e-6) #(np.ceil(clk_freq * 20) / self._clk_freq)) + + + self._tx_t = self._system.rf_raster_time #3.125 + self._grad_t = self._system.grad_raster_time + self._tx_warmup = self._system.rf_dead_time + self._rx_t = self._system.adc_raster_time + + def load_seqfile(self, seq_file): + self._seq = pp.Sequence(self._system) + self._seq.read(seq_file, detect_rf_use=True) + self._seq_duration = self._seq.get_definition("TotalDuration") + + def curate(self, times: np.ndarray = None, updates: np.ndarray=None): #Lincoln's clean-up code + # Make sure times are ordered, and overwrite duplicates to last inserted update + time_sorted, unique_idx = np.unique(times, return_index=True) + update_sorted = (updates)[unique_idx] + + # Compressed repeated values + update_compressed_idx = np.concatenate([[0], np.nonzero(update_sorted[1:] - update_sorted[:-1])[0] + 1]) + update_arr = np.append(update_sorted[update_compressed_idx], 0.0) # end all arrays at 0.0 - Check with Vlad if necessary + time_arr = np.append(time_sorted[update_compressed_idx], self._block_duration_us) + + return (time_arr, update_arr) + + + + def block_events_to_amps_times(self): + # Future versions can exploit event libraries for more succint representation + # adc = self._seq.adc_library # Library of ADC events + # delay = self._seq.delay_library # Library of delay events + # adc_times = self._seq.adc_times() + + # Initialize variables for flocra dictionary + block_duration = 0.0 + self._num_samples_total = int(0) + grad_vx_amp = np.array([0.0]) + grad_vx_time = np.array([0.0]) + grad_vy_amp = np.array([0.0]) + grad_vy_time = np.array([0.0]) + grad_vz_amp = np.array([0.0]) + grad_vz_time = np.array([0.0]) + + tx0_amp = np.array([0.0]) + tx0_time = np.array([0.0]) + tx0_gate_amp = [] + tx0_gate_time = [] + + rx0_gate_amp = [0.0] + rx0_gate_time = [0.0] + # TODO: Amplitude violation, slew violation, safety checks; can make this more concise using class' def for grad and RF props + for block_counter in self._seq.block_events: + block = self._seq.get_block(block_counter) + if block.gx is not None: + log.info('gx max:', self._system.max_grad) + if block.gx.type == 'trap': + grad_vx_amp = np.concatenate((grad_vx_amp, [0, block.gx.amplitude / self._system.max_grad, block.gx.amplitude / self._system.max_grad, 0])) + grad_vx_time = np.concatenate((grad_vx_time, block_duration + [0, block.gx.rise_time, block.gx.rise_time + block.gx.flat_time, block.gx.rise_time + block.gx.flat_time + block.gx.fall_time ])) + else: + grad_vx_amp.append(np.array(block.gx.waveform / self._system.max_grad)) + grad_vx_time.append(np.array(block.gx.tt)+ block_duration) + + + if block.gy is not None: + if block.gy.type == 'trap': + grad_vy_amp = np.concatenate((grad_vy_amp, [0, block.gy.amplitude / self._system.max_grad, block.gy.amplitude / self._system.max_grad, 0])) + grad_vy_time = np.concatenate((grad_vy_time, block_duration + [0, block.gy.rise_time, block.gy.rise_time + block.gy.flat_time, block.gy.rise_time + block.gy.flat_time + block.gy.fall_time ])) + else: + grad_vy_amp.append(np.array(block.gy.waveform / self._system.max_grad)) + grad_vy_time.append(np.array(block.gy.tt) + block_duration) + + + if block.gz is not None: + if block.gz.type == 'trap': + grad_vz_amp = np.concatenate((grad_vz_amp, [0, block.gz.amplitude / self._system.max_grad, block.gz.amplitude / self._system.max_grad, 0])) + grad_vz_time = np.concatenate((grad_vz_time, block_duration + [0, block.gz.rise_time, block.gz.rise_time + block.gz.flat_time, block.gz.rise_time + block.gz.flat_time + block.gz.fall_time ])) + else: + grad_vz_amp.append(np.array(block.gz.waveform / self._system.max_grad)) + grad_vz_time.append(np.array(block.gz.tt) + block_duration) + + if block.rf is not None: + if(block.rf.freq_offset > 0): # changes lo_freq, so need to playout current dict + self._lo_freq = self._center_freq + block.rf.freq_offset + else: + signal_scaled = block.rf.signal / self._rf_amp_max + if (np.max(np.abs(signal_scaled)) > 1): + log.info('RF amplitude violation') + + mag = np.abs(signal_scaled) + phase = np.angle(signal_scaled) + tx0_pulse_amp = mag * np.exp((phase + block.rf.phase_offset) * 1j) + tx0_pulse_time = block_duration + np.max([block.rf.delay, self._tx_warmup]) + block.rf.t - block.rf.t[0] #pp adds 0.5us start + + + if self._tx_zero_end: + tx0_pulse_time = np.append(tx0_pulse_time, tx0_pulse_time[-1] + self._tx_t) + tx0_pulse_amp = np.append(tx0_pulse_amp, 0) + + tx0_amp = np.concatenate((tx0_amp, tx0_pulse_amp)) + tx0_time = np.concatenate((tx0_time, tx0_pulse_time)) + + tx0_gate_amp = np.concatenate((tx0_gate_amp, np.array([1.0, 0.0]))) + if (tx0_pulse_time[0] - self._tx_warmup) < 0: + log.info('RF delay needs to be longer than RF deadtime (tx warmup)') + # tx0_gate_time = np.concatenate((tx0_gate_time,np.array([round(tx0_pulse_time[0] - self._tx_warmup, ndigits=6),tx0_pulse_time[-1]]))) + tx0_gate_time = np.round(np.concatenate((tx0_gate_time, np.array([tx0_pulse_time[0] - self._tx_warmup, tx0_pulse_time[-1] + self._system.rf_ringdown_time]))), decimals = 6) + + if block.adc is not None: + # log.info('prescribed dwell time:', block.adc.dwell) + self._rx_div = np.round(block.adc.dwell / self._clk_t).astype(int) + self._rx_t = self._clk_t * self._rx_div + rx_t_debug = self._rx_t + # log.info('rx_t:', rx_t_debug) + + # log.info(self._rx_div * self._clk_t, '!= dwell') + # log.info('Dwell time', block.adc.dwell, 'rounded to:', self._rx_t) + + rx0_start = block_duration + np.max([block.adc.dead_time, block.adc.delay]) + # rx0_start = block_duration + block.adc.dead_time + block.adc.delay # pp does this, why? TODO: Figure out adc in Sequence block_durations: could be a bug + rx0_end = rx0_start + (block.adc.num_samples * self._rx_t) + # log.info('rx_time:', (rx0_end - rx0_start)* 1e6) + + rx0_gate_amp = np.concatenate((rx0_gate_amp, np.array([1.0, 0.0]))) + rx0_gate_time = np.concatenate((rx0_gate_time,np.array([rx0_start, rx0_end]))) + + self._num_samples_total += block.adc.num_samples + if block.adc.freq_offset > 0: + self._lo_freq_center = self._center_freq + block.adc_freq_offset + + + + block_duration += self._seq.block_durations[block_counter] + + block_duration_us = block_duration * 1e6 + self._block_duration_us = block_duration_us + # Make all arrays shape compatible and test before putting it in a dict - roll it into a class' definition to clean up code + if grad_vx_amp.shape[0] > 1: + grad_vx_amp_cat = grad_vx_amp + grad_vx_time_cat = grad_vx_time * 1e6 # us + else: + grad_vx_amp_cat = [0.0] + grad_vx_time_cat = [0.0] + + if grad_vy_amp.shape[0] > 1: + grad_vy_amp_cat = grad_vy_amp + grad_vy_time_cat = grad_vy_time * 1e6 # us + else: + grad_vy_amp_cat = [0.0] + grad_vy_time_cat = [0.0] + + if grad_vz_amp.shape[0] > 1: + grad_vz_amp_cat = grad_vz_amp + grad_vz_time_cat = grad_vz_time * 1e6 # us + else: + grad_vz_amp_cat = [0.0] + grad_vz_time_cat = [0.0] + + # Add this as dummy for now. not really sure whether we will depracate this + grad_vz2_amp_cat = [0.0] + grad_vz2_time_cat = [0.0] + + + # assert(grad_vx_amp_cat.shape == grad_vx_time_cat.shape), 'Issue with Gx waveform, cannot proceed' + # assert(grad_vy_amp_cat.shape == grad_vy_time_cat.shape), 'Issue with Gy waveform, cannot proceed' + # assert(grad_vz_amp_cat.shape == grad_vz_time_cat.shape), 'Issue with Gz waveform, cannot proceed' + + if tx0_amp.shape[0] > 0: + tx0_amp_cat = tx0_amp + tx0_time_cat =tx0_time * 1e6 # us + tx0_gate_amp_cat = tx0_gate_amp + tx0_gate_time_cat = tx0_gate_time * 1e6 # us + + else: + tx0_amp_cat =[0.0, 0.0] + tx0_time_cat = [0.0, block_duration_us] + tx0_gate_amp_cat = [0.0, 0.0] + tx0_gate_time_cat = [0.0, block_duration_us] + + if rx0_gate_amp.shape[0] > 0: + rx0_gate_amp_cat = rx0_gate_amp + rx0_gate_time_cat = rx0_gate_time * 1e6 # us + + else: + rx0_gate_amp_cat = [0.0] + rx0_gate_time_cat =[0.0] + + # log.info('Obtained amplitudes and times for all blocks') + # log.info('Making the flodict: six lines to the spectrometer') + + flo_dict = dict() + # Tx - gate and first channel + flo_dict['tx_gate'] = self.curate(tx0_gate_time_cat, tx0_gate_amp_cat) + flo_dict['tx0'] = self.curate(tx0_time_cat, tx0_amp_cat) + + # Gradients + flo_dict['grad_vx'] = self.curate(np.array(grad_vx_time_cat), np.array(grad_vx_amp_cat)) + flo_dict['grad_vy'] = self.curate(np.array(grad_vy_time_cat), np.array(grad_vy_amp_cat)) + flo_dict['grad_vz'] = self.curate(np.array(grad_vz_time_cat), np.array(grad_vz_amp_cat)) + flo_dict['grad_vz2'] = self.curate(np.array(grad_vz2_time_cat), np.array(grad_vz2_amp_cat)) + # Rx + flo_dict['rx0_en'] = self.curate(rx0_gate_time_cat, rx0_gate_amp_cat) # no control on sampling rate + + log.info('flodict', flo_dict) + # Save the final dictionary as a property of the object + self._grad_t = self._grad_t * 1e6 #us - seq world to marcos world + self._rx_t = self._rx_t * 1e6 #us + self._flo_dict = flo_dict + + + + \ No newline at end of file diff --git a/external/marcos_client/.gitignore b/external/marcos_client/.gitignore old mode 100644 new mode 100755 diff --git a/external/marcos_client/__init__.py b/external/marcos_client/__init__.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/ref_test_fhd_too_fast.csv b/external/marcos_client/csvs/ref_test_fhd_too_fast.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/ref_test_many_uneven_latencies.csv b/external/marcos_client/csvs/ref_test_many_uneven_latencies.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/ref_test_oc1_too_fast.csv b/external/marcos_client/csvs/ref_test_oc1_too_fast.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/ref_test_two_uneven_latencies.csv b/external/marcos_client/csvs/ref_test_two_uneven_latencies.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_auto_leds_expt.csv b/external/marcos_client/csvs/test_auto_leds_expt.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_cfg.csv b/external/marcos_client/csvs/test_cfg.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_cic_shift_expt.csv b/external/marcos_client/csvs/test_cic_shift_expt.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_fhd_many.csv b/external/marcos_client/csvs/test_fhd_many.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_fhd_multiple.csv b/external/marcos_client/csvs/test_fhd_multiple.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_fhd_series.csv b/external/marcos_client/csvs/test_fhd_series.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_fhd_single.csv b/external/marcos_client/csvs/test_fhd_single.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_fhd_too_fast.csv b/external/marcos_client/csvs/test_fhd_too_fast.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_four_par.csv b/external/marcos_client/csvs/test_four_par.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_four_par_expt_iq.csv b/external/marcos_client/csvs/test_four_par_expt_iq.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_init_grad_expt_fhd.csv b/external/marcos_client/csvs/test_init_grad_expt_fhd.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_init_grad_expt_oc1.csv b/external/marcos_client/csvs/test_init_grad_expt_oc1.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_lo_change_expt.csv b/external/marcos_client/csvs/test_lo_change_expt.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_long_time.csv b/external/marcos_client/csvs/test_long_time.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_many_quick.csv b/external/marcos_client/csvs/test_many_quick.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_many_uneven_latencies.csv b/external/marcos_client/csvs/test_many_uneven_latencies.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_mult_quick.csv b/external/marcos_client/csvs/test_mult_quick.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_four.csv b/external/marcos_client/csvs/test_oc1_four.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_many.csv b/external/marcos_client/csvs/test_oc1_many.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_series.csv b/external/marcos_client/csvs/test_oc1_series.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_series_same.csv b/external/marcos_client/csvs/test_oc1_series_same.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_single.csv b/external/marcos_client/csvs/test_oc1_single.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_too_fast.csv b/external/marcos_client/csvs/test_oc1_too_fast.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_two.csv b/external/marcos_client/csvs/test_oc1_two.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_two_dict.csv b/external/marcos_client/csvs/test_oc1_two_dict.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_oc1_two_same.csv b/external/marcos_client/csvs/test_oc1_two_same.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_rx_simple.csv b/external/marcos_client/csvs/test_rx_simple.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_single.csv b/external/marcos_client/csvs/test_single.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_single_delays.csv b/external/marcos_client/csvs/test_single_delays.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_single_expt.csv b/external/marcos_client/csvs/test_single_expt.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_single_quick.csv b/external/marcos_client/csvs/test_single_quick.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_stream_quick.csv b/external/marcos_client/csvs/test_stream_quick.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_three_delays.csv b/external/marcos_client/csvs/test_three_delays.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_three_quick.csv b/external/marcos_client/csvs/test_three_quick.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_two_delays b/external/marcos_client/csvs/test_two_delays old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_two_delays.csv b/external/marcos_client/csvs/test_two_delays.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_two_quick.csv b/external/marcos_client/csvs/test_two_quick.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_two_uneven_latencies.csv b/external/marcos_client/csvs/test_two_uneven_latencies.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_tx_complex_expt.csv b/external/marcos_client/csvs/test_tx_complex_expt.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_uneven_sparse.csv b/external/marcos_client/csvs/test_uneven_sparse.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_uneven_sparse_expt_fhd.csv b/external/marcos_client/csvs/test_uneven_sparse_expt_fhd.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_uneven_sparse_expt_oc1.csv b/external/marcos_client/csvs/test_uneven_sparse_expt_oc1.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/csvs/test_uneven_times.csv b/external/marcos_client/csvs/test_uneven_times.csv old mode 100644 new mode 100755 diff --git a/external/marcos_client/examples.py b/external/marcos_client/examples.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/experiment.py b/external/marcos_client/experiment.py old mode 100644 new mode 100755 index 559d72b..d93c307 --- a/external/marcos_client/experiment.py +++ b/external/marcos_client/experiment.py @@ -65,7 +65,7 @@ def __init__(self, seq_dict=None, seq_csv=None, rx_lo=0, # which of internal NCO local oscillators (LOs), out of 0, 1, 2, to use for each channel - grad_max_update_rate=0.2, # MSPS, across all channels in parallel, best-effort + grad_max_update_rate=0.125, # was 0.125 MSPS, across all channels in parallel, best-effort gpa_fhdo_offset_time=0, # when GPA-FHDO is used, offset the Y, Z and Z2 gradient times by 1x, 2x and 3x this value to emulate 'simultaneous' updates print_infos=True, # show server info messages assert_errors=True, # halt on server errors @@ -120,6 +120,8 @@ def __init__(self, if initial_wait is None: # auto-set the initial wait to be long enough for initial gradient configuration to finish, plus 1us for miscellaneous startup self._initial_wait = 1 + 1/grad_max_update_rate + else: + self._initial_wait = initial_wait self._auto_leds = auto_leds diff --git a/external/marcos_client/grad_board.py b/external/marcos_client/grad_board.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/local_config.py b/external/marcos_client/local_config.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/local_config.py.example b/external/marcos_client/local_config.py.example old mode 100644 new mode 100755 diff --git a/external/marcos_client/local_config2.py b/external/marcos_client/local_config2.py new file mode 100755 index 0000000..eff5319 --- /dev/null +++ b/external/marcos_client/local_config2.py @@ -0,0 +1,25 @@ +## IP address: RP address or 'localhost' if emulating a local server. +## Uncomment one of the lines below. +#ip_address = "localhost" +ip_address = "10.42.0.114" + +## Port: always 11111 for now +port = 11111 + +## FPGA clock frequency: uncomment one of the below to configure various +## system behaviour. Right now only 122.88 is supported. +fpga_clk_freq_MHz = 122.88 # RP-122 +#fpga_clk_freq_MHz = 125.0 # RP-125 + +## Gradient board: uncomment one of the below to configure the gradient data format +grad_board = "gpa-fhdo" +#grad_board = "ocra1" + +## GPA-FHDO current per volt setting (determined by resistors) +gpa_fhdo_current_per_volt = 2.5 + +## Flocra-pulseq path, for use of the flocra-pulseq library (optional). +## Uncomment the lines below and adjust the path to suit your +## flocra-pulseq location. +#import sys +#sys.path.append('/home/vlad/Documents/mri/flocra-pulseq') \ No newline at end of file diff --git a/external/marcos_client/marcompile.py b/external/marcos_client/marcompile.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/marcostek.py b/external/marcos_client/marcostek.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/marmachine.py b/external/marcos_client/marmachine.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/plot_csv.py b/external/marcos_client/plot_csv.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/readme.org b/external/marcos_client/readme.org old mode 100644 new mode 100755 diff --git a/external/marcos_client/ref_loopback.npz b/external/marcos_client/ref_loopback.npz old mode 100644 new mode 100755 diff --git a/external/marcos_client/server_comms.py b/external/marcos_client/server_comms.py old mode 100644 new mode 100755 index e3dfcf7..71ee8a4 --- a/external/marcos_client/server_comms.py +++ b/external/marcos_client/server_comms.py @@ -2,6 +2,7 @@ import msgpack, warnings from external.marcos_client.marmachine import MarServerWarning +# from marmachine import MarServerWarning version_major = 1 version_minor = 0 diff --git a/external/marcos_client/test_base.py b/external/marcos_client/test_base.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_gpa_fhdo.py b/external/marcos_client/test_gpa_fhdo.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_gpa_fhdo.seq b/external/marcos_client/test_gpa_fhdo.seq old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_long_sequence.py b/external/marcos_client/test_long_sequence.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_loopback.py b/external/marcos_client/test_loopback.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_loopback.seq b/external/marcos_client/test_loopback.seq old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_marga.py b/external/marcos_client/test_marga.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_marga_model.py b/external/marcos_client/test_marga_model.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_noise.py b/external/marcos_client/test_noise.py old mode 100644 new mode 100755 diff --git a/external/marcos_client/test_server.py b/external/marcos_client/test_server.py old mode 100644 new mode 100755 index d5a8c65..c43d6d6 --- a/external/marcos_client/test_server.py +++ b/external/marcos_client/test_server.py @@ -12,7 +12,7 @@ st = pdb.set_trace -from local_config import ip_address, port, fpga_clk_freq_MHz, grad_board +from local_config2 import ip_address, port, fpga_clk_freq_MHz, grad_board from server_comms import * diff --git a/external/marcos_experiments/.gitignore b/external/marcos_experiments/.gitignore old mode 100644 new mode 100755 diff --git a/external/marcos_experiments/LICENSE b/external/marcos_experiments/LICENSE old mode 100644 new mode 100755 diff --git a/external/marcos_experiments/my_first_experiment.py b/external/marcos_experiments/my_first_experiment.py old mode 100644 new mode 100755 diff --git a/external/marcos_experiments/pulseq_assembler.py b/external/marcos_experiments/pulseq_assembler.py old mode 100644 new mode 100755 diff --git a/external/marcos_extras/.gitignore b/external/marcos_extras/.gitignore old mode 100644 new mode 100755 diff --git a/external/marcos_extras/marcos_fpga_rp-122.bit b/external/marcos_extras/marcos_fpga_rp-122.bit old mode 100644 new mode 100755 diff --git a/external/marcos_extras/marcos_fpga_rp-122.bit.bin b/external/marcos_extras/marcos_fpga_rp-122.bit.bin old mode 100644 new mode 100755 diff --git a/external/marcos_extras/marcos_fpga_rp-122.dtbo b/external/marcos_extras/marcos_fpga_rp-122.dtbo old mode 100644 new mode 100755 diff --git a/external/marcos_extras/marcos_fpga_rp-125.bit b/external/marcos_extras/marcos_fpga_rp-125.bit old mode 100644 new mode 100755 diff --git a/external/marcos_extras/marcos_fpga_rp-125.bit.bin b/external/marcos_extras/marcos_fpga_rp-125.bit.bin old mode 100644 new mode 100755 diff --git a/external/marcos_extras/marcos_fpga_rp-125.dtbo b/external/marcos_extras/marcos_fpga_rp-125.dtbo old mode 100644 new mode 100755 diff --git a/external/marcos_extras/readme.org b/external/marcos_extras/readme.org old mode 100644 new mode 100755 diff --git a/external/marcos_server/.gitignore b/external/marcos_server/.gitignore old mode 100644 new mode 100755 diff --git a/external/marcos_server/readme.org b/external/marcos_server/readme.org old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/CMakeLists.txt b/external/marcos_server/src/CMakeLists.txt old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/hardware.cpp b/external/marcos_server/src/hardware.cpp old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/hardware.hpp b/external/marcos_server/src/hardware.hpp old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/iface.cpp b/external/marcos_server/src/iface.cpp old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/iface.hpp b/external/marcos_server/src/iface.hpp old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/marcos_server.cpp b/external/marcos_server/src/marcos_server.cpp old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/mpack/mpack.c b/external/marcos_server/src/mpack/mpack.c old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/mpack/mpack.h b/external/marcos_server/src/mpack/mpack.h old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/server_config.h b/external/marcos_server/src/server_config.h old mode 100644 new mode 100755 diff --git a/external/marcos_server/src/version.hpp b/external/marcos_server/src/version.hpp old mode 100644 new mode 100755 diff --git a/external/marga/.gitignore b/external/marga/.gitignore old mode 100644 new mode 100755 diff --git a/external/marga/LICENSE b/external/marga/LICENSE old mode 100644 new mode 100755 diff --git a/external/marga/component.xml b/external/marga/component.xml old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ad5781_model.sv b/external/marga/hdl/ad5781_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ad5781_model_tb.sv b/external/marga/hdl/ad5781_model_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ads8684_model.sv b/external/marga/hdl/ads8684_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ads8684_model_tb.sv b/external/marga/hdl/ads8684_model_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/dac80504_model.sv b/external/marga/hdl/dac80504_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/dac80504_model_tb.sv b/external/marga/hdl/dac80504_model_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/gpa_fhdo_iface.sv b/external/marga/hdl/gpa_fhdo_iface.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/gpa_fhdo_iface_tb.sv b/external/marga/hdl/gpa_fhdo_iface_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/gpa_fhdo_model.sv b/external/marga/hdl/gpa_fhdo_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_ad5781_model_tb.sav b/external/marga/hdl/icarus_compile/001_ad5781_model_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_dac80504_model_tb.sav b/external/marga/hdl/icarus_compile/001_dac80504_model_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_gpa_fhdo_iface_tb.sav b/external/marga/hdl/icarus_compile/001_gpa_fhdo_iface_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_marbuffer_tb.sav b/external/marga/hdl/icarus_compile/001_marbuffer_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_mardecode_tb.sav b/external/marga/hdl/icarus_compile/001_mardecode_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_marfifo_tb.sav b/external/marga/hdl/icarus_compile/001_marfifo_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_marga_simple_tb.sav b/external/marga/hdl/icarus_compile/001_marga_simple_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/icarus_compile/001_ocra1_iface_tb.sav b/external/marga/hdl/icarus_compile/001_ocra1_iface_tb.sav old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marbuffer.sv b/external/marga/hdl/marbuffer.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marbuffer_tb.sv b/external/marga/hdl/marbuffer_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/mardecode.sv b/external/marga/hdl/mardecode.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/mardecode_tb.sv b/external/marga/hdl/mardecode_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marfifo.sv b/external/marga/hdl/marfifo.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marfifo_tb.sv b/external/marga/hdl/marfifo_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marga.sv b/external/marga/hdl/marga.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marga_model.sv b/external/marga/hdl/marga_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/marga_simple_tb.sv b/external/marga/hdl/marga_simple_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ocra1_iface.sv b/external/marga/hdl/ocra1_iface.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ocra1_iface_tb.sv b/external/marga/hdl/ocra1_iface_tb.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/ocra1_model.sv b/external/marga/hdl/ocra1_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/hdl/rx_chain_model.sv b/external/marga/hdl/rx_chain_model.sv old mode 100644 new mode 100755 diff --git a/external/marga/readme.org b/external/marga/readme.org old mode 100644 new mode 100755 diff --git a/external/marga/src/CMakeLists.txt b/external/marga/src/CMakeLists.txt old mode 100644 new mode 100755 diff --git a/external/marga/src/marga_model.cpp b/external/marga/src/marga_model.cpp old mode 100644 new mode 100755 diff --git a/external/marga/src/marga_model.hpp b/external/marga/src/marga_model.hpp old mode 100644 new mode 100755 diff --git a/external/marga/src/marga_sim.sav b/external/marga/src/marga_sim.sav old mode 100644 new mode 100755 diff --git a/external/marga/src/marga_sim_main.cpp b/external/marga/src/marga_sim_main.cpp old mode 100644 new mode 100755 diff --git a/external/marga/xgui/marga_v1_0.tcl b/external/marga/xgui/marga_v1_0.tcl old mode 100644 new mode 100755 diff --git a/external/ocra-pulseq/test_files/test_loopback.seq b/external/ocra-pulseq/test_files/test_loopback.seq old mode 100644 new mode 100755 diff --git a/external/seq/adjustments_acq/calibration.py b/external/seq/adjustments_acq/calibration.py old mode 100644 new mode 100755 index a625cc1..7b677cb --- a/external/seq/adjustments_acq/calibration.py +++ b/external/seq/adjustments_acq/calibration.py @@ -28,7 +28,7 @@ def larmor_step_search( seq_file=Path(mri4all_paths.DATA_ACQ) / "se_6.seq", step_search_center=cfg.LARMOR_FREQ, - steps=30, + steps=10, step_bw_MHz=5e-3, plot=False, shim_x=cfg.SHIM_X, @@ -36,9 +36,10 @@ def larmor_step_search( shim_z=cfg.SHIM_Z, delay_s=1, gui_test=False, + dummy_scans=3, ): """ - Run a stepped search through a range of frequencies to find the highest signal response + Run a stepped search through a range of frequencies to find the highest signal response and highest SNR signal. Used to find a starting point, not for precision Args: @@ -66,21 +67,22 @@ def larmor_step_search( # seq_file = constants.SCANNER_CONTROL_CAL_SEQ_FILES/'se_6.seq' # TODO: Seq file should be loadable or new sequence should be made # Run the experiment once to prep array - rxd, rx_t = scr.run_pulseq( - seq_file, - rf_center=larmor_freq, - tx_t=1, - grad_t=10, - tx_warmup=100, - shim_x=shim_x, - shim_y=shim_y, - shim_z=shim_z, - grad_cal=False, - save_np=False, - save_mat=False, - save_msgs=False, - gui_test=gui_test, - ) + for dummy_scan in range(1): # no dummy scan required for first run + rxd, rx_t = scr.run_pulseq( + seq_file, + rf_center=larmor_freq, + tx_t=1, + grad_t=10, + tx_warmup=100, + shim_x=shim_x, + shim_y=shim_y, + shim_z=shim_z, + grad_cal=False, + save_np=False, + save_mat=False, + save_msgs=False, + gui_test=gui_test, + ) # Create array for storing data rx_arr = np.zeros((rxd.shape[0], steps), dtype=np.cdouble) @@ -91,65 +93,66 @@ def larmor_step_search( # Pause for spin recovery time.sleep(delay_s) - snr_array = [] - peak_array = [] - # Repeat for each frequency after the first for i in range(0, steps): + print(f"{swept_freqs[i]:.4f} MHz ({i}/{steps})") ipc_comm.send_status( f"Adjusting frequency: Searching {swept_freqs[i]:.4f} MHz ({i+1}/{steps})" ) - rx_arr[:, i], _ = scr.run_pulseq( - seq_file, - rf_center=swept_freqs[i], - tx_t=1, - grad_t=10, - tx_warmup=100, - shim_x=shim_x, - shim_y=shim_y, - shim_z=shim_z, - grad_cal=False, - save_np=False, - save_mat=False, - save_msgs=False, - gui_test=gui_test, - ) + for dummy_scan in range(dummy_scans): # to get steady state + rxd_rep, _ = scr.run_pulseq( + seq_file, + rf_center=swept_freqs[i], + tx_t=1, + grad_t=10, + tx_warmup=100, + shim_x=shim_x, + shim_y=shim_y, + shim_z=shim_z, + grad_cal=False, + save_np=False, + save_mat=False, + save_msgs=False, + gui_test=gui_test, + ) + rx_arr[:, i] += rxd_rep + rx_arr[:, i] = rx_arr[:, i] / dummy_scans # averaging instead of using the last scan + larmor_freq_peak, larmor_freq_snr, _, _, best_snr_index = get_freq_vals_from_echoes(steps, rx_arr, larmor_freq, swept_freqs[1]- swept_freqs[0]) # Calculate signal to noise ratio - signal_index = 0 - noise_index = 0 - for index in range(0, rxd.shape[0] - 1): - if index > rxd.shape[0] / 4 and index < (rxd.shape[0] - rxd.shape[0] / 4): - signal_array[signal_index, i] = rx_arr[index, i] - signal_index += 1 - else: - noise_array[noise_index, i] = rx_arr[index, i] - noise_index += 1 - snr = np.mean(np.abs(signal_array[:, i])) / np.std(np.abs(noise_array[:, i])) - peak = np.max(np.abs(rx_arr[index, i])) - print(f"SNR({i}) = " + str(snr)) - print(f"Peak({i}) = " + str(peak)) - snr_array.append(snr) - peak_array.append(peak) + # signal_index = 0 + # noise_index = 0 + # for index in range(0, rxd.shape[0] - 1): + # if index > rxd.shape[0] / 4 and index < (rxd.shape[0] - rxd.shape[0] / 4): + # signal_array[signal_index, i] = rx_arr[index, i] + # signal_index += 1 + # else: + # noise_array[noise_index, i] = rx_arr[index, i] + # noise_index += 1 + # snr = np.mean(np.abs(signal_array[:, i])) / np.std(np.abs(noise_array[:, i])) + # peak = np.max(np.abs(rx_arr[index, i])) + # print(f"SNR({i}) = " + str(snr)) + # print(f"Peak({i}) = " + str(peak)) + # snr_array.append(snr) + # peak_array.append(peak) # print("Test = ") # print(np.max(np.abs(rx_arr), axis=0)) # Find the frequency data with the largest maximum absolute value # max_ind = np.argmax(np.max(np.abs(rx_arr), axis=0, keepdims=False)) - max_ind = np.argmax(peak_array) - - max_freq = swept_freqs[max_ind] - print(f"Fequency with highest amplitude: {max_freq:.4f} MHz") + # max_ind = np.argmax(peak_array) + # max_freq = swept_freqs[max_ind] + # print(f"Frequency with highest amplitude: {max_freq:.4f} MHz") - # Find the frequency data with the largest maximum SNR value - max_snr_ind = np.argmax(snr_array) - max_snr_freq = swept_freqs[max_snr_ind] - print(f"Frequency with highest SNR: {max_snr_freq:.4f} MHz") + # # Find the frequency data with the largest maximum SNR value + # max_snr_ind = np.argmax(snr_array) + # max_snr_freq = swept_freqs[max_snr_ind] + # print(f"Frequency with highest SNR: {max_snr_freq:.4f} MHz") - # Plot setup for UI - plt.style.use("dark_background") + # # Plot setup for UI + # plt.style.use("dark_background") # Plot figure if plot: @@ -178,7 +181,7 @@ def larmor_step_search( data_dict = {"rx_arr": rx_arr, "rx_t": rx_t, "larmor_freq": larmor_freq} # Return the frequency that worked the best with SNR - return max_freq, max_snr_freq, data_dict, fig_signal, fig_noise + return larmor_freq_peak, larmor_freq_snr, data_dict, fig_signal, fig_noise, best_snr_index def larmor_cal( @@ -261,25 +264,32 @@ def larmor_cal( # - Calculating the difference between every point in the middle third of the echo data # - Cutting out the largest differences (representing phase wraps) by removing all changes above a certain size # - Averaging from there + print( + "Using old method for calculating average phase slope, this will be deprecated in the future" + ) + ordered_dphis = np.zeros(rx_count // 3 - 1) for echo_n in range(echo_count): dphis = np.ediff1d( np.angle(rx_arr[echo_n, rx_count // 3 : 2 * (rx_count // 3)]) ) stds[echo_n] = np.std(dphis) - ordered_dphis = dphis[np.argsort(np.abs(dphis))] - large_change_ind = np.argmax(np.abs(np.ediff1d(np.abs(ordered_dphis)))) + ordered_dphis[:] = dphis[np.argsort(np.abs(dphis))] + large_change_ind = np.argmax( + np.abs(np.ediff1d(np.abs(ordered_dphis))) + ) dphi_vals = ordered_dphis[: large_change_ind - 1] avgs[echo_n] = np.mean(dphi_vals) - # Find the average slopes across echoes, find expected change in larmor frequency from there - dphi = np.mean(avgs) - dw = dphi / (rx_t * np.pi) - std = np.mean(stds) - print(f" Estimated frequency offset: {dw:.6f} MHz") - print(f" Spread (std): {std:.6f}") + + # Calculate the best SNR by dividing avgs by stds element-wise + snr_values = avgs / stds + best_snr_index = np.argmax(snr_values) + print(f"Best SNR index: {best_snr_index}, SNR: {snr_values[best_snr_index]:.6f}") + # Update larmor frequency - larmor_freq += dw * step_size + larmor_freq += best_snr_index * step_size + print(f"New larmor frequency: {larmor_freq:.5f} MHz") # Delay for spin recovery time.sleep(delay_s) @@ -312,24 +322,24 @@ def larmor_cal( ) # Announce results - print(f"Calibrated Larmor frequency: {larmor_freq:.6f} MHz") - if std >= 1: - print( - "Didn't converge (std = " - + str(std) - + f"), try {fft_x[np.argmax(rx_fft[:, 0])]:.6f}" - ) - # larmor_freq = fft_x[np.argmax(rx_fft[:, 0])] - larmor_freq = larmor_start + # print(f"Calibrated Larmor frequency: {larmor_freq:.6f} MHz") + # if std >= 1: + # print( + # "Didn't converge (std = " + # + str(std) + # + f"), try {fft_x[np.argmax(rx_fft[:, 0])]:.6f}" + # ) + # larmor_freq = fft_x[np.argmax(rx_fft[:, 0])] + # # larmor_freq = larmor_start # Plot if needed if plot: fig, axs = plt.subplots(5, 1, constrained_layout=True) - if std < 1: - fig.suptitle(f"Larmor: {larmor_freq:.4f} MHz") - else: - fig.suptitle(f"Didn't converge -- Try eyeballing from bottom graph") + # if std < 1: + # fig.suptitle(f"Larmor: {larmor_freq:.4f} MHz") + # else: + # fig.suptitle(f"Didn't converge -- Try eyeballing from bottom graph") axs[0].plot(np.real(rxd)) axs[0].set_title("Concatenated signal -- Real") @@ -414,7 +424,7 @@ def rf_max_cal( # Cap search values to not hit system limits rf_min, rf_max = 0.05, 0.95 - rf_max_val = 0 + rf_max_val = 0 # Run iterative search for it in range(iterations): @@ -428,7 +438,11 @@ def rf_max_cal( # Repeatedly run the experiment from seq file for i in range(points): # Cap rf value if needed for system - adj_rf_max = max(cfg.RF_MAX * cfg.RF_PI2_FRACTION, 5000) / rf_amp_vals[i] + print(f"{rf_amp_vals[i]:.4f} ({i}/{points})") + ipc_comm.send_status( + f"Adjusting amplitude: Searching {rf_amp_vals[i]:.4f} ({i+1}/{points})" + ) + adj_rf_max = max(cfg.RF_MAX * cfg.RF_PI2_FRACTION, cfg.RF_MAX) / rf_amp_vals[i] rxd, rx_t = scr.run_pulseq( seq_file, rf_center=larmor_freq, @@ -479,8 +493,9 @@ def rf_max_cal( else: max_ind = np.argmax(peak_max_arr) rf_max_val = rf_amp_vals[max_ind] - + print(f"Estimated RF max: {rf_max_val:.2f} fractional power") # Plot if asked + plot = False if plot and it < iterations - 1: fig, axs = plt.subplots(2, 1, constrained_layout=True) fig.suptitle(f"Iteration {it + 1}/{iterations}") @@ -510,6 +525,7 @@ def rf_max_cal( ) # Plot if asked + plot = False if plot: fig, axs = plt.subplots(2, 1, constrained_layout=True) fig.suptitle(f"Iteration {it + 1}/{iterations}") @@ -531,6 +547,8 @@ def rf_max_cal( "rx_t": rx_t, "rxd_list": rxd_list, "rf_max": est_rf_max, + "peak_max_arr": peak_max_arr, + "rf_amp_vals": rf_amp_vals, } return est_rf_max, rf_pi2_fraction, data_dict @@ -543,7 +561,7 @@ def rf_duration_cal( smooth=True, iterations=2, first_max=False, - plot=True, + plot=False, ): """ Calibrate RF optimal duration for pi/2 flip angle @@ -658,7 +676,7 @@ def rf_duration_cal( plt.ioff() plt.show() - return rf_optimal_duration_val + return rf_optimal_duration_val, rf_duration_vals, peak_max_arr # TODO Add gui test functionality @@ -827,8 +845,11 @@ def readout_wf(tstart, echo_idx): peaks, _ = sig.find_peaks(np.abs(rx_fft), width=2) peak_results = sig.peak_widths(np.abs(rx_fft), peaks, rel_height=0.95) - max_peak = np.argmax(peak_results[0]) - fwhm = peak_results[0][max_peak] + if peak_results is not None: + max_peak = np.argmax(peak_results[0]) + fwhm = peak_results[0][max_peak] + else: + fwhm = 0 fft_scale = 1e6 / ( rx_period * rx_fft.shape[0] @@ -879,8 +900,8 @@ def readout_wf(tstart, echo_idx): cfg.GY_MAX = grad_max elif channel == "z": cfg.GZ_MAX = grad_max - - return grad_max + + return grad_max, fft_x, rx_fft, hline def shim_cal_linear( @@ -901,6 +922,7 @@ def shim_cal_linear( smooth=True, plot=True, gui_test=False, + grad_t = 10, ): """ Calibrate linear shims (offset for linear gradients) @@ -950,11 +972,11 @@ def shim_cal_linear( else: shim_z = shim - rxd, rx_t = scr.run_pulseq( + rxd, _ = scr.run_pulseq( seq_file, rf_center=larmor_freq, tx_t=1, - grad_t=10, + grad_t=grad_t, tx_warmup=100, shim_x=shim_x, shim_y=shim_y, @@ -995,7 +1017,7 @@ def shim_cal_linear( plt.show() - return best_shim + return best_shim, fwhm_list, shim_range def shim_cal_multicoil( @@ -1248,3 +1270,83 @@ def load_plot_in_ui( plot_result.type = "plot" plot_result.file_path = "/other/" + file_name + ".plot" return plot_result + + +def get_freq_vals_from_echoes(steps, rx_arr, larmor_freq, step_size, threshold=0.01, snr_tolerance = 0.1): + peaks = np.zeros(steps) + stds = np.zeros(steps) + snr_values = np.zeros(steps) + larmor_freq_peak = larmor_freq + larmor_freq_snr = larmor_freq + larmor_freq_played = np.zeros(steps) + print(f"Calculating SNR for {steps} echoes...") + for echo_n in range(steps): + + # Calculate the absolute value of the signal for the current echo + abs_signal = np.abs(rx_arr[echo_n, :]) + + # Determine the number of points to use for noise calculation + n_points = int(0.4 * abs_signal.shape[0]) + + # Calculate the peak value of the signal + peak_value = np.max(abs_signal) + + # Calculate noise level using the last n points + noise_level_right = np.mean(abs_signal[-n_points:]) + noise_level_left = np.mean(abs_signal[:n_points]) + noise_level = (noise_level_right + noise_level_left) / 2 + + + # Calculate the standard deviation of the last n points + if noise_level == 0: + print(f" Echo {echo_n + 1}: Noise standard deviation is zero, setting SNR to zero.") + snr = 0 + else: + # Calculate the SNR + # snr = peak_value / noise_std + snr = peak_value / noise_level if noise_level != 0 else 0 + + # Calculate the coefficient of variation + if peak_value == 0: + CV = 0 + else: + CV = (noise_level / peak_value) * 100 # susing level instead of std + + + # Store the SNR for the current echo + peaks[echo_n] = peak_value + stds[echo_n] = noise_level + snr_values[echo_n] = snr + larmor_freq_played[echo_n] = larmor_freq + (echo_n * step_size) + print(f" Echo {echo_n + 1}: Frequency = {larmor_freq + (echo_n * step_size):.4f}, Peak = {peak_value:.6f}, Noise = {noise_level:.6f}, SNR = {snr:.6f}, CV_like = {CV:.2f}%") + + # Calculate the best SNR by dividing avgs by stds element-wise + + # Check if the top two or three SNR values are within the tolerance + sorted_indices = np.argsort(snr_values)[::-1] # Sort indices by SNR in descending order + top_snr_indices = sorted_indices[:3] # Get indices of top three SNR values + + if len(top_snr_indices) > 1 and (snr_values[top_snr_indices[0]] - snr_values[top_snr_indices[1]] <= snr_tolerance): + # If the top two SNR values are within the tolerance + if len(top_snr_indices) > 2 and (snr_values[top_snr_indices[1]] - snr_values[top_snr_indices[2]] <= snr_tolerance): + # If the top three SNR values are also within the tolerance + best_snr_index = top_snr_indices[np.argmax(peaks[top_snr_indices[:3]])] + else: + best_snr_index = top_snr_indices[np.argmax(peaks[top_snr_indices[:2]])] + else: + # If the top two SNR values are not within the tolerance, take the best SNR index + best_snr_index = np.argmax(snr_values) + + + best_peak_index = np.argmax(peaks) + print(f"Best SNR index: {best_snr_index}, SNR: {snr_values[best_snr_index]:.6f}") + + + # Update larmor frequency + + larmor_freq_peak = larmor_freq_played[best_peak_index] + print(f"New larmor frequency peak: {larmor_freq_peak:.5f} MHz") + larmor_freq_snr = larmor_freq_played[best_snr_index] + print(f"New larmor frequency snr: {larmor_freq_snr:.5f} MHz") + + return larmor_freq_peak, larmor_freq_snr, peaks, stds, best_snr_index \ No newline at end of file diff --git a/external/seq/adjustments_acq/config.py b/external/seq/adjustments_acq/config.py old mode 100644 new mode 100755 index 1010555..2544b6a --- a/external/seq/adjustments_acq/config.py +++ b/external/seq/adjustments_acq/config.py @@ -22,9 +22,6 @@ SHIM_Z = configuration_data.shim_parameters.shim_z SHIM_MC = configuration_data.shim_parameters.shim_mc -DBG_FA_EXC = 90 -DBG_FA_REF = 180 - def update(): global configuration_data diff --git a/external/seq/adjustments_acq/scripts.py b/external/seq/adjustments_acq/scripts.py old mode 100644 new mode 100755 index c8d7ba4..0444df6 --- a/external/seq/adjustments_acq/scripts.py +++ b/external/seq/adjustments_acq/scripts.py @@ -15,9 +15,8 @@ import external.seq.adjustments_acq.config as cfg # pylint: disable=import-error import external.marcos_client.experiment as ex # pylint: disable=import-error -from external.flocra_pulseq.interpreter import ( - PSInterpreter, -) # pylint: disable=import-error + +from external.flocra_pulseq.interpreter_pp import seq2flocra import common.helper as helper from common.constants import * @@ -27,6 +26,7 @@ from common.ipc import Communicator + ipc_comm = Communicator(Communicator.ACQ) @@ -55,6 +55,7 @@ def run_pulseq( raw_filename="", expected_duration_sec=-1, hardware_simulation=False, + system = None, ): """ Interpret pulseq .seq file through flocra_pulseq @@ -75,93 +76,45 @@ def run_pulseq( expt (flocra_pulseq.interpreter): Default None, pass in existing experiment to continue an object plot_instructions (bool): Default None, plot instructions for debugging gui_test (bool): Default False, load dummy data for gui testing - + system (pp.Opts): Default None, system configuration for pypulseq Returns: numpy.ndarray: Rx data array float: (us) Rx period """ log.info(f"Pulseq scan with Larmor {rf_center}") - - log.debug("Running flocra_pulseq using following parameters:") - log.debug(f"rf_center={rf_center}") - log.debug(f"rf_max={rf_max}") - log.debug(f"gx_max={gx_max}") - log.debug(f"gy_max={gy_max}") - log.debug(f"gz_max={gz_max}") - log.debug(f"shim_x={shim_x}") - log.debug(f"shim_y={shim_y}") - log.debug(f"shim_z={shim_z}") - log.debug(f"Seq file={seq_file}") + log.info("Running flocra_pulseq using following parameters:") + log.info(f"rf_center={rf_center}") + log.info(f"rf_max={rf_max}") + log.info(f"gx_max={gx_max}") + log.info(f"gy_max={gy_max}") + log.info(f"gz_max={gz_max}") + log.info(f"shim_x={shim_x}") + log.info(f"shim_y={shim_y}") + log.info(f"shim_z={shim_z}") + log.info(f"Seq file={seq_file}") print(f"case path = {case_path}") - # Convert .seq file to machine dict - psi = PSInterpreter( - rf_center=rf_center * 1e6, - tx_warmup=tx_warmup, - rf_amp_max=rf_max, - tx_t=tx_t, - grad_t=grad_t, - gx_max=gx_max, - gy_max=gy_max, - gz_max=gz_max, - log_file=case_path + "/flocra", - ) - instructions, param_dict = psi.interpret(seq_file) - - # Shim - log.debug("Running shim function...") - instructions = shim(instructions, (shim_x, shim_y, shim_z)) - - # temp = instructions - # instructions = { - # "tx0": temp["tx0"], - # "tx1": temp["tx0"], # DBG: Running the TX0 also on TX1 for testing purpose - # "grad_vx": temp["grad_vx"], - # "grad_vy": temp["grad_vy"], - # "grad_vz": temp["grad_vz"], - # "grad_vz2": temp["grad_vz2"], - # "rx0_en": temp["rx0_en"], - # "tx_gate": temp["tx_gate"], - # } - # print(instructions) - - if plot_instructions: - plt.clf() - _, axs = plt.subplots(3, 1, sharex="col", constrained_layout=True) - for key in ["tx0"]: - axs[0].step( - instructions[key][0], abs(instructions[key][1]), where="post", label=key - ) - for key in ["rx0_en"]: - axs[1].step( - instructions[key][0], instructions[key][1], where="post", label=key - ) - for key in ["grad_vx", "grad_vy", "grad_vz", "grad_vz2"]: - axs[2].step( - instructions[key][0], instructions[key][1], where="post", label=key - ) - for ax in axs: - ax.legend() - ax.grid(True, color="#333") - - if hardware_simulation: - log.info("Hardware simulation set. Skipping scan.") - return [], [] - + # Initialize the interpreter object and feed seq file or object + psi = seq2flocra(center_freq=rf_center * 1e6, + rf_amp_max=rf_max, system=system) + psi.load_seqfile(seq_file) + psi.block_events_to_amps_times() + instructions = psi._flo_dict + log.info("***GPA grad t***: ", psi._grad_t) # Initialize experiment class if expt is None: log.debug("Initializing marcos client...") expt = ex.Experiment( lo_freq=rf_center, - rx_t=param_dict["rx_t"], + rx_t=psi._rx_t, init_gpa=True, - gpa_fhdo_offset_time=grad_t / 3, - grad_max_update_rate=0.125, + gpa_fhdo_offset_time= psi._grad_t / 3, # psi._grad_t / 3 + grad_max_update_rate=0.125,# 0.125 - 0.06125 works halt_and_reset=True, ) - - # Optionbally run gradient linearization calibration + + # Optionally run gradient linearization calibration if grad_cal: expt.gradb.calibrate( channels=[0, 1, 2], @@ -171,12 +124,16 @@ def run_pulseq( poly_degree=5, ) - # Add flat delay to avoid housekeeping at the start - flat_delay = 10 - for buf in instructions.keys(): - instructions[buf] = (instructions[buf][0] + flat_delay, instructions[buf][1]) - # Load instructions + # instructions = { + # "tx0": psi._flo_dict['tx0'], + # "tx_gate": psi._flo_dict['tx_gate'], + # "rx0_en": psi._flo_dict['rx0_en'], # adc 0 + # "grad_vx": psi._flo_dict['grad_vx'], + # "grad_vy": psi._flo_dict['grad_vy'], + # "grad_vz": psi._flo_dict['grad_vz'], + # } + expt.add_flodict(instructions) # if plot_instructions: @@ -188,19 +145,22 @@ def run_pulseq( ipc_comm.send_acq_data(helper.get_datetime(), expected_duration_sec, False) # Run experiment + + log.debug('instructions:.......') + # log.debug(instructions) + rxd, msgs = expt.run() + # log.info('rxd shape:', rxd["rx0"].shape) # Optionally save messages if save_msgs: - print("Received messages:") - print("---") - print(msgs) # TODO include message saving - print("---") + log.debug("Received messages:") + log.debug("---") + log.debug(msgs) # TODO include message saving + log.debug("---") # Announce completion - nSamples = param_dict["readout_number"] - log.debug(f"Finished -- read {nSamples} samples") - + if not raw_filename: from datetime import datetime @@ -226,7 +186,7 @@ def run_pulseq( expt.__del__() # Return rx output array and rx period - return rxd["rx0"], param_dict["rx_t"] + return rxd["rx0"], psi._rx_t def shim(instructions, shim): @@ -438,9 +398,9 @@ def plot_signal_2d(recon_dict): if len(sys.argv) == 3: seq_file = cfg.SEQ_PATH + sys.argv[2] _, rx_t = run_pulseq(seq_file, save_np=True, save_mat=True) - print(f"rx_t = {rx_t}") + log.debug(f"rx_t = {rx_t}") else: - print( + log.debug( '"pulseq" takes one .seq filename as an argument (just the filename, make sure it\'s in your seq_files path!)' ) elif command == "plot2d": @@ -449,7 +409,7 @@ def plot_signal_2d(recon_dict): tr_count = int(sys.argv[3]) plot_signal_2d(recon_2d(rxd, tr_count, larmor_freq=cfg.LARMOR_FREQ)) else: - print('Format arguments as "plot2d [2d_data_filename] [tr count]"') + log.debug('Format arguments as "plot2d [2d_data_filename] [tr count]"') elif command == "plot1d": if len(sys.argv) == 5: rxd = np.load(cfg.DATA_PATH + sys.argv[2]) @@ -457,7 +417,7 @@ def plot_signal_2d(recon_dict): tr_count = int(sys.argv[4]) plot_signal_1d(recon_1d(rxd, rx_t, trs=tr_count)) else: - print( + log.debug( 'Format arguments as "plot1d [1d_data_filename] [rx_t] [tr_count]"' ) elif command == "plot_se": @@ -467,11 +427,11 @@ def plot_signal_2d(recon_dict): tr_count = int(sys.argv[4]) plot_signal_1d(recon_0d(rxd, rx_t, trs=tr_count)) else: - print( + log.debug( 'Format arguments as "plot_se [spin_echo_data_filename] [rx_t] [tr_count]"' ) else: - print("Enter a script command from: [pulseq, plot_se, plot1d, plot2d]") + log.debug("Enter a script command from: [pulseq, plot_se, plot1d, plot2d]") else: - print("Enter a script command from: [pulseq, plot_se, plot1d, plot2d]") + log.debug("Enter a script command from: [pulseq, plot_se, plot1d, plot2d]") diff --git a/external/seq/adjustments_acq/se_6.seq b/external/seq/adjustments_acq/se_6.seq old mode 100644 new mode 100755 diff --git a/external/seq/cal_seq_files/se_2.seq b/external/seq/cal_seq_files/se_2.seq old mode 100644 new mode 100755 diff --git a/installation/Vagrantfile b/installation/Vagrantfile index b1ea140..e657501 100755 --- a/installation/Vagrantfile +++ b/installation/Vagrantfile @@ -10,6 +10,7 @@ SCRIPT Vagrant.configure(2) do |config| config.vm.box = "bento/ubuntu-22.04" # 22.04 LTS config.vm.provision "shell", inline: $script + config.vm.provider "virtualbox" do |vb| # Increase memory for Virtualbox diff --git a/installation/install.sh b/installation/install.sh index af80f75..702b2a5 100755 --- a/installation/install.sh +++ b/installation/install.sh @@ -3,6 +3,10 @@ set -euo pipefail MRI4ALL_BASE=/opt/mri4all MRI4ALL_USER=vagrant +DELTA_BASE=/opt/ +DELTA_GRAD_BASE=/opt/planar_gradient_coil_design +DELTA_PASSIVE_SHIMMING_BASE=/opt/passive_shimming + error() { local parent_lineno="$1" @@ -69,19 +73,40 @@ create_folders () { create_folder $MRI4ALL_BASE/data create_folder $MRI4ALL_BASE/config create_folder $MRI4ALL_BASE/logs + create_folder $DELTA_GRAD_BASE + create_folder $DELTA_PASSIVE_SHIMMING_BASE } install_console() { echo "## Installing console repositories..." cd $MRI4ALL_BASE - sudo su $MRI4ALL_USER -c "git clone https://github.com/mri4all/console.git console" + sudo su $MRI4ALL_USER -c "git clone --branch workshop_2025 https://github.com/sairamgeethanath/console.git console" cd console if [ ! -e "$MRI4ALL_BASE/console/external/marcos_client/local_config.py" ]; then sudo su $MRI4ALL_USER -c "cp $MRI4ALL_BASE/console/external/marcos_client/local_config.py.example $MRI4ALL_BASE/console/external/marcos_client/local_config.py" fi } -install_python_dependencies() { +install_gradients() { + echo "## Installing gradient design repository..." + cd $DELTA_GRAD_BASE + sudo su $MRI4ALL_USER -c "git clone --branch workshop_2025 https://github.com/imr-framework/planar_gradient_coil_design.git planar_gradient_coil_design" + cd planar_gradient_coil_design + sudo su $MRI4ALL_USER -c "git submodule update --init --recursive" +} + +install_passive_shimming() { + echo "## Installing passive shimming repository..." + cd $DELTA_PASSIVE_SHIMMING_BASE + sudo su $MRI4ALL_USER -c "git clone --branch workshop_2025 https://github.com/imr-framework/passive_shimming.git passive_shimming" + cd passive_shimming + sudo su $MRI4ALL_USER -c "git submodule update --init --recursive" +} + + + + +install_python_dependencies_console() { echo "## Installing Python runtime environment..." if [ ! -e "$MRI4ALL_BASE/env" ]; then @@ -94,15 +119,53 @@ install_python_dependencies() { sudo su $MRI4ALL_USER -c "$MRI4ALL_BASE/env/bin/pip install --isolated -r \"$MRI4ALL_BASE/console/requirements.txt\"" } + +install_python_dependencies_gradients() { + echo "## Installing Python runtime environment..." + + if [ ! -e "$DELTA_GRAD_BASE/env" ]; then + sudo su $MRI4ALL_USER -c "mkdir \"$DELTA_GRAD_BASE/env\"" + sudo su $MRI4ALL_USER -c "python3 -m venv $DELTA_GRAD_BASE/env" + fi + + echo "## Installing required Python packages..." + cd /opt/mri4all/console + sudo su $MRI4ALL_USER -c "$DELTA_GRAD_BASE/env/bin/pip install --isolated -r \"$DELTA_GRAD_BASE/planar_gradient_coil_design/requirements.txt\"" +} + + +install_python_dependencies_passive_shimming() { + echo "## Installing Python runtime environment..." + + if [ ! -e "$DELTA_PASSIVE_SHIMMING_BASE/env" ]; then + sudo su $MRI4ALL_USER -c "mkdir \"$DELTA_PASSIVE_SHIMMING_BASE/env\"" + sudo su $MRI4ALL_USER -c "python3 -m venv $DELTA_PASSIVE_SHIMMING_BASE/env" + fi + + echo "## Installing required Python packages..." + cd /opt/mri4all/console + sudo su $MRI4ALL_USER -c "$DELTA_PASSIVE_SHIMMING_BASE/env/bin/pip install --isolated -r \"$DELTA_PASSIVE_SHIMMING_BASE/passive_shimming/requirements.txt\"" +} + + echo "" -echo "## Installing MRI4ALL console software..." +echo "## Installing MRI4ALL console software, planar gradient design, passive shimming and pypulseq..." echo "" install_linux_packages install_docker create_folders + install_console -install_python_dependencies +install_python_dependencies_console + +install_gradients +install_python_dependencies_gradients + +install_passive_shimming +install_python_dependencies_passive_shimming + + echo "" echo "Installation complete." diff --git a/installation/mri4all-sudoers b/installation/mri4all-sudoers old mode 100644 new mode 100755 diff --git a/installation/mri4all_acq.service b/installation/mri4all_acq.service old mode 100644 new mode 100755 diff --git a/installation/mri4all_recon.service b/installation/mri4all_recon.service old mode 100644 new mode 100755 diff --git a/installation/run_mri4all.sh b/installation/run_mri4all.sh old mode 100644 new mode 100755 diff --git a/mypy.ini b/mypy.ini old mode 100644 new mode 100755 diff --git a/pypulseq/SAR/QGlobal.mat b/pypulseq/SAR/QGlobal.mat deleted file mode 100644 index 3cd4257..0000000 Binary files a/pypulseq/SAR/QGlobal.mat and /dev/null differ diff --git a/pypulseq/SAR/SAR_calc.py b/pypulseq/SAR/SAR_calc.py deleted file mode 100644 index cbf2ac4..0000000 --- a/pypulseq/SAR/SAR_calc.py +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright of the Board of Trustees of Columbia University in the City of New York -from pathlib import Path -from typing import Tuple -from typing import Union - -import matplotlib.pyplot as plt -import numpy as np -import numpy.matlib -import scipy.io as sio -from scipy import interpolate - -from pypulseq.Sequence.sequence import Sequence -from pypulseq.calc_duration import calc_duration - - -def _calc_SAR(Q: np.ndarray, I: np.ndarray) -> np.ndarray: - """ - Compute the SAR output for a given Q matrix and I current values. - - Parameters - ---------- - Q : numpy.ndarray - Q matrix. Refer Graesslin, Ingmar, et al. "A specific absorption rate prediction concept for parallel - transmission MR." Magnetic resonance in medicine 68.5 (2012): 1664-1674. - I : numpy.ndarray - I matrix, capturing the current (in Amps) on each of the transmit channels. Refer Graesslin, Ingmar, et al. "A - specific absorption rate prediction concept for parallel transmission MR." Magnetic resonance in medicine - 68.5 (2012): 1664-1674. - - Returns - ------- - SAR : numpy.ndarray - Contains the SAR value for a particular Q matrix - """ - - if len(I.shape) == 1: # Just to fit the multi-transmit case for now, TODO - I = np.tile(I, (Q.shape[0], 1)) # Nc x Nt - - I_fact = np.divide(np.matmul(I, np.conjugate(I).T), I.shape[1]) - SAR_temp = np.multiply(Q, I_fact) - SAR = np.abs(np.sum(SAR_temp[:])) - - return SAR - - -def _load_Q() -> Tuple[np.ndarray, np.ndarray]: - """ - Load Q matrix that is precomputed based on the VHM model for 8 channels. Refer Graesslin, Ingmar, et al. "A - specific absorption rate prediction concept for parallel transmission MR." Magnetic resonance in medicine 68.5 - (2012): 1664-1674. - - Returns - ------- - Qtmf, Qhmf : numpy.ndarray - Contains the Q-matrix of global SAR values for body-mass and head-mass respectively. - """ - # Load relevant Q matrices computed from the model - this code will be integrated later - starting from E fields - path_Q = str(Path(__file__).parent / 'QGlobal.mat') - Q = sio.loadmat(path_Q) - Q = Q['Q'] - val = Q[0, 0] - - Qtmf = val['Qtmf'] - Qhmf = val['Qhmf'] - return Qtmf, Qhmf - - -def _SAR_from_seq(seq: Sequence, Qtmf: np.ndarray, Qhmf: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - Compute global whole body and head only SAR values for the given `seq` object. - - Parameters - ---------- - seq : Sequence - Sequence object to calculate for which SAR values will be calculated. - Qtmf : numpy.ndarray - Q-matrix of global SAR values for body-mass. - Qhmf : numpy.ndarray - Q-matrix of global SAR values for head-mass. - - Returns - ------- - SAR_wbg : numpy.ndarray - SAR values for body-mass. - SAR_hg : numpy.ndarray - SAR values for head-mass. - t : numpy.ndarray - Corresponding time points. - """ - # Identify RF blocks and compute SAR - 10 seconds must be less than twice and 6 minutes must be less than - # 4 (WB) and 3.2 (head-20) - block_events = seq.dict_block_events - num_events = len(block_events) - t = np.zeros(num_events) - SAR_wbg = np.zeros(t.shape) - SAR_hg = np.zeros(t.shape) - t_prev = 0 - - for block_counter in block_events: - block = seq.get_block(block_counter) - block_dur = calc_duration(block) - t[block_counter - 1] = t_prev + block_dur - t_prev = t[block_counter - 1] - if hasattr(block, 'rf'): # has rf - rf = block.rf - signal = rf.signal - # This rf could be parallel transmit as well - SAR_wbg[block_counter] = _calc_SAR(Qtmf, signal) - SAR_hg[block_counter] = _calc_SAR(Qhmf, signal) - - return SAR_wbg, SAR_hg, t - - -def _SAR_interp(SAR: np.ndarray, t: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """ - Interpolate SAR values for one second resolution. - - Parameters - ---------- - SAR : numpy.ndarray - SAR values - t : numpy.ndarray - Current time points. - - Returns - ------- - SAR_interp : numpy.ndarray - Interpolated values of SAR for a temporal resolution of 1 second. - t_sec : numpy.ndarray - Time points at 1 second resolution. - """ - t_sec = np.arange(1, np.floor(t[-1]) + 1, 1) - f = interpolate.interp1d(t, SAR) - SAR_interp = f(t_sec) - return SAR_interp, t_sec - - -def _SAR_lims_check(SARwbg_lim_s, SARhg_lim_s, tsec) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, - np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """ - Check for SAR violations as compared to IEC 10 second and 6 minute averages; - returns SAR values that are interpolated for the fixed IEC time intervals. - - Parameters - ---------- - SARwbg_lim_s : numpy.ndarray - SARhg_lim_s : numpy.ndarray - tsec : numpy.ndarray - - Returns - ------- - SAR_wbg_tensec : numpy.ndarray - SAR_wbg_sixmin : numpy.ndarray - SAR_hg_tensec : numpy.ndarray - SAR_hg_sixmin : numpy.ndarray - SAR_wbg_sixmin_peak : numpy.ndarray - SAR_hg_sixmin_peak : numpy.ndarray - SAR_wbg_tensec_peak : numpy.ndarray - SAR_hg_tensec_peak : numpy.ndarray - """ - if tsec[-1] > 10: - six_min_threshold_wbg = 4 - ten_sec_threshold_wbg = 8 - - six_min_threshold_hg = 3.2 - ten_sec_threshold_hg = 6.4 - - SAR_wbg_lim_app = np.concatenate((np.zeros(5), SARwbg_lim_s, np.zeros(5)), axis=0) - SAR_hg_lim_app = np.concatenate((np.zeros(5), SARhg_lim_s, np.zeros(5)), axis=0) - - SAR_wbg_tensec = _do_sw_sar(SAR_wbg_lim_app, tsec, 10) # < 2 SARmax - SAR_hg_tensec = _do_sw_sar(SAR_hg_lim_app, tsec, 10) # < 2 SARmax - SAR_wbg_tensec_peak = np.round(np.max(SAR_wbg_tensec), 2) - SAR_hg_tensec_peak = np.round(np.max(SAR_hg_tensec), 2) - - if (np.max(SAR_wbg_tensec) > ten_sec_threshold_wbg) or (np.max(SAR_hg_tensec) > ten_sec_threshold_hg): - print('Pulse exceeding 10 second Global SAR limits, increase TR') - SAR_wbg_sixmin = 'NA' - SAR_hg_sixmin = 'NA' - SAR_wbg_sixmin_peak = 'NA' - SAR_hg_sixmin_peak = 'NA' - - if tsec[-1] > 600: - SAR_wbg_lim_app = np.concatenate((np.zeros(300), SARwbg_lim_s, np.zeros(300)), axis=0) - SAR_hg_lim_app = np.concatenate((np.zeros(300), SARhg_lim_s, np.zeros(300)), axis=0) - - SAR_hg_sixmin = _do_sw_sar(SAR_hg_lim_app, tsec, 600) - SAR_wbg_sixmin = _do_sw_sar(SAR_wbg_lim_app, tsec, 600) - SAR_wbg_sixmin_peak = np.round(np.max(SAR_wbg_sixmin), 2) - SAR_hg_sixmin_peak = np.round(np.max(SAR_hg_sixmin), 2) - - if (np.max(SAR_hg_sixmin) > six_min_threshold_wbg) or (np.max(SAR_hg_sixmin) > six_min_threshold_hg): - print('Pulse exceeding 10 second Global SAR limits, increase TR') - else: - print('Need at least 10 seconds worth of sequence to calculate SAR') - SAR_wbg_tensec = 'NA' - SAR_wbg_sixmin = 'NA' - SAR_hg_tensec = "NA" - SAR_hg_sixmin = "NA" - SAR_wbg_sixmin_peak = 'NA' - SAR_hg_sixmin_peak = 'NA' - SAR_wbg_tensec_peak = 'NA' - SAR_hg_tensec_peak = 'NA' - - return SAR_wbg_tensec, SAR_wbg_sixmin, SAR_hg_tensec, SAR_hg_sixmin, \ - SAR_wbg_sixmin_peak, SAR_hg_sixmin_peak, SAR_wbg_tensec_peak, SAR_hg_tensec_peak - - -def _do_sw_sar(SAR: np.ndarray, tsec: np.ndarray, t: np.ndarray) -> np.ndarray: - """ - Compute a sliding window average of SAR values. - - Parameters - ---------- - SAR : numpy.ndarray - SAR values. - tsec : numpy.ndarray - Corresponding time points at 1 second resolution. - t : numpy.ndarray - Corresponding time points. - - Returns - ------- - SAR_timeavag : numpy.ndarray - Sliding window time average of SAR values. - """ - SAR_time_avg = np.zeros(len(tsec) + int(t)) - for instant in range(int(t / 2), int(t / 2) + (int(tsec[-1]))): # better to go from -sw / 2: sw / 2 - SAR_time_avg[instant] = sum(SAR[range(instant - int(t / 2), instant + int(t / 2) - 1)]) / t - SAR_time_avg = SAR_time_avg[int(t / 2):int(t / 2) + (int(tsec[-1]))] - return SAR_time_avg - - -def calc_SAR(file: Union[str, Path, Sequence]) -> None: - """ - Compute Global SAR values on the `.seq` object for head and whole body over the specified time averages. - - Parameters - ---------- - file : str, Path or Seuqence - `.seq` file for which global SAR values will be computed. Can be path to `.seq` file as `str` or `Path`, or the - `Sequence` object itself. - - Raises - ------ - ValueError - If `file` is a `str` or `Path` to the `.seq` file and this file does not exist on disk. - """ - if isinstance(file, (str, Path)): - if isinstance(file, str): - file = Path(file) - - if file.exists() and file.is_file(): - seq_obj = Sequence() - seq_obj.read(str(file)) - seq_obj = seq_obj - else: - raise ValueError('Seq file does not exist.') - else: - seq_obj = file - - Q_tmf, Q_hmf = _load_Q() - SAR_wbg, SAR_hg, t = _SAR_from_seq(seq_obj, Q_tmf, Q_hmf) - SARwbg_lim, tsec = _SAR_interp(SAR_wbg, t) - SARhg_lim, tsec = _SAR_interp(SAR_hg, t) - SAR_wbg_tensec, SAR_wbg_sixmin, SAR_hg_tensec, SAR_hg_sixmin, SAR_wbg_sixmin_peak, SAR_hg_sixmin_peak, \ - SAR_wbg_tensec_peak, SAR_hg_tensec_peak = _SAR_lims_check(SARwbg_lim, SARhg_lim, tsec) - - # Plot 10 sec average SAR - if (tsec[-1] > 10): - plt.plot(tsec, SAR_wbg_tensec, 'x-', label='Whole Body: 10sec') - plt.plot(tsec, SAR_hg_tensec, '.-', label='Head only: 10sec') - - # plt.plot(t, SARwbg, label='Whole Body - instant') - # plt.plot(t, SARhg, label='Whole Body - instant') - - plt.xlabel('Time (s)') - plt.ylabel('SAR (W/kg)') - plt.title('Global SAR - Mass Normalized - Whole body and head only') - - plt.legend() - plt.grid(True) - plt.show() diff --git a/pypulseq/SAR/__init__.py b/pypulseq/SAR/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pypulseq/Sequence/.write_seq.py.swp b/pypulseq/Sequence/.write_seq.py.swp deleted file mode 100644 index 142da67..0000000 Binary files a/pypulseq/Sequence/.write_seq.py.swp and /dev/null differ diff --git a/pypulseq/Sequence/__init__.py b/pypulseq/Sequence/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pypulseq/Sequence/block.py b/pypulseq/Sequence/block.py deleted file mode 100644 index a34890e..0000000 --- a/pypulseq/Sequence/block.py +++ /dev/null @@ -1,375 +0,0 @@ -from types import SimpleNamespace - -import numpy as np - -from pypulseq.block_to_events import block_to_events -from pypulseq.calc_duration import calc_duration -from pypulseq.compress_shape import compress_shape -from pypulseq.decompress_shape import decompress_shape -from pypulseq.supported_labels import get_supported_labels - - -def add_block(self, block_index: int, *args: SimpleNamespace) -> None: - """ - Inserts PyPulseq block of sequence events into `self.dict_block_events` at position `block_index`. Also performs - gradient checks. - - Parameters - ---------- - block_index : int - Index at which `SimpleNamespace` objects have to be inserted into `self.dict_block_events`. - args : iterable[SimpleNamespace] - Iterable of `SimpleNamespace` objects to be added to `self.dict_block_events`. - - Raises - ------ - ValueError - If trigger event that is passed is of unsupported control event type. - If delay is set for a gradient even that starts with a non-zero amplitude. - RuntimeError - If two consecutive gradients to not have the same amplitude at the connection point. - If the first gradient in the block does not start with 0. - If a gradient that doesn't end at zero is not aligned to the block boundary. - """ - events = block_to_events(args) - block_duration = calc_duration(*events) - self.dict_block_events[block_index] = np.zeros(7, dtype=int) # np.int deprecated - duration = 0 - - check_g = {} # Key-value mapping of index and pairs of gradients/times - extensions = [] - - for event in events: - if event.type == 'rf': - mag = np.abs(event.signal) - amplitude = np.max(mag) - mag = np.divide(mag, amplitude) - # Following line of code is a workaround for numpy's divide functions returning NaN when mathematical - # edge cases are encountered (eg. divide by 0) - mag[np.isnan(mag)] = 0 - phase = np.angle(event.signal) - phase[phase < 0] += 2 * np.pi - phase /= 2 * np.pi - - mag_shape = compress_shape(mag) - data = np.insert(mag_shape.data, 0, mag_shape.num_samples) - mag_id, found = self.shape_library.find(data) - if not found: - self.shape_library.insert(mag_id, data) - - phase_shape = compress_shape(phase) - data = np.insert(phase_shape.data, 0, phase_shape.num_samples) - phase_id, found = self.shape_library.find(data) - if not found: - self.shape_library.insert(phase_id, data) - - use = 0 - use_cases = {'excitation': 1, 'refocusing': 2, 'inversion': 3} - if hasattr(event, 'use'): - use = use_cases[event.use] - - data = [amplitude, mag_id, phase_id, event.delay, event.freq_offset, event.phase_offset, event.dead_time, - event.ringdown_time, use] - data_id, found = self.rf_library.find(data) - if not found: - self.rf_library.insert(data_id, data) - - self.dict_block_events[block_index][1] = data_id - duration = max(duration, len(mag) * self.rf_raster_time + event.delay) - elif event.type == 'grad': - channel_num = ['x', 'y', 'z'].index(event.channel) - idx = 2 + channel_num - - check_g[channel_num] = SimpleNamespace() - check_g[channel_num].idx = idx - check_g[channel_num].start = np.array((event.delay + min(event.t), event.first)) - check_g[channel_num].stop = np.array( - (event.delay + max(event.t) + self.system.grad_raster_time, event.last)) - - amplitude = max(abs(event.waveform)) - if amplitude > 0: - g = event.waveform / amplitude - else: - g = event.waveform - shape = compress_shape(g) - data = np.insert(shape.data, 0, shape.num_samples) - shape_id, found = self.shape_library.find(data) - if not found: - self.shape_library.insert(shape_id, data) - data = [amplitude, shape_id, event.delay, event.first, event.last] - grad_id, found = self.grad_library.find(data) - if not found: - self.grad_library.insert(grad_id, data, 'g') - self.dict_block_events[block_index][idx] = grad_id - duration = max(duration, event.delay + len(g) * self.grad_raster_time) - elif event.type == 'trap': - channel_num = ['x', 'y', 'z'].index(event.channel) - idx = 2 + channel_num - - check_g[channel_num] = SimpleNamespace() - check_g[channel_num].idx = idx - check_g[channel_num].start = np.array((0, 0)) - check_g[channel_num].stop = np.array((event.delay + event.rise_time + event.fall_time + event.flat_time, 0)) - - data = [event.amplitude, event.rise_time, event.flat_time, event.fall_time, event.delay] - trap_id, found = self.grad_library.find(data) - if not found: - self.grad_library.insert(trap_id, data, 't') - self.dict_block_events[block_index][idx] = trap_id - duration = max(duration, event.delay + event.rise_time + event.flat_time + event.fall_time) - elif event.type == 'adc': - data = [event.num_samples, event.dwell, max(event.delay, event.dead_time), event.freq_offset, - event.phase_offset, event.dead_time] - adc_id, found = self.adc_library.find(data) - if not found: - self.adc_library.insert(adc_id, data) - self.dict_block_events[block_index][5] = adc_id - duration = max(duration, event.delay + event.num_samples * event.dwell + event.dead_time) - elif event.type == 'delay': - data = [event.delay] - delay_id, found = self.delay_library.find(data) - if not found: - self.delay_library.insert(delay_id, data) - self.dict_block_events[block_index][0] = delay_id - duration = max(duration, event.delay) - elif event.type == 'output' or event.type == 'trigger': - event_type = ['output', 'trigger'].index(event.type) + 1 - if event_type == 1: - # Trigger codes supported by the Siemens interpreter as of May 2019 - event_channel = ['osc0', 'osc1', 'ext1'].index(event.channel) + 1 - elif event_type == 2: - # Trigger codes supported by the Siemens interpreter as of June 2019 - event_channel = ['physio1', 'physio2'].index(event.channel) + 1 - else: - raise ValueError('Unsupported control event type.') - - data = [event_type, event_channel, event.delay, event.duration] - trigger_id, found = self.trigger_library.find(data) - if not found: - self.trigger_library.insert(trigger_id, data) - - # Now we collect the list of extension objects and we will add it to the event table later - ext = {'type': self.get_extension_type_ID('TRIGGERS'), 'ref': trigger_id} - extensions.append(ext) - duration = max(duration, event.delay + event.duration) - elif event.type == 'labelset': - label_id = get_supported_labels().index(event.label) + 1 - data = [event.value, label_id] - label_id2, found = self.label_set_library.find(data) - if not found: - self.label_set_library.insert(label_id2, data) - - ext = {'type': self.get_extension_type_ID('LABELSET'), 'ref': label_id2} - extensions.append(ext) - elif event.type == 'labelinc': - label_id = get_supported_labels().index(event.label) + 1 - data = [event.value, label_id] - label_id2, found = self.label_inc_library.find(data) - if not found: - self.label_inc_library.insert(label_id2, data) - - ext = {'type': self.get_extension_type_ID('LABELINC'), 'ref': label_id2} - extensions.append(ext) - - # ========= - # ADD EXTENSIONS - # ========= - if len(extensions) > 0: - """ - Add extensions now... but it's tricky actually we need to check whether the exactly the same list of extensions - already exists, otherwise we have to create a new one... ooops, we have a potential problem with the key - mapping then... The trick is that we rely on the sorting of the extension IDs and then we can always find the - last one in the list by setting the reference to the next to 0 and then proceed with the other elements. - """ - sort_idx = np.argsort([e['ref'] for e in extensions]) - extensions = np.take(extensions, sort_idx) - all_found = True - extension_id = 0 - for i in range(len(extensions)): - data = [extensions[i]['type'], extensions[i]['ref'], extension_id] - extension_id, found = self.extensions_library.find(data) - all_found = all_found and found - if not found: - break - - if not all_found: - # Add the list - extension_id = 0 - for i in range(len(extensions)): - data = [extensions[i]['type'], extensions[i]['ref'], extension_id] - extension_id, found = self.extensions_library.find(data) - if not found: - self.extensions_library.insert(extension_id, data) - - # Now we add the ID - self.dict_block_events[block_index][6] = extension_id - - # ========= - # PERFORM GRADIENT CHECKS - # ========= - for grad_to_check in check_g.values(): - - if abs(grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time: - if grad_to_check.start[0] != 0: - raise ValueError('No delay allowed for gradients which start with a non-zero amplitude') - - if block_index > 1: - prev_id = self.dict_block_events[block_index - 1][grad_to_check.idx] - if prev_id != 0: - prev_lib = self.grad_library.get(prev_id) - prev_dat = prev_lib['data'] - prev_type = prev_lib['type'] - if prev_type == 't': - raise RuntimeError( - 'Two consecutive gradients need to have the same amplitude at the connection point') - elif prev_type == 'g': - last = prev_dat[4] - if abs(last - grad_to_check.start[1]) > self.system.max_slew * self.system.grad_raster_time: - raise RuntimeError( - 'Two consecutive gradients need to have the same amplitude at the connection point') - else: - raise RuntimeError('First gradient in the the first block has to start at 0.') - - if grad_to_check.stop[1] > self.system.max_slew * self.system.grad_raster_time and abs( - grad_to_check.stop[0] - block_duration) > 1e-7: - raise RuntimeError("A gradient that doesn't end at zero needs to be aligned to the block boundary.") - - eps = np.finfo(float).eps # np.float deprecated - assert abs(duration - block_duration) < eps - self.arr_block_durations.append(block_duration) - - -def get_block(self, block_index: int) -> SimpleNamespace: - """ - Returns PyPulseq block at `block_index` position in `self.dict_block_events`. - - Parameters - ---------- - block_index : int - Index of PyPulseq block to be retrieved from `self.dict_block_events`. - - Returns - ------- - block : SimpleNamespace - PyPulseq block at 'block_index' position in `self.dict_block_events`. - - Raises - ------ - ValueError - If a trigger event of an unsupported control type is encountered. - If a label object of an unknown extension ID is encountered. - """ - - block = SimpleNamespace() - event_ind = self.dict_block_events[block_index] - - if event_ind[0] > 0: # Delay - delay = SimpleNamespace() - delay.type = 'delay' - delay.delay = self.delay_library.data[event_ind[0]][0] - block.delay = delay - - if event_ind[1] > 0: # RF - block.rf = self.rf_from_lib_data(self.rf_library.data[event_ind[1]]) - - # Gradients - grad_channels = ['gx', 'gy', 'gz'] - for i in range(1, len(grad_channels) + 1): - if event_ind[2 + (i - 1)] > 0: - grad, compressed = SimpleNamespace(), SimpleNamespace() - grad_type = self.grad_library.type[event_ind[2 + (i - 1)]] - lib_data = self.grad_library.data[event_ind[2 + (i - 1)]] - grad.type = 'trap' if grad_type == 't' else 'grad' - grad.channel = grad_channels[i - 1][1] - if grad.type == 'grad': - amplitude = lib_data[0] - shape_id = lib_data[1] - delay = lib_data[2] - shape_data = self.shape_library.data[shape_id] - compressed.num_samples = shape_data[0] - compressed.data = shape_data[1:] - g = decompress_shape(compressed) - grad.waveform = amplitude * g - grad.t = np.arange(g.size) * self.grad_raster_time - grad.delay = delay - if len(lib_data) > 4: - grad.first = lib_data[3] - grad.last = lib_data[4] - else: - grad.first = grad.waveform[0] - grad.last = grad.waveform[-1] - else: - if max(lib_data.shape) < 5: # added by GT - grad.amplitude, grad.rise_time, grad.flat_time, grad.fall_time = [lib_data[x] for x in range(4)] - grad.delay = 0 - else: - grad.amplitude, grad.rise_time, grad.flat_time, grad.fall_time, grad.delay = [lib_data[x] for x in - range(5)] - grad.area = grad.amplitude * (grad.flat_time + grad.rise_time / 2 + grad.fall_time / 2) - grad.flat_area = grad.amplitude * grad.flat_time - setattr(block, grad_channels[i - 1], grad) - # ADC - if event_ind[5] > 0: - lib_data = self.adc_library.data[event_ind[5]] - if len(lib_data) < 6: - lib_data = np.append(lib_data, 0) - - adc = SimpleNamespace() - adc.num_samples, adc.dwell, adc.delay, adc.freq_offset, adc.phase_offset, adc.dead_time = [lib_data[x] for x in - range(6)] - adc.num_samples = int(adc.num_samples) - adc.type = 'adc' - block.adc = adc - - # Triggers - if event_ind[6] > 0: - # We have extensions - triggers, labels, etc. - next_ext_id = event_ind[6] - while next_ext_id != 0: - ext_data = self.extensions_library.data[next_ext_id] - # Format: ext_type, ext_id, next_ext_id - ext_type = self.get_extension_type_string(ext_data[0]) - - if ext_type == 'TRIGGERS': - trigger_types = ['output', 'trigger'] - data = self.trigger_library.data[ext_data[1]] - trigger = SimpleNamespace() - trigger.type = trigger_types[int(data[0])] - if data[0] == 0: - trigger_channels = ['osc0', 'osc1', 'ext1'] - trigger.channel = trigger_channels[int(data[1])] - elif data[0] == 1: - trigger_channels = ['physio1', 'physio2'] - trigger.channel = trigger_channels[int(data[1])] - else: - raise ValueError('Unsupported trigger event type') - - trigger.delay = data[2] - trigger.duration = data[3] - # Allow for multiple triggers per block - if hasattr(block, 'trigger'): - block.trigger[len(block.trigger)] = trigger - else: - block.trigger = {0: trigger} - elif ext_type == 'LABELSET' or ext_type == 'LABELINC': - label = SimpleNamespace() - label.type = ext_type.lower() - supported_labels = get_supported_labels() - if ext_type == 'LABELSET': - data = self.label_set_library.data[ext_data[1]] - else: - data = self.label_inc_library.data[ext_data[1]] - - label.label = supported_labels[data[1] - 1] - label.value = data[0] - # Allow for multiple labels per block - if hasattr(block, 'label'): - block.label[len(block.label)] = label - else: - block.label = {0: label} - else: - raise RuntimeError(f'Unknown extension ID {ext_data[0]}') - - next_ext_id = ext_data[2] - - return block diff --git a/pypulseq/Sequence/parula.py b/pypulseq/Sequence/parula.py deleted file mode 100644 index 6d46232..0000000 --- a/pypulseq/Sequence/parula.py +++ /dev/null @@ -1,84 +0,0 @@ -from matplotlib.colors import LinearSegmentedColormap - - -def main(N: int) -> LinearSegmentedColormap: - """ - Returns a Parula colormap to be used with matplotlib's `cycler`. `cm_data` has values copied from MATLAB for - `parula(64)`. - - Parameters - ---------- - N : int - Number of RGB quantization levels. - - Returns - ------- - LinearSegmentedColormap - Parula color map. - """ - cm_data = [[0.2422, 0.1504, 0.6603], - [0.25039048, 0.16499524, 0.70761429], - [0.25777143, 0.18178095, 0.7511381], - [0.26472857, 0.19775714, 0.79521429], - [0.27064762, 0.21467619, 0.83637143], - [0.27511429, 0.2342381, 0.87098571], - [0.2783, 0.25587143, 0.89907143], - [0.28033333, 0.27823333, 0.9221], - [0.2813381, 0.30059524, 0.94137619], - [0.28101429, 0.32275714, 0.95788571], - [0.27946667, 0.34467143, 0.97167619], - [0.27597143, 0.36668095, 0.98290476], - [0.26991429, 0.3892, 0.9906], - [0.26024286, 0.41232857, 0.99515714], - [0.24403333, 0.43583333, 0.99883333], - [0.22064286, 0.46025714, 0.99728571], - [0.19633333, 0.48471905, 0.98915238], - [0.18340476, 0.50737143, 0.97979524], - [0.17864286, 0.52885714, 0.96815714], - [0.1764381, 0.54990476, 0.95201905], - [0.16874286, 0.5702619, 0.93587143], - [0.154, 0.5902, 0.9218], - [0.14602857, 0.60911905, 0.90785714], - [0.13802381, 0.62762857, 0.89729048], - [0.12481429, 0.64592857, 0.88834286], - [0.11125238, 0.6635, 0.87631429], - [0.09520952, 0.67982857, 0.85978095], - [0.06887143, 0.69477143, 0.83935714], - [0.02966667, 0.70816667, 0.81633333], - [0.00357143, 0.72026667, 0.7917], - [0.00665714, 0.73121429, 0.76601429], - [0.04332857, 0.74109524, 0.73940952], - [0.09639524, 0.75, 0.7120381], - [0.14077143, 0.7584, 0.68415714], - [0.1717, 0.7669619, 0.65544286], - [0.19376667, 0.77576667, 0.6251], - [0.21608571, 0.7843, 0.5923], - [0.24695714, 0.79179524, 0.55674286], - [0.29061429, 0.79729048, 0.51882857], - [0.34064286, 0.8008, 0.47885714], - [0.3909, 0.80287143, 0.43544762], - [0.44562857, 0.80241905, 0.39091905], - [0.5044, 0.7993, 0.348], - [0.5615619, 0.79423333, 0.30448095], - [0.61739524, 0.78761905, 0.2612381], - [0.67198571, 0.77927143, 0.2227], - [0.7242, 0.76984286, 0.19102857], - [0.77383333, 0.75980476, 0.16460952], - [0.82031429, 0.74981429, 0.15352857], - [0.86343333, 0.7406, 0.15963333], - [0.90354286, 0.73302857, 0.17741429], - [0.93925714, 0.72878571, 0.20995714], - [0.97275714, 0.72977143, 0.23944286], - [0.99564762, 0.74337143, 0.23714762], - [0.99698571, 0.76585714, 0.21994286], - [0.99520476, 0.78925238, 0.2027619], - [0.9892, 0.81356667, 0.18853333], - [0.97862857, 0.83862857, 0.17655714], - [0.96764762, 0.8639, 0.16429048], - [0.96100952, 0.88901905, 0.15367619], - [0.95967143, 0.91345714, 0.14225714], - [0.96279524, 0.9373381, 0.12650952], - [0.96911429, 0.96062857, 0.1063619], - [0.9769, 0.9839, 0.0805]] - - return LinearSegmentedColormap.from_list(name='parula', colors=cm_data, N=N) diff --git a/pypulseq/Sequence/read_seq.py b/pypulseq/Sequence/read_seq.py deleted file mode 100644 index 06d93ae..0000000 --- a/pypulseq/Sequence/read_seq.py +++ /dev/null @@ -1,398 +0,0 @@ -import re -from pathlib import Path -from typing import Dict, Tuple - -import numpy as np - -from pypulseq.calc_duration import calc_duration -from pypulseq.event_lib import EventLibrary -from pypulseq.supported_labels import get_supported_labels - - -def read(self, path: str, detect_rf_use: bool = False) -> None: - """ - Reads a `.seq` file from `path`. - - Parameters - ---------- - path : Path - Path of .seq file to be read. - detect_rf_use : bool, default=False - - Raises - ------ - ValueError - - RuntimeError - """ - - input_file = open(path, 'r') - self.shape_library = EventLibrary() - self.adc_library = EventLibrary() - self.delay_library = EventLibrary() - self.grad_library = EventLibrary() - self.grad_raster_time = self.system.grad_raster_time - self.rf_library = EventLibrary() - self.rf_raster_time = self.system.rf_raster_time - self.label_inc_library = EventLibrary() - self.label_set_library = EventLibrary() - self.trigger_library = EventLibrary() - - self.dict_block_events = {} - self.dict_definitions = {} - - jemris_generated = False - - while True: - section = __skip_comments(input_file) - if section == -1: - break - if section == '[DEFINITIONS]': - self.dict_definitions = __read_definitions(input_file) - elif section == '[JEMRIS]': - jemris_generated = True - elif section == '[VERSION]': - version_major, version_minor, version_revision = __read_version(input_file) - - if version_major != self.version_major: - raise RuntimeError(f'Unsupported version_major: {version_major}. Expected: {self.version_major}') - - if version_major == 1 and version_minor == 2 and self.version_major == 1 and self.version_minor == 3: - compatibility_mode_12x_13x = True - else: - compatibility_mode_12x_13x = False - - if version_minor != self.version_minor: - raise RuntimeError(f'Unsupported version_minor: {version_minor}. Expected: {self.version_minor}') - - if version_revision > self.version_revision: - raise RuntimeError( - f'Unsupported version_revision: {version_revision}. Expected: {self.version_revision}') - - if not compatibility_mode_12x_13x: - self.version_major = version_major - self.version_minor = version_minor - self.version_revision = version_revision - - elif section == '[BLOCKS]': - self.dict_block_events = __read_blocks(input_file, compatibility_mode_12x_13x) - elif section == '[RF]': - if jemris_generated: - self.rf_library = __read_events(input_file, (1, 1, 1, 1, 1), event_library=self.rf_library) - else: - self.rf_library = __read_events(input_file, (1, 1, 1, 1e-6, 1, 1), event_library=self.rf_library) - elif section == '[GRADIENTS]': - self.grad_library = __read_events(input_file, (1, 1, 1e-6), 'g', self.grad_library) - elif section == '[TRAP]': - if jemris_generated: - self.grad_library = __read_events(input_file, (1, 1e-6, 1e-6, 1e-6), 't', self.grad_library) - else: - self.grad_library = __read_events(input_file, (1, 1e-6, 1e-6, 1e-6, 1e-6), 't', self.grad_library) - elif section == '[ADC]': - self.adc_library = __read_events(input_file, (1, 1e-9, 1e-6, 1, 1), event_library=self.adc_library) - elif section == '[DELAYS]': - self.delay_library = __read_events(input_file, (1e-6,), event_library=self.delay_library) - elif section == '[SHAPES]': - self.shape_library = __read_shapes(input_file) - elif section == '[EXTENSIONS]': - self.extensions_library = __read_events(input_file) - elif section[:18] == 'extension TRIGGERS': - extension_id = int(section[18:]) - self.set_extension_string_ID('TRIGGERS', extension_id) - self.trigger_library = __read_events(input_file, (1, 1, 1e-6, 1e-6), event_library=self.trigger_library) - elif section[:18] == 'extension LABELSET': - extension_id = int(section[18:]) - self.set_extension_string_ID('LABELSET', extension_id) - l1 = lambda s: int(s) - l2 = lambda s: get_supported_labels().index(s) + 1 - self.label_set_library = __read_and_parse_events(input_file, l1, l2) - elif section[:18] == 'extension LABELINC': - extension_id = int(section[18:]) - self.set_extension_string_ID('LABELINC', extension_id) - l1 = lambda s: int(s) - l2 = lambda s: get_supported_labels().index(s) + 1 - self.label_inc_library = __read_and_parse_events(input_file, l1, l2) - else: - raise ValueError(f'Unknown section code: {section}') - - self.arr_block_durations = np.zeros(len(self.dict_block_events)) - grad_channels = ['gx', 'gy', 'gz'] - grad_prev_last = np.zeros(len(grad_channels)) - for block_counter in range(len(self.dict_block_events)): - block = self.get_block(block_counter + 1) - block_duration = calc_duration(block) - self.arr_block_durations[block_counter] = block_duration - # We also need to keep track of the event IDs because some PyPulseq files written by external software may contain - # repeated entries so searching by content will fail - event_idx = self.dict_block_events[block_counter + 1] - # Update the objects by filling in the fields not contained in the PyPulseq file - for j in range(len(grad_channels)): - if hasattr(block, grad_channels[j]): - grad = getattr(block, grad_channels[j]) - else: - grad_prev_last[j] = 0 - continue - - if grad.type == 'grad': - if grad.delay > 0: - grad_prev_last[j] = 0 - - if hasattr(grad, 'first'): - continue - - grad.first = grad_prev_last[j] - # Restore samples on the edges of the gradient raster intervals for that we need the first sample - odd_step1 = [grad.first, *2 * grad.waveform] - odd_step2 = odd_step1 * (np.mod(range(len(odd_step1)), 2) * 2 - 1) - waveform_odd_rest = np.cumsum(odd_step2) * (np.mod(len(odd_step2), 2) * 2 - 1) - grad.lsat = waveform_odd_rest[-1] - grad_prev_last[j] = grad.last - - eps = np.finfo(np.float).eps - if grad.delay + len(grad.waveform) * self.grad_raster_time + eps < block_duration: - grad_prev_last[j] = 0 - - amplitude = np.max(np.abs(grad.waveform)) - old_data = [amplitude, grad.shape_id, grad.delay] - new_data = [amplitude, grad.shape_id, grad.delay, grad.first, grad.last] - event_id = event_idx[j + 2] - # update_data() - else: - grad_prev_last[j] = 0 - - if detect_rf_use: - for k in self.rf_library.keys(): - lib_data = self.rf_library.data[k] - rf = self.rf_from_lib_data(lib_data) - flip_deg = np.abs(np.sum(rf.signal)) * rf.t[0] * 360 - if len(lib_data) < 9: - if flip_deg < 90.01: - lib_data[8] = 0 - else: - lib_data[8] = 2 - self.rf_library.data[k] = lib_data - - -def __read_definitions(input_file) -> Dict[str, str]: - """ - Read dict_definitions from .seq file. - - Parameters - ---------- - input_file : file object - .seq file - - Returns - ------- - dict_definitions : dict - Dict object containing key value pairs of dict_definitions. - """ - definitions = dict() - line = __strip_line(input_file) - while line != '' and line[0] != '#': - tok = line.split(' ') - try: # Try converting every element into a float - [float(x) for x in tok[1:]] - definitions[tok[0]] = np.array(tok[1:], dtype=float) - except ValueError: # Try clause did not work! - definitions[tok[0]] = tok[1:] - line = __strip_line(input_file) - - return definitions - - -def __read_version(input_file) -> Tuple[int, int, int]: - """ - Read version from .seq file. - - Parameters - ---------- - input_file : file object - .seq file - - Returns - ------- - tuple - Tuple of major, minor and revision number. - """ - line = __strip_line(input_file) - major, minor, revision = 0, 0, 0 - while line != '' and line[0] != '#': - tok = line.split(' ') - if tok[0] == 'major': - major = int(tok[1]) - elif tok[0] == 'minor': - minor = int(tok[1]) - elif tok[0] == 'revision': - revision = tok[1] - else: - raise RuntimeError(f'Incompatible version. Expected: {major}{minor}{revision}') - line = __strip_line(input_file) - - return major, minor, revision - - -def __read_blocks(input_file, compatibility_mode_12x_13x: bool) -> dict: - """ - Read Pulseq blocks from .seq file. - - Parameters - ---------- - input_file : file - .seq file - compatibility_mode_12x_13x : bool - - Returns - ------- - event_table : dict - Dict object containing key value pairs of Pulseq block ID and block definition. - """ - - line = __strip_line(input_file) - - event_table = dict() - while line != '' and line != '#': - block_events = np.fromstring(line, dtype=int, sep=' ') - - if compatibility_mode_12x_13x: - event_table[block_events[0]] = np.array([*block_events[1:], 0]) - else: - event_table[block_events[0]] = block_events[1:] - - line = __strip_line(input_file) - - return event_table - - -def __read_events(input_file, scale: list = (1,), event_type: str = str(), - event_library: EventLibrary = EventLibrary()) -> EventLibrary: - """ - Read Pulseq events from .seq file. - - Parameters - ---------- - input_file : file object - .seq file - scale : list, default=(1,) - Scaling factor. - event_type : str - Type of Pulseq event. - event_library : EventLibrary, default=EventLibrary() - EventLibrary - - Returns - ------- - dict_definitions : dict - `EventLibrary` object containing Pulseq event dict_definitions. - """ - line = __strip_line(input_file) - - while line != '' and line != '#': - data = np.fromstring(line, dtype=float, sep=' ') - event_id = data[0] - data = data[1:] * scale - if event_type == '': - event_library.insert(key_id=event_id, new_data=data) - else: - event_library.insert(key_id=event_id, new_data=data, data_type=event_type) - line = __strip_line(input_file) - - return event_library - - -def __read_and_parse_events(input_file, *args) -> EventLibrary: - event_library = EventLibrary() - line = __strip_line(input_file) - - while line != '' and line != '#': - datas = re.split('(\s+)', line) - datas = [d for d in datas if d != ' '] - data = np.zeros(len(datas) - 1, dtype=np.int) - event_id = int(datas[0]) - for i in range(1, len(datas)): - if i > len(args): - data[i - 1] = int(datas[i]) - else: - data[i - 1] = args[i - 1](datas[i]) - event_library.insert(key_id=event_id, new_data=data) - line = __strip_line(input_file) - - return event_library - - -def __read_shapes(input_file) -> EventLibrary: - """ - Read Pulseq shapes from .seq file. - - Parameters - ---------- - input_file : file - .seq file - - Returns - ------- - shape_library : EventLibrary - `EventLibrary` object containing shape dict_definitions. - """ - shape_library = EventLibrary() - - line = __skip_comments(input_file) - - while line != -1 and (line != '' or line[0:8] == 'shape_id'): - tok = line.split(' ') - id = int(tok[1]) - line = __skip_comments(input_file) - tok = line.split(' ') - num_samples = int(tok[1]) - data = [] - line = __skip_comments(input_file) - while line != '' and line != '#': - data.append(float(line)) - line = __strip_line(input_file) - line = __skip_comments(input_file) - data.insert(0, num_samples) - data = np.asarray(data) - shape_library.insert(key_id=id, new_data=data) - return shape_library - - -def __skip_comments(input_file) -> str: - """ - Skip one '#' comment in .seq file. - - Parameters - ---------- - input_file : file - .seq file - - Returns - ------- - line : str - First line in `input_file` after skipping one '#' comment block. Note: File pointer is remembered, so successive calls work as expected. - """ - - line = __strip_line(input_file) - - while line != -1 and (line == '' or line[0] == '#'): - line = __strip_line(input_file) - - return line - - -def __strip_line(input_file) -> str: - """ - Removes spaces and newline whitespaces. - - Parameters - ---------- - input_file : file - .seq file - - Returns - ------- - line : str - First line in input_file after removing spaces and newline whitespaces. Note: File pointer is remembered, - so successive calls work as expected. Returns -1 for eof. - """ - line = input_file.readline() # If line is an empty string, end of the file has been reached - return line.strip() if line != '' else -1 diff --git a/pypulseq/Sequence/sequence.py b/pypulseq/Sequence/sequence.py deleted file mode 100644 index 764035b..0000000 --- a/pypulseq/Sequence/sequence.py +++ /dev/null @@ -1,902 +0,0 @@ -import math -from collections import OrderedDict -from types import SimpleNamespace -from typing import Tuple -from typing import Union -from warnings import warn - -import matplotlib as mpl -import numpy as np -from matplotlib import pyplot as plt -from scipy.signal import argrelextrema - -from pypulseq import major, minor, revision -from pypulseq.Sequence import block -from pypulseq.Sequence import parula -from pypulseq.Sequence.read_seq import read -from pypulseq.Sequence.test_report import test_report as ext_test_report -from pypulseq.Sequence.write_seq import write -from pypulseq.calc_rf_center import calc_rf_center -from pypulseq.check_timing import check_timing as ext_check_timing -from pypulseq.decompress_shape import decompress_shape -from pypulseq.event_lib import EventLibrary -from pypulseq.opts import Opts -from pypulseq.points_to_waveform import points_to_waveform -from pypulseq.supported_labels import get_supported_labels - - -class Sequence: - version_major: int = major - version_minor: int = minor - version_revision = revision - - def __init__(self, system=Opts()): - # ========= - # EVENT LIBRARIES - # ========= - self.adc_library = EventLibrary() # Library of ADC events - self.delay_library = EventLibrary() # Library of delay events - # Library of extension events. Extension events form single-linked zero-terminated lists - self.extensions_library = EventLibrary() - self.grad_library = EventLibrary() # Library of gradient events - self.label_inc_library = EventLibrary() # Library of Label(inc) events (reference from the extensions library) - self.label_set_library = EventLibrary() # Library of Label(set) events (reference from the extensions library) - self.rf_library = EventLibrary() # Library of RF events - self.shape_library = EventLibrary() # Library of compressed shapes - self.trigger_library = EventLibrary() # Library of trigger events - - # ========= - # OTHER - # ========= - self.system = system - - self.rf_raster_time = self.system.rf_raster_time # RF raster time (system dependent) - self.grad_raster_time = self.system.grad_raster_time # Gradient raster time (system dependent) - - self.dict_block_events = OrderedDict() # Event table - self.dict_definitions = OrderedDict() # Optional sequence dict_definitions - - self.arr_block_durations = [] # Cache of block durations - self.arr_extension_numeric_idx = [] # numeric IDs of the used extensions - self.arr_extension_string_idx = [] # string IDs of the used extensions - - def __str__(self): - s = "Sequence:" - s += "\nshape_library: " + str(self.shape_library) - s += "\nrf_library: " + str(self.rf_library) - s += "\ngrad_library: " + str(self.grad_library) - s += "\nadc_library: " + str(self.adc_library) - s += "\ndelay_library: " + str(self.delay_library) - s += "\nextensions_library: " + str(self.extensions_library) # inserted for trigger support by mveldmann - s += "\nrf_raster_time: " + str(self.rf_raster_time) - s += "\ngrad_raster_time: " + str(self.grad_raster_time) - s += "\ndict_block_events: " + str(len(self.dict_block_events)) - return s - - def add_block(self, *args: SimpleNamespace) -> None: - """ - Adds event(s) as a block to `Sequence`. - - Parameters - ---------- - args - Event or list of events to be added as a block to `Sequence`. - """ - block.add_block(self, len(self.dict_block_events) + 1, *args) - - def calculate_kspace(self, trajectory_delay: int = 0, spoil_val: float = []) -> Tuple[np.array, np.array, np.array, np.array, np.array]: - """ - Calculates the k-space trajectory of the entire pulse sequence. - - Parameters - ---------- - trajectory_delay : int, default=0 - Compensation factor in millis to align ADC and gradients in the reconstruction. - - Returns - ------- - k_traj_adc : numpy.array - K-space trajectory sampled at `t_adc` timepoints. - k_traj : numpy.array - K-space trajectory of the entire pulse sequence. - t_excitation : numpy.array - Excitation timepoints. - t_refocusing : numpy.array - Refocusing timepoints. - t_adc : numpy.array - Sampling timepoints. - """ - # Initialise the counters and accumulator objects - count_excitation = 0 - count_refocusing = 0 - count_adc_samples = 0 - - # Loop through the blocks to prepare preallocations - for block_counter in range(len(self.dict_block_events)): - block = self.get_block(block_counter + 1) - if hasattr(block, 'rf'): - if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': - count_excitation += 1 - else: - count_refocusing += 1 - - if hasattr(block, 'adc'): - count_adc_samples += int(block.adc.num_samples) - - t_excitation = np.zeros(count_excitation) - t_refocusing = np.zeros(count_refocusing) - k_time = np.zeros(count_adc_samples) - current_duration = 0 - count_excitation = 0 - count_refocusing = 0 - kc_outer = 0 - traj_recon_delay = trajectory_delay - - # Go through the blocks and collect RF and ADC timing data - for block_counter in range(len(self.dict_block_events)): - block = self.get_block(block_counter + 1) - - if hasattr(block, 'rf'): - rf = block.rf - rf_center, _ = calc_rf_center(rf) - t = rf.delay + rf_center - if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': - t_excitation[count_excitation] = current_duration + t - count_excitation += 1 - else: - t_refocusing[count_refocusing] = current_duration + t - count_refocusing += 1 - - if hasattr(block, 'adc'): - _k_time = np.arange(block.adc.num_samples) + 0.5 - _k_time = _k_time * block.adc.dwell + block.adc.delay + current_duration + traj_recon_delay - k_time[kc_outer:kc_outer + block.adc.num_samples] = _k_time - kc_outer += block.adc.num_samples - current_duration += self.arr_block_durations[block_counter] - - # Now calculate the actual k-space trajectory based on the gradient waveforms - gw = self.gradient_waveforms() - i_excitation = np.round(t_excitation / self.grad_raster_time) - i_refocusing = np.round(t_refocusing / self.grad_raster_time) - i_periods = np.sort([1, *(i_excitation + 1), *(i_refocusing + 1), gw.shape[1] + 1]).astype(int) - # i_periods -= 1 # Python is 0-indexed - ii_next_excitation = min(len(i_excitation), 1) - ii_next_refocusing = min(len(i_refocusing), 1) - k_traj = np.zeros_like(gw) - k = np.zeros((3, 1)) - - for i in range(len(i_periods) - 1): - i_period_end = i_periods[i + 1] - 1 - gw_period = gw[:, i_periods[i] - 1:i_period_end] * self.grad_raster_time - k_period = np.concatenate((k, gw_period), axis=1) - k_period = np.cumsum(k_period, axis=1) - # account for spoiling: when traj is out of bound, set it back to 0 - if spoil_val != []: - for j in range(3): - while max(abs(k_period[j,:]))>=spoil_val*0.9: - ind_spoil = np.where(abs(k_period[j,:]) >= spoil_val*0.9)[0][0] - spoil_start_1 = np.where(gw_period[j,:ind_spoil] == 0)[0] - spoil_start_2 = argrelextrema(gw_period[j,:ind_spoil], np.less)[0] - spoil_end = np.where(gw_period[j,ind_spoil+1:] == 0)[0] - - spoil_start_1 = spoil_start_1[-1] + 1 if spoil_start_1.size > 0 else 0 - spoil_start_2 = spoil_start_2[-1] + 1 if spoil_start_2.size > 0 else 0 - spoil_start = max(spoil_start_1,spoil_start_2) - spoil_end = ind_spoil + 1 + spoil_end[0] if spoil_end.size > 0 else -1 - - k_period_temp = np.concatenate((k[j,:], gw_period[j,:])) - k_period[j,:spoil_start] = np.cumsum(k_period_temp[:spoil_start]) - k_period[j,spoil_start:spoil_end] = 0 - k_period[j,spoil_end:] = np.cumsum(k_period_temp[spoil_end:]) - - k_traj[:, i_periods[i] - 1:i_period_end] = k_period[:, 1:] - k = k_period[:, -1] - - if ii_next_excitation > 0 and i_excitation[ii_next_excitation - 1] == i_period_end: - k[:] = 0 - k_traj[:, i_period_end - 1] = np.nan - ii_next_excitation = min(len(i_excitation), ii_next_excitation + 1) - - if ii_next_refocusing > 0 and i_refocusing[ii_next_refocusing - 1] == i_period_end: - k = -k - ii_next_refocusing = min(len(i_refocusing), ii_next_refocusing + 1) - - k = k.reshape((-1, 1)) # To be compatible with np.concatenate - - k_traj_adc = [] - for _k_traj_row in k_traj: - result = np.interp(xp=np.array(range(1, k_traj.shape[1] + 1)) * self.grad_raster_time, - fp=_k_traj_row, - x=k_time) - k_traj_adc.append(result) - k_traj_adc = np.stack(k_traj_adc) - t_adc = k_time - - return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc - - # def calculate_kspace_PP(self, trajectory_delay: float = 0): - # gw_pp, t_excitation, t_refocusing, t_adc = self.waveforms_and_times() - # t_adc += trajectory_delay - # - # ng = len(gw_pp) - # # Integrate waveforms as PP - # gm_pp = np.empty(ng) - # tc = [] - # for i in range(ng): - # if gw_pp[i] is None: - # continue - # gm_pp[i] = 0 # TODO - - def check_timing(self) -> Tuple[bool, list]: - """ - Check timing of the events in each block based on grad raster time system limit. - - Returns - ------- - is_ok : bool - Boolean flag indicating timing errors. - error_report : str - Error report in case of timing errors. - """ - error_report = [] - is_ok = True - num_blocks = len(self.dict_block_events) - total_duration = 0 - - for block_counter in range(num_blocks): - block = self.get_block(block_counter + 1) - event_names = ['rf', 'gx', 'gy', 'gz', 'adc', 'delay'] - ind = [hasattr(block, attr) for attr in event_names] - events = [getattr(block, event_names[i]) for i in range(len(event_names)) if ind[i] == 1] - res, rep, duration = ext_check_timing(self.system, *events) - is_ok = is_ok and res - if len(rep) != 0: - error_report.append(f'Event: {block_counter} - {rep}\n') - total_duration += duration - - # Check if all the gradients in the last block are ramped down properly - if len(events) != 0: - for e in range(len(events)): - if not isinstance(events[e], list) and events[e].type == 'grad': - if events[e].last != 0: - error_report.append( - f'Event {block_counter} gradients do not ramp to 0 at the end of the sequence') - - self.set_definition('Total duration', total_duration) - - return is_ok, error_report - - def duration(self) -> Tuple[int, int, np.ndarray]: - """ - Returns the total duration of this sequence, and the total count of blocks and events. - - Returns - ------- - duration : int - Duration of this sequence in millis. - num_blocks : int - Number of blocks in this sequence. - event_count : numpy.ndarray - Number of events in this sequence. - """ - - num_blocks = len(self.dict_block_events) - event_count = np.zeros(len(self.dict_block_events[1])) - duration = 0 - for block_counter in range(num_blocks): - event_count += self.dict_block_events[block_counter + 1] > 0 - duration += self.arr_block_durations[block_counter] - - return duration, num_blocks, event_count - - def flip_grad_axis(self, axis: str) -> None: - """ - Convenience function to invert all gradients along specified axis. - - Parameters - ---------- - axis : str - Gradients to invert or scale. Must be one of 'x', 'y' or 'z'. - """ - self.mod_grad_axis(axis, modifier=-1) - - def get_block(self, block_index: int) -> SimpleNamespace: - """ - Retrieves block of events identified by `block_index` from `Sequence`. - - Parameters - ---------- - block_index : int - Index of block to be retrieved from `Sequence`. - - Returns - ------- - SimpleNamespace - Event identified by `block_index`. - """ - return block.get_block(self, block_index) - - def get_definition(self, key: str) -> str: - """ - Retrieves definition identified by `key` from `Sequence`. Returns `None` if no matching definition is found. - - Parameters - ---------- - key : str - Key of definition to retrieve. - - Returns - ------- - str - Definition identified by `key` if found, else returns ''. - """ - if key in self.dict_definitions: - return self.dict_definitions[key] - else: - return '' - - def get_extension_type_ID(self, extension_string: str) -> int: - """ - Get numeric extension ID for `extension_string`. Will automatically create a new ID if unknown. - - Parameters - ---------- - extension_string : str - Given string extension ID. - - Returns - ------- - extension_id : int - Numeric ID for given string extension ID. - - """ - if extension_string not in self.arr_extension_string_idx: - if len(self.arr_extension_numeric_idx) == 0: - extension_id = 1 - else: - extension_id = 1 + max(self.arr_extension_numeric_idx) - - self.arr_extension_numeric_idx.append(extension_id) - self.arr_extension_string_idx.append(extension_string) - assert len(self.arr_extension_numeric_idx) == len(self.arr_extension_string_idx) - else: - num = self.arr_extension_string_idx.index(extension_string) - extension_id = self.arr_extension_numeric_idx[num] - - return extension_id - - def get_extension_type_string(self, extension_id: int) -> str: - """ - Get string extension ID for `extension_id`. - - Parameters - ---------- - extension_id : int - Given numeric extension ID. - - Returns - ------- - extension_str : str - String ID for the given numeric extension ID. - - Raises - ------ - ValueError - If given numeric extension ID is unknown. - """ - if extension_id in self.arr_extension_numeric_idx: - num = self.arr_extension_numeric_idx.index(extension_id) - else: - raise ValueError(f'Extension for the given ID - {extension_id} - is unknown.') - - extension_str = self.arr_extension_string_idx[num] - return extension_str - - def gradient_waveforms(self) -> np.ndarray: - """ - Decompress the entire gradient waveform. Returns an array of shape `gradient_axes x timepoints`. - `gradient_axes` is typically 3. - - Returns - ------- - grad_waveforms : numpy.ndarray - Decompressed gradient waveform. - """ - duration, num_blocks, _ = self.duration() - - wave_length = math.ceil(duration / self.grad_raster_time) - grad_channels = 3 - grad_waveforms = np.zeros((grad_channels, wave_length)) - grad_channels = ['gx', 'gy', 'gz'] - - t0 = 0 - t0_n = 0 - for block_counter in range(num_blocks): - block = self.get_block(block_counter + 1) - for j in range(len(grad_channels)): - if hasattr(block, grad_channels[j]): - grad = getattr(block, grad_channels[j]) - if grad.type == 'grad': - nt_start = round((grad.delay + grad.t[0]) / self.grad_raster_time) - waveform = grad.waveform - else: - nt_start = round(grad.delay / self.grad_raster_time) - if abs(grad.flat_time) > np.finfo(float).eps: - t = np.cumsum([0, grad.rise_time, grad.flat_time, grad.fall_time]) - trap_form = np.multiply([0, 1, 1, 0], grad.amplitude) - else: - t = np.cumsum([0, grad.rise_time, grad.fall_time]) - trap_form = np.multiply([0, 1, 0], grad.amplitude) - - tn = math.floor(t[-1] / self.grad_raster_time) - t = np.append(t, t[-1] + self.grad_raster_time) - trap_form = np.append(trap_form, 0) - - if abs(grad.amplitude) > np.finfo(float).eps: - waveform = points_to_waveform(times=t, amplitudes=trap_form, - grad_raster_time=self.grad_raster_time) - else: - waveform = np.zeros(tn + 1) - - if len(waveform) != np.sum(np.isfinite(waveform)): - warn('Not all elements of the generated waveform are finite') - - """ - Matlab dynamically resizes arrays during slice assignment operation if assignment is out of bounds - Numpy does not; following is a workaround - """ - l1, l2 = int(t0_n + nt_start), int(t0_n + nt_start + len(waveform)) - if l2 > grad_waveforms.shape[1]: - z = np.zeros((grad_waveforms.shape[0], l2 - grad_waveforms.shape[1])) - grad_waveforms = np.hstack((grad_waveforms, z)) - grad_waveforms[j, l1:l2] = waveform - - t0 += self.arr_block_durations[block_counter] - t0_n = round(t0 / self.grad_raster_time) - - return grad_waveforms - - def mod_grad_axis(self, axis: str, modifier: int) -> None: - """ - Invert or scale all gradients along the corresponding axis/channel. The function acts on all gradient objects - already added to the sequence object. - - Parameters - ---------- - axis : str - Gradients to invert or scale. Must be one of 'x', 'y' or 'z'. - modifier : int - Scaling value. - - Raises - ------ - ValueError - If invalid `axis` is passed. Must be one of 'x', 'y','z'. - RuntimeError - If same gradient event is used on multiple axes. - """ - if axis not in ['x', 'y', 'z']: - raise ValueError(f"Invalid axis. Must be one of 'x', 'y','z'. Passed: {axis}") - - channel_num = ['x', 'y', 'z'].index(axis) - other_channels = [0, 1, 2] - other_channels.remove(channel_num) - - # Go through all event table entries and list gradient objects in the library - all_grad_events = np.array(list(self.dict_block_events.values())) - all_grad_events = all_grad_events[:, 2:5] - - selected_events = np.unique(all_grad_events[:, channel_num]) - selected_events = selected_events[selected_events != 0] - other_events = np.unique(all_grad_events[:, other_channels]) - if len(np.intersect1d(selected_events, other_events)) > 0: - raise RuntimeError('mod_grad_axis does not yet support the same gradient event used on multiple axes.') - - for i in range(len(selected_events)): - self.grad_library.data[selected_events[i]][0] *= modifier - if self.grad_library.type[selected_events[i]] == 'g' and self.grad_library.lengths[selected_events[i]] == 5: - # Need to update first and last fields - self.grad_library.data[selected_events[i]][3] *= modifier - self.grad_library.data[selected_events[i]][4] *= modifier - - def plot(self, label: str = str(), save: bool = False, time_range=(0, np.inf), time_disp: str = 's', - plot_type: str = 'Gradient') -> None: - """ - Plot `Sequence`. - - Parameters - ---------- - label : str, defualt=str() - - save : bool, default=False - Boolean flag indicating if plots should be saved. The two figures will be saved as JPG with numerical - suffixes to the filename 'seq_plot'. - time_range : iterable, default=(0, np.inf) - Time range (x-axis limits) for plotting the sequence. Default is 0 to infinity (entire sequence). - time_disp : str, default='s' - Time display type, must be one of `s`, `ms` or `us`. - plot_type : str, default='Gradient' - Gradients display type, must be one of either 'Gradient' or 'Kspace'. - """ - mpl.rcParams['lines.linewidth'] = 0.75 # Set default Matplotlib linewidth - - valid_plot_types = ['Gradient', 'Kspace'] - valid_time_units = ['s', 'ms', 'us'] - valid_labels = get_supported_labels() - if plot_type not in valid_plot_types: - raise ValueError('Unsupported plot type') - if not all([isinstance(x, (int, float)) for x in time_range]) or len(time_range) != 2: - raise ValueError('Invalid time range') - if time_disp not in valid_time_units: - raise ValueError('Unsupported time unit') - - fig1, fig2 = plt.figure(1), plt.figure(2) - sp11 = fig1.add_subplot(311) - sp12, sp13 = fig1.add_subplot(312, sharex=sp11), fig1.add_subplot(313, sharex=sp11) - fig2_subplots = [fig2.add_subplot(311, sharex=sp11), fig2.add_subplot(312, sharex=sp11), - fig2.add_subplot(313, sharex=sp11)] - - t_factor_list = [1, 1e3, 1e6] - t_factor = t_factor_list[valid_time_units.index(time_disp)] - - t0 = 0 - label_defined = False - label_idx_to_plot = [] - label_legend_to_plot = [] - label_store = dict() - for i in range(len(valid_labels)): - label_store[valid_labels[i]] = 0 - if label.upper() == valid_labels[i]: - label_idx_to_plot.append(i) - label_legend_to_plot.append(valid_labels[i]) - - if len(label_idx_to_plot) != 0: - p = parula.main(len(label_idx_to_plot) + 1) - label_colors_to_plot = p(np.arange(len(label_idx_to_plot))) - - for block_counter in range(len(self.dict_block_events)): - block = self.get_block(block_counter + 1) - is_valid = time_range[0] <= t0 <= time_range[1] - if is_valid: - if hasattr(block, 'label'): - for i in range(len(block.label)): - if block.label[i].type == 'labelinc': - label_store[block.label[i].label] += block.label[i].value - else: - label_store[block.label[i].label] = block.label[i].value - label_defined = True - - if hasattr(block, 'adc'): - adc = block.adc - # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this - # is the present convention - the samples are shifted by 0.5 dwell - t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell - sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), 'rx') - sp13.plot(t_factor * (t0 + t), - np.angle(np.exp(1j * adc.phase_offset) * np.exp(1j * 2 * np.pi * t * adc.freq_offset)), - 'b.') - - if label_defined and len(label_idx_to_plot) != 0: - cycler = mpl.cycler(color=label_colors_to_plot) - sp11.set_prop_cycle(cycler) - label_store_arr = list(label_store.values()) - lbl_vals = np.take(label_store_arr, label_idx_to_plot) - t = t0 + adc.delay + (adc.num_samples - 1) / 2 * adc.dwell - p = sp11.plot(t_factor * t, lbl_vals, '.') - if len(label_legend_to_plot) != 0: - sp11.legend(p, label_legend_to_plot, loc='upper left') - label_legend_to_plot = [] - - if hasattr(block, 'rf'): - rf = block.rf - tc, ic = calc_rf_center(rf) - t = rf.t + rf.delay - tc = tc + rf.delay - sp12.plot(t_factor * (t0 + t), np.abs(rf.signal)) - sp13.plot(t_factor * (t0 + t), np.angle(rf.signal * np.exp(1j * rf.phase_offset) - * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)), - t_factor * (t0 + tc), np.angle(rf.signal[ic] * np.exp(1j * rf.phase_offset) - * np.exp(1j * 2 * math.pi * rf.t[ic] * rf.freq_offset)), - 'xb') - - grad_channels = ['gx', 'gy', 'gz'] - for x in range(len(grad_channels)): - if hasattr(block, grad_channels[x]): - grad = getattr(block, grad_channels[x]) - if grad.type == 'grad': - # In place unpacking of grad.t with the starred expression - t = grad.delay + [0, *(grad.t + (grad.t[1] - grad.t[0]) / 2), - grad.t[-1] + grad.t[1] - grad.t[0]] - waveform = 1e-3 * np.array((grad.first, *grad.waveform, grad.last)) - else: - t = np.cumsum([0, grad.delay, grad.rise_time, grad.flat_time, grad.fall_time]) - waveform = 1e-3 * grad.amplitude * np.array([0, 0, 1, 1, 0]) - fig2_subplots[x].plot(t_factor * (t0 + t), waveform) - t0 += self.arr_block_durations[block_counter] - - grad_plot_labels = ['x', 'y', 'z'] - sp11.set_ylabel('ADC') - sp12.set_ylabel('RF mag (Hz)') - sp13.set_ylabel('RF/ADC phase (rad)') - sp13.set_xlabel('t(s)') - for x in range(3): - _label = grad_plot_labels[x] - fig2_subplots[x].set_ylabel(f'G{_label} (kHz/m)') - fig2_subplots[-1].set_xlabel('t(s)') - - # Setting display limits - disp_range = t_factor * np.array([time_range[0], min(t0, time_range[1])]) - [x.set_xlim(disp_range) for x in [sp11, sp12, sp13, *fig2_subplots]] - - fig1.tight_layout() - fig2.tight_layout() - if save: - fig1.savefig('seq_plot1.jpg') - fig2.savefig('seq_plot2.jpg') - plt.show() - - def read(self, file_path: str) -> None: - """ - Read `.seq` file from `file_path`. - - Parameters - ---------- - file_path : str - Path to `.seq` file to be read. - """ - read(self, file_path) - - def rf_from_lib_data(self, lib_data: list) -> SimpleNamespace: - """ - Construct RF object from `lib_data`. - - Parameters - ---------- - lib_data : list - RF envelope. - - Returns - ------- - rf : SimpleNamespace - RF object constructed from lib_data. - """ - rf = SimpleNamespace() - rf.type = 'rf' - - amplitude, mag_shape, phase_shape = lib_data[0], lib_data[1], lib_data[2] - shape_data = self.shape_library.data[mag_shape] - compressed = SimpleNamespace() - compressed.num_samples = shape_data[0] - compressed.data = shape_data[1:] - mag = decompress_shape(compressed) - shape_data = self.shape_library.data[phase_shape] - compressed.num_samples = shape_data[0] - compressed.data = shape_data[1:] - phase = decompress_shape(compressed) - rf.signal = amplitude * mag * np.exp(1j * 2 * np.pi * phase) - rf.t = np.arange(1, len(mag) + 1) * self.rf_raster_time - - rf.delay = lib_data[3] - rf.freq_offset = lib_data[4] - rf.phase_offset = lib_data[5] - - if len(lib_data) < 7: - lib_data = np.append(lib_data, 0) - rf.dead_time = lib_data[6] - - if len(lib_data.shape) < 8: - lib_data = np.append(lib_data, 0) - rf.ringdown_time = lib_data[7] - - if len(lib_data.shape) < 9: - lib_data = np.append(lib_data, 0) - - use_cases = {1: 'excitation', 2: 'refocusing', 3: 'inversion'} - if lib_data[8] in use_cases: - rf.use = use_cases[lib_data[8]] - - return rf - - def set_definition(self, key: str, val: Union[int, list, np.ndarray, str, tuple]) -> None: - """ - Sets custom definition to the `Sequence`. - - Parameters - ---------- - key : str - Definition key. - val : int, list, numpy.ndarray, str or tuple - Definition value. - """ - if key == 'FOV': - if max(val) > 1: - text = 'Definition FOV uses values exceeding 1 m.' - text += 'New Pulseq interpreters expect values in units of meters.' - warn(text) - - self.dict_definitions[key] = val - - def set_extension_string_ID(self, extension_str: str, extension_id: int) -> None: - """ - Set numeric ID for the given string extension ID. - - Parameters - ---------- - extension_str : str - Given string extension ID. - extension_id : int - Given numeric extension ID. - - Raises - ------ - ValueError - If given numeric or string extension ID is not unique. - """ - if extension_str in self.arr_extension_string_idx or extension_id in self.arr_extension_numeric_idx: - raise ValueError('Numeric or string ID is not unique') - - self.arr_extension_numeric_idx.append(extension_id) - self.arr_extension_string_idx.append(extension_str) - assert len(self.arr_extension_numeric_idx) == len(self.arr_extension_string_idx) - - def test_report(self) -> str: - """ - Analyze the sequence and return a text report. - """ - return ext_test_report(self) - - # def waveforms_and_times(self) -> Tuple[np.array, np.array, np.array, np.array]: - # """ - # - # """ - # num_grad_channels = 3 - # grad_channels = ['gx', 'gy', 'gz'] - # - # num_blocks = len(self.dict_block_events) - # - # # Collect the shape pieces into an array - # grad_pieces = np.empty((num_grad_channels, num_blocks), dtype=np.object) - # # Also collect RF and timing data - # t_excitation, t_refocusing, t_adc = [], [], [] - # - # current_duration = 0 - # out_len = np.zeros(num_grad_channels, dtype=np.int) - # eps = np.finfo(np.float).eps - # - # for block_counter in range(num_blocks): - # block = self.get_block(block_counter + 1) - # - # for j in range(num_grad_channels): - # if hasattr(block, grad_channels[j]): - # grad = getattr(block, grad_channels[j]) - # if grad.type == 'grad': - # """ - # Restore & recompress shape: if we had a trapezoid converted to shape, we have to find the - # "corners" and we can eliminate internal samples on the straight segments. But first we have to - # restore samples on the edges of the gradient raster intervals. For that we need the first - # sample. - # """ - # max_abs = max(abs(grad.waveform)) - # odd_step1 = [grad.first, *2 * grad.waveform] - # odd_step2 = np.multiply(odd_step1, np.mod(np.arange(1, len(odd_step1) + 1), 2) * 2 - 1) - # waveform_odd_rest = np.multiply(np.cumsum(odd_step2), - # np.mod(np.arange(1, len(odd_step2) + 1), 2) * 2 - 1) - # waveform_odd_interp = np.array( - # [grad.first, *0.5 * (grad.waveform[:-1] + grad.waveform[1:]), grad.last]) - # - # m1 = abs(waveform_odd_rest[-1] - grad.last) - # m2 = abs(waveform_odd_rest[-1] - grad.last) / max_abs * 100 - # message = f'Last restored point of shaped gradient differs too much from the recorded last, ' \ - # f'deviation: {m1} Hz/m ({m2} %). Event number: {block_counter}' - # assert abs(waveform_odd_rest[-1] - grad.last) <= 2e-5 * max_abs, message - # - # waveform_odd_mask = abs(waveform_odd_rest - waveform_odd_interp) <= eps + 2e-5 * max_abs - # waveform_odd = waveform_odd_interp * waveform_odd_mask + waveform_odd_rest * ( - # 1 - waveform_odd_mask) - # - # # Combine odd and even - # comb = np.array(([0, *grad.waveform], [*waveform_odd])) - # waveform_os = np.concatenate([*comb.T])[1:] - # - # tt_os = np.arange(len(waveform_os - 1)) * self.grad_raster_time * 0.5 - # - # mask_changes = np.abs([1, *np.diff(waveform_os, 2), 1]) > 1e-8 - # waveform_changes = waveform_os[mask_changes] - # tt_changes = tt_os[mask_changes] - # - # tgc = np.array([tt_changes, waveform_changes]) - # out_len[j] = tgc.shape[1] - # grad_pieces[j, block_counter] = current_duration + grad.delay + tgc - # else: - # if abs(grad.flat_time) > eps: - # out_len[j] += 4 - # grad_pieces[j, block_counter] = np.array([current_duration + - # grad.delay + - # np.cumsum( - # [0, grad.rise_time, grad.flat_time, - # grad.fall_time]), - # grad.amplitude * np.array([0, 1, 1, 0])]) - # else: - # out_len[j] = out_len[j] + 3 - # grad_pieces[j, block_counter] = np.array([current_duration + - # grad.delay + - # np.cumsum([0, grad.rise_time, grad.fall_time]), - # grad.amplitude * np.array([0, 1, 0])]) - # - # if hasattr(block, 'rf'): - # rf = block.rf - # t, _ = rf.delay + calc_rf_center(rf) - # if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': - # t_excitation.append(current_duration + t) - # else: - # t_refocusing.append(current_duration + t) - # - # if hasattr(block, 'adc'): - # t_adc.extend( - # (np.arange(block.adc.num_samples) + 0.5) * block.adc.dwell + block.adc.delay + current_duration) - # - # current_duration += self.arr_block_durations[block_counter] - # - # # Collect wave data - # wave_data = np.empty(num_grad_channels, dtype=np.object) - # for i in range(num_grad_channels): - # wave_data[i] = np.zeros((2, out_len[i])) - # - # wave_count = np.zeros(num_grad_channels, dtype=np.int) - # for block_counter in range(num_blocks): - # for j in range(num_grad_channels): - # if grad_pieces[j, block_counter] is not None: - # wave_data_local = grad_pieces[j, block_counter] - # length = wave_data_local.shape[1] - # if wave_count[j] == 0 or wave_data[j][0, wave_count[j] - 1] != wave_data_local[0, 0]: - # # Python does not dynamically resize arrays during slice-based assignment - # # Indices do not necessarily start from 0, so take max() - # new_size = max(wave_count[j] + np.arange(length)) - # # Last index in an array of shape (..., new_size) would be this value - # last_ind = wave_data[j].shape[1] - 1 - # if new_size > last_ind: - # diff = new_size - last_ind # Pad the array with these many values - # wave_data[j] = np.hstack((wave_data[j], np.zeros((wave_data[j].shape[0], diff)))) - # - # wave_data[j][:, wave_count[j] + np.arange(length)] = wave_data_local - # wave_count[j] += length - # else: - # new_size = max(wave_count[j] + np.arange(length - 1)) # See previous if-block - # if new_size > wave_data[j].shape[1] - 1: - # diff = new_size - (wave_data[j].shape[1] - 1) - # wave_data[j] = np.hstack((wave_data[j], np.zeros((wave_data[j].shape[0], diff)))) - # - # wave_data[j][:, wave_count[j] + np.arange(length - 1)] = wave_data_local[:, 1:] - # wave_count[j] += length - 1 - # - # wave_pp = [] - # for j in range(num_grad_channels): - # if wave_count[j] <= 0: - # continue - # - # if not np.all(np.isfinite(wave_data[j])): - # warn('Not all elements of the generated waveform are finite.') - # - # x = wave_data[j][0, :wave_count[j]] - # fp = wave_data[j][1, :wave_count[j]] - # - # P_linear = np.zeros(x.shape) - # for n in range(1, N + 1): - # P_linear += ((data[n, 1] - data[n - 1, 1]) / (data[n, 0] - data[n - 1, 0]) * (x - data[n - 1, 0]) - # + data[n - 1, 1]) * (x > data[n - 1, 0]) * (x <= data[n, 0]) - # - # wave_pp[j] = interp1d(x, fp) - # # TODO - # - # return wave_pp, t_excitation, t_refocusing, t_adc - - def write(self, name: str) -> None: - """ - Writes the calling `Sequence` object as a `.seq` file with filename `name`. - - Parameters - ---------- - name :str - Filename of `.seq` file to be written to disk. - """ - write(self, name) diff --git a/pypulseq/Sequence/test_report.py b/pypulseq/Sequence/test_report.py deleted file mode 100644 index a384caf..0000000 --- a/pypulseq/Sequence/test_report.py +++ /dev/null @@ -1,148 +0,0 @@ -import numpy as np - -from pypulseq.convert import convert - - -def test_report(self) -> str: - """ - Analyze the sequence and return a text report. - - Returns - ------- - report : str - - """ - # Find RF pulses and list flip angles - flip_angles_deg = [] - for k in self.rf_library.keys: - lib_data = self.rf_library.data[k] - rf = self.rf_from_lib_data(lib_data) - flip_angles_deg.append(np.abs(np.sum(rf.signal) * rf.t[0] * 360)) - - flip_angles_deg = np.unique(flip_angles_deg) - - # Calculate TE, TR - duration, num_blocks, event_count = self.duration() - - k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc = self.calculate_kspace() - - k_abs_adc = np.sqrt(np.sum(np.square(k_traj_adc), axis=0)) - k_abs_echo, index_echo = np.min(k_abs_adc), np.argmin(k_abs_adc) - t_echo = t_adc[index_echo] - t_ex_tmp = t_excitation[t_excitation < t_echo] - TE = t_echo - t_ex_tmp[-1] - - if len(t_excitation) < 2: - TR = duration - else: - t_ex_tmp1 = t_excitation[t_excitation > t_echo] - if len(t_ex_tmp1) == 0: - TR = t_ex_tmp[-1] - t_ex_tmp[-2] - else: - TR = t_ex_tmp1[0] - t_ex_tmp[-1] - - # Check sequence dimensionality and spatial resolution - k_extent = np.max(np.abs(k_traj_adc), axis=1) - k_scale = np.max(k_extent) - is_cartesian = False - if k_scale != 0: - k_bins = 4e6 - k_threshold = k_scale / k_bins - - # Detect unused dimensions and delete them - if np.any(k_extent < k_threshold): - k_traj_adc = np.delete(k_traj_adc, np.where(k_extent < k_threshold), axis=0) - k_extent = np.delete(k_extent, np.where(k_extent < k_threshold), axis=0) - - # Bin the k-space trajectory to detect repetitions / slices - k_len = k_traj_adc.shape[1] - k_repeat = np.zeros(k_len) - - k_map = dict() - for i in range(k_len): - l = k_bins + np.round(k_traj_adc[:, i] / k_threshold) - key_string = ('{:.0f} ' * len(l)).format(*l) - if key_string in k_map: - k_repeat[i] = k_map[key_string] + 1 - else: - k_repeat[i] = 1 - k_map[key_string] = k_repeat[i] - - repeats = np.max(k_repeat) - - k_traj_rep1 = k_traj_adc[:, k_repeat == 1] - - k_counters = np.zeros(k_traj_rep1.shape) - dims = k_traj_rep1.shape[0] - ordering = dict() - for j in range(dims): - c = 1 - k_map = dict() - for i in range(k_traj_rep1.shape[1]): - key = round(k_traj_rep1[j, i] / k_threshold) - if key in k_map: - k_counters[j, i] = k_map[key] - else: - k_counters[j, i] = c - k_map[key] = c - c += 1 - ordering[j] = k_map.values() - - unique_k_positions = np.max(k_counters, axis=1) - is_cartesian = np.prod(unique_k_positions) == k_traj_rep1.shape[1] - else: - unique_k_positions = 1 - - gw = self.gradient_waveforms() - gws = (gw[:, 1:] - gw[:, :-1]) / self.system.grad_raster_time - ga = np.max(np.abs(gw), axis=1) - gs = np.max(np.abs(gws), axis=1) - - ga_abs = np.max(np.sqrt(np.sum(np.square(gw), axis=0))) - gs_abs = np.max(np.sqrt(np.sum(np.square(gws), axis=0))) - - timing_ok, timing_error_report = self.check_timing() - - report = f'Number of blocks: {num_blocks}\n' \ - f'Number of events:\n' \ - f'RF: {event_count[1]:6.0f}\n' \ - f'Gx: {event_count[2]:6.0f}\n' \ - f'Gy: {event_count[3]:6.0f}\n' \ - f'Gz: {event_count[4]:6.0f}\n' \ - f'ADC: {event_count[5]:6.0f}\n' \ - f'Delay: {event_count[0]:6.0f}\n' \ - f'Sequence duration: {duration:.6f} s\n' \ - f'TE: {TE:.6f} s\n' \ - f'TR: {TR:.6f} s\n' - report += 'Flip angle: ' + ('{:.02f} ' * len(flip_angles_deg)).format(*flip_angles_deg) + 'deg\n' - report += 'Unique k-space positions (aka cols, rows, etc.): ' + ('{:.0f} ' * len(unique_k_positions)).format( - *unique_k_positions) + '\n' - - if np.all(unique_k_positions > 1): - report += f'Dimensions: {len(k_extent)}\n' - report += ('Spatial resolution: {:.02f} mm\n' * len(k_extent)).format(*(0.5 / k_extent * 1e3)) - report += f'Repetitions/slices/contrasts: {repeats}\n' - - if is_cartesian: - report += 'Cartesian encoding trajectory detected\n' - else: - report += 'Non-cartesian/irregular encoding trajectory detected (eg: EPI, spiral, radial, etc.)\n' - - if timing_ok: - report += 'Event timing check passed successfully\n' - else: - report += f'Event timing check failed. Error listing follows:\n {timing_error_report}' - - ga_converted = convert(from_value=ga, from_unit='Hz/m', to_unit='mT/m') - gs_converted = convert(from_value=gs, from_unit='Hz/m/s', to_unit='T/m/s') - report += 'Max gradient: ' + ('{:.0f} ' * len(ga)).format(*ga) + 'Hz/m == ' + ( - '{:.02f} ' * len(ga_converted)).format(*ga_converted) + 'mT/m\n' - report += 'Max slew rate: ' + ('{:.0f} ' * len(gs)).format(*gs) + 'Hz/m/s == ' + ( - '{:.02f} ' * len(ga_converted)).format(*gs_converted) + 'mT/m/s\n' - - ga_abs_converted = convert(from_value=ga_abs, from_unit='Hz/m', to_unit='mT/m') - gs_abs_converted = convert(from_value=gs_abs, from_unit='Hz/m/s', to_unit='T/m/s') - report += f'Max absolute gradient: {ga_abs:.0f} Hz/m == {ga_abs_converted:.2f} mT/m\n' - report += f'Max absolute slew rate: {gs_abs:g} Hz/m/s == {gs_abs_converted:.2f} T/m/s' - - return report diff --git a/pypulseq/Sequence/write_seq.py b/pypulseq/Sequence/write_seq.py deleted file mode 100644 index db5ab78..0000000 --- a/pypulseq/Sequence/write_seq.py +++ /dev/null @@ -1,204 +0,0 @@ -import numpy as np - -from pypulseq.supported_labels import get_supported_labels - - -def write(self, file_name: str) -> None: - """ - Writes the calling `Sequence` object as a `.seq` file with filename `file_name`. - - Parameters - ---------- - file_name : str - File name of `.seq` file to be written to disk. - - Raises - ------ - RuntimeError - If an unsupported definition is encountered. - """ - # `>.0f` is used when only decimals have to be displayed. - # `>g` is used when insignificant zeros have to be truncated. - file_name += '.seq' if file_name[-4:] != '.seq' not in file_name else '' - output_file = open(file_name, 'w') - output_file.write('# Pulseq sequence file\n') - output_file.write('# Created by PyPulseq\n\n') - - output_file.write('[VERSION]\n') - output_file.write(f'major {self.version_major}\n') - output_file.write(f'minor {self.version_minor}\n') - output_file.write(f'revision {self.version_revision}\n') - output_file.write('\n') - - if len(self.dict_definitions) != 0: - output_file.write('[DEFINITIONS]\n') - keys = list(self.dict_definitions.keys()) - values = list(self.dict_definitions.values()) - for block_counter in range(len(keys)): - output_file.write(f'{keys[block_counter]} ') - if isinstance(values[block_counter], str): - output_file.write(values[block_counter] + ' ') - 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])): - if isinstance(values[block_counter][i], (int, float)): - output_file.write(f'{values[block_counter][i]:0.9g} ') - else: - output_file.write(f'{values[block_counter][i]} ') - else: - raise RuntimeError('Unsupported definition') - output_file.write('\n') - output_file.write('\n') - - output_file.write('# Format of blocks:\n') - output_file.write('# # D RF GX GY GZ ADC EXT\n') - output_file.write('[BLOCKS]\n') - id_format_width = '{:' + str(len(str(len(self.dict_block_events)))) + 'd}' - id_format_str = id_format_width + ' ' + '{:2d} {:2d} {:3d} {:3d} {:3d} {:2d} {:2d}\n' - for block_counter in range(len(self.dict_block_events)): - s = id_format_str.format(*(block_counter + 1, *self.dict_block_events[block_counter + 1])) - output_file.write(s) - output_file.write('\n') - - if len(self.rf_library.keys) != 0: - output_file.write('# Format of RF events:\n') - output_file.write('# id amplitude mag_id phase_id delay freq phase\n') - output_file.write('# .. Hz .... .... us Hz rad\n') - output_file.write('[RF]\n') - rf_lib_keys = self.rf_library.keys - # See comment at the beginning of this method definition - id_format_str = '{:.0f} {:12g} {:.0f} {:.0f} {:g} {:g} {:g}\n' - for k in rf_lib_keys.keys(): - lib_data1 = self.rf_library.data[k][0:3] - lib_data2 = self.rf_library.data[k][4:6] - delay = np.round(self.rf_library.data[k][3] * 1e6) - s = id_format_str.format(k, *lib_data1, delay, *lib_data2) - output_file.write(s) - output_file.write('\n') - - grad_lib_values = np.array(list(self.grad_library.type.values())) - arb_grad_mask = grad_lib_values == 'g' - trap_grad_mask = grad_lib_values == 't' - - if any(arb_grad_mask): - output_file.write('# Format of arbitrary gradients:\n') - output_file.write('# id amplitude shape_id delay\n') - output_file.write('# .. Hz/m .... us\n') - output_file.write('[GRADIENTS]\n') - id_format_str = '{:.0f} {:12g} {:.0f} {:.0f}\n' # See comment at the beginning of this method definition - keys = np.array(list(self.grad_library.keys.keys())) - for k in keys[arb_grad_mask]: - s = id_format_str.format(k, *self.grad_library.data[k][:2], np.round(self.grad_library.data[k][2] * 1e6)) - output_file.write(s) - output_file.write('\n') - - if any(trap_grad_mask): - output_file.write('# Format of trapezoid gradients:\n') - output_file.write('# id amplitude rise flat fall delay\n') - output_file.write('# .. Hz/m us us us us\n') - output_file.write('[TRAP]\n') - keys = np.array(list(self.grad_library.keys.keys())) - id_format_str = '{:2g} {:12g} {:3g} {:4g} {:3g} {:3g}\n' - for k in keys[trap_grad_mask]: - data = np.copy(self.grad_library.data[k]) # Make a copy to leave the original untouched - data[1:] = np.round(1e6 * data[1:]) - """ - Python always rounds to nearest even value, this can cause inconsistencies with MATLAB Pulseq's .seq files. - Read more - https://stackoverflow.com/questions/29671945/format-string-rounding-inconsistent - Numpy too - https://stackoverflow.com/questions/50374779/how-to-avoid-incorrect-rounding-with-numpy-round - """ - s = id_format_str.format(k, *data) - output_file.write(s) - output_file.write('\n') - - if len(self.adc_library.keys) != 0: - output_file.write('# Format of ADC events:\n') - output_file.write('# id num dwell delay freq phase\n') - output_file.write('# .. .. ns us Hz rad\n') - output_file.write('[ADC]\n') - keys = self.adc_library.keys - # See comment at the beginning of this method definition - id_format_str = '{:.0f} {:.0f} {:.0f} {:.0f} {:g} {:g}\n' - for k in keys.values(): - data = np.multiply(self.adc_library.data[k][0:5], [1, 1e9, 1e6, 1, 1]) - s = id_format_str.format(k, *data) - output_file.write(s) - output_file.write('\n') - - if len(self.delay_library.keys) != 0: - output_file.write('# Format of delays:\n') - output_file.write('# id delay (us)\n') - output_file.write('[DELAYS]\n') - keys = self.delay_library.keys - id_format_str = '{:.0f} {:.0f}\n' # See if-block for self.rf.library.keys - for k in keys.values(): - s = id_format_str.format(k, *np.round(1e6 * self.delay_library.data[k])) - output_file.write(s) - output_file.write('\n') - - if len(self.extensions_library.keys) != 0: - output_file.write('# Format of extension lists:\n') - output_file.write('# id type ref next_id\n') - output_file.write('# next_id of 0 terminates the list\n') - output_file.write('# Extension list is followed by extension specifications\n') - output_file.write('[EXTENSIONS]\n') - keys = self.extensions_library.keys - id_format_str = '{:.0f} {:.0f} {:.0f} {:.0f}\n' # See comment at the beginning of this method definition - for k in keys.values(): - s = id_format_str.format(k, *np.round(self.extensions_library.data[k])) - output_file.write(s) - output_file.write('\n') - - if len(self.trigger_library.keys) != 0: - output_file.write('# Extension specification for digital output and input triggers:\n') - output_file.write('# id type channel delay (us) duration (us)\n') - output_file.write(f'extension TRIGGERS {self.get_extension_type_ID("TRIGGERS")}\n') - keys = self.trigger_library.keys - id_format_str = '{:.0f} {:.0f} {:.0f} {:.0f} {:.0f}\n' # See comment at the beginning of this method definition - for k in keys.values(): - s = id_format_str.format(k, *np.round(self.trigger_library.data[k] * [1, 1, 1e6, 1e6])) - output_file.write(s) - output_file.write('\n') - - if len(self.label_set_library.keys) != 0: - lbls = get_supported_labels() - - output_file.write('# Extension specification for setting labels:\n') - output_file.write('# id set labelstring\n') - tid = self.get_extension_type_ID('LABELSET') - output_file.write(f'extension LABELSET {tid}\n') - keys = self.label_set_library.keys - id_format_str = '{:.0f} {:.0f} {}\n' # See comment at the beginning of this method definition - for k in keys.values(): - s = id_format_str.format(k, self.label_set_library.data[k][0], lbls[self.label_set_library.data[k][1] - 1]) - output_file.write(s) - output_file.write('\n') - - output_file.write('# Extension specification for setting labels:\n') - output_file.write('# id set labelstring\n') - tid = self.get_extension_type_ID('LABELINC') - output_file.write(f'extension LABELINC {tid}\n') - keys = self.label_inc_library.keys - id_format_str = '{:.0f} {:.0f} {}\n' # See comment at the beginning of this method definition - for k in keys.values(): - s = id_format_str.format(k, self.label_inc_library.data[k][0], lbls[self.label_inc_library.data[k][1] - 1]) - output_file.write(s) - output_file.write('\n') - - if len(self.shape_library.keys) != 0: - output_file.write('# Sequence Shapes\n') - output_file.write('[SHAPES]\n\n') - keys = self.shape_library.keys - for k in keys.values(): - shape_data = self.shape_library.data[k] - s = 'shape_id {:.0f}\n' - s = s.format(k) - output_file.write(s) - s = 'num_samples {:.0f}\n' - s = s.format(shape_data[0]) - output_file.write(s) - s = '{:.9g}\n' * len(shape_data[1:]) - s = s.format(*shape_data[1:]) - output_file.write(s) - output_file.write('\n') diff --git a/pypulseq/__init__.py b/pypulseq/__init__.py deleted file mode 100644 index f626145..0000000 --- a/pypulseq/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -from pathlib import Path - -import numpy as np - -path_version = Path(__file__).parent.parent / 'VERSION' -with open(str(path_version), 'r') as version_file: - # s = version_file.read().strip().split('.') - # print(s) - 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 -from pypulseq.align import align -from pypulseq.calc_duration import calc_duration -from pypulseq.calc_ramp import calc_ramp -from pypulseq.calc_rf_center import calc_rf_center -from pypulseq.make_adc import make_adc -from pypulseq.make_arbitrary_rf import make_arbitrary_rf -from pypulseq.make_block_pulse import make_block_pulse -from pypulseq.make_delay import make_delay -from pypulseq.make_digital_output_pulse import make_digital_output_pulse -from pypulseq.make_extended_trapezoid import make_extended_trapezoid -from pypulseq.make_extended_trapezoid_area import make_extended_trapezoid_area -from pypulseq.make_gauss_pulse import make_gauss_pulse -from pypulseq.make_label import make_label -from pypulseq.make_sinc_pulse import make_sinc_pulse -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.make_trigger import make_trigger -from pypulseq.opts import Opts -from pypulseq.points_to_waveform import points_to_waveform -from pypulseq.split_gradient import split_gradient -from pypulseq.split_gradient_at import split_gradient_at -from pypulseq.supported_labels import get_supported_labels -from pypulseq.traj_to_grad import traj_to_grad diff --git a/pypulseq/add_gradients.py b/pypulseq/add_gradients.py deleted file mode 100644 index 2c781b2..0000000 --- a/pypulseq/add_gradients.py +++ /dev/null @@ -1,97 +0,0 @@ -from types import SimpleNamespace -from typing import Union - -import numpy as np - -from pypulseq.calc_duration import calc_duration -from pypulseq.make_arbitrary_grad import make_arbitrary_grad -from pypulseq.opts import Opts -from pypulseq.points_to_waveform import points_to_waveform - - -def add_gradients(grads: Union[list, tuple], system=Opts(), max_grad: int = 0, max_slew: int = 0) -> SimpleNamespace: - """ - Superpose several gradient events. - - Parameters - ---------- - grads : list or tuple - List or tuple of 'SimpleNamespace' gradient events. - system : Opts, optional, default=Opts() - System limits. - max_grad : float, optional, default=0 - Maximum gradient amplitude. - max_slew : float, optional, default=0 - Maximum slew rate. - - Returns - ------- - grad : SimpleNamespace - Superimposition of gradient events from `grads`. - """ - max_grad = max_grad if max_grad > 0 else system.max_grad - max_slew = max_slew if max_slew > 0 else system.max_slew - - if len(grads) < 2: - raise Exception() - - # First gradient defines channel - channel = grads[0].channel - - # Find out the general delay of all gradients and other statistics - delays, firsts, lasts, durs = [], [], [], [] - for ii in range(len(grads)): - delays.append(grads[ii].delay) - firsts.append(grads[ii].first) - lasts.append(grads[ii].last) - durs.append(calc_duration(grads[ii])) - - # Convert to numpy.ndarray for fancy-indexing later on - firsts, lasts = np.array(firsts), np.array(lasts) - - common_delay = min(delays) - total_duration = max(durs) - - waveforms = dict() - max_length = 0 - for ii in range(len(grads)): - g = grads[ii] - if g.type == 'grad': - waveforms[ii] = g.waveform - elif g.type == 'trap': - if g.flat_time > 0: # Triangle or trapezoid - times = [g.delay - common_delay, - g.delay - common_delay + g.rise_time, - g.delay - common_delay + g.rise_time + g.flat_time, - g.delay - common_delay + g.rise_time + g.flat_time + g.fall_time] - amplitudes = [0, g.amplitude, g.amplitude, 0] - else: - times = [g.delay - common_delay, - g.delay - common_delay + g.rise_time, - g.delay - common_delay + g.rise_time + g.flat_time] - amplitudes = [0, g.amplitude, 0] - waveforms[ii] = points_to_waveform(times=times, amplitudes=amplitudes, - grad_raster_time=system.grad_raster_time) - else: - raise ValueError('Unknown gradient type') - - if g.delay - common_delay > 0: - # Stop for numpy.arange is not g.delay - common_delay - system.grad_raster_time like in Matlab - # so as to include the endpoint - t_delay = np.arange(0, g.delay - common_delay, step=system.grad_raster_time) - waveforms[ii] = np.insert(waveforms[ii], 0, t_delay) - - num_points = len(waveforms[ii]) - max_length = num_points if num_points > max_length else max_length - - w = np.zeros(max_length) - for ii in range(len(grads)): - wt = np.zeros(max_length) - wt[0:len(waveforms[ii])] = waveforms[ii] - w += wt - - grad = make_arbitrary_grad(channel, w, system, max_slew=max_slew, max_grad=max_grad, delay=common_delay) - grad.first = np.sum(firsts[np.array(delays) == common_delay]) - grad.last = np.sum(lasts[np.where(durs == total_duration)]) - - return grad diff --git a/pypulseq/add_ramps.py b/pypulseq/add_ramps.py deleted file mode 100644 index 5cdeeee..0000000 --- a/pypulseq/add_ramps.py +++ /dev/null @@ -1,76 +0,0 @@ -from copy import copy -from types import SimpleNamespace -from typing import Union, List - -import numpy as np - -from pypulseq.calc_ramp import calc_ramp -from pypulseq.opts import Opts - - -def add_ramps(k: Union[list, np.ndarray, tuple], system=Opts(), rf: SimpleNamespace = None, max_grad: int = 0, - max_slew: int = 0) -> List[np.ndarray]: - """ - Adds segment so that `k` k-space trajectory ramps up from 0 to `k[0]` and ramps down from `k[-1]` to 0. If `k` is a - tuple or list of k-space trajectories, ramp-ups and ramp-downs are added to each. - - Parameters - ---------- - k : array_like - Array-like of k-space trajectories to add ramp-ups and -downs to. - system : Opts, optional, default=Opts() - System limits. - rf : SimpleNamespace, optional - Zeros are added to this pulse sequence event over the ramp times in `k`. - max_grad : int, optional, default=0 - Maximum gradient amplitude. - max_slew : int, optional, default=0 - Maximum slew rate. - - Returns - ------- - result : list[ - List of ramped up and ramped down k-space trajectories from `k`. - - Raises - ------ - ValueError - If `k` is not list, np.ndarray or tuple - RuntimeError - If gradient ramps fail to be calculated - """ - if not isinstance(k, (list, np.ndarray, tuple)): - raise ValueError(f'k has to be one of list, np.ndarray, tuple. Passed: {type(k)}') - - k_arg = copy(k) - if max_grad > 0: - system.max_grad = max_grad - - if max_slew > 0: - system.max_slew = max_slew - - k = np.vstack(k) - num_channels = k.shape[0] - k = np.vstack((k, np.zeros((3 - num_channels, k.shape[1])))) - - k_up, ok1 = calc_ramp(np.zeros((3, 2)), k[:, :2], system) - k_down, ok2 = calc_ramp(k[:, -2:], np.zeros((3, 2)), system) - if not (ok1 and ok2): - raise RuntimeError('Failed to calculate gradient ramps') - - k_up = np.hstack((np.zeros((3, 2)), k_up)) - k_down = np.hstack((k_down, np.zeros((3, 1)))) - - k = np.hstack((k_up, k, k_down)) - - result = [] - if not isinstance(k_arg, list): - result.append(k[:num_channels]) - else: - for i in range(num_channels): - result.append(k[i]) - - if rf is not None: - result.append(np.concatenate((np.zeros(k_up.shape[1] * 10), rf, np.zeros(k_down.shape[1] * 10)))) - - return result diff --git a/pypulseq/align.py b/pypulseq/align.py deleted file mode 100644 index 61c1381..0000000 --- a/pypulseq/align.py +++ /dev/null @@ -1,64 +0,0 @@ -from types import SimpleNamespace -from typing import List, Union - -import numpy as np - -from pypulseq.calc_duration import calc_duration - - -def align(**kwargs: Union[SimpleNamespace, List[SimpleNamespace]]) -> List[SimpleNamespace]: - """ - Aligns `SimpleNamespace` objects as per specified alignment options by setting delays of the pulse sequence events - within the block. All previously configured delays within objects are taken into account during calculating of the - block duration but then reset according to the selected alignment. Possible values for align_spec are 'left', - 'center', 'right'. - - Parameters - ---------- - args : dict[str, list[SimpleNamespace] - Dictionary mapping of alignment options and `SimpleNamespace` objects. - Template: alignment_spec1=SimpleNamespace, alignment_spec2=[SimpleNamespace, ...], ... - Alignment spec must be one of `left`, `center` or `right`. - - Returns - ------- - objects : list - List of aligned `SimpleNamespace` objects. - - Raises - ------ - ValueError - If first parameter is not of type `str`. - If invalid alignment spec is passed. Must be one of `left`, `center` or `right`. - """ - alignment_specs = list(kwargs.keys()) - if not isinstance(alignment_specs[0], str): - raise ValueError(f'First parameter must be of type str. Passed: {type(alignment_specs[0])}') - - alignment_options = ['left', 'center', 'right'] - if np.any([align_opt not in alignment_options for align_opt in alignment_specs]): - raise ValueError('Invalid alignment spec.') - - alignments = [] - objects = [] - for a in alignment_specs: - objects_to_align = kwargs[a] - a = alignment_options.index(a) - if isinstance(objects_to_align, (list, np.ndarray, tuple)): - alignments.extend([a] * len(objects_to_align)) - objects.extend(objects_to_align) - elif isinstance(objects_to_align, SimpleNamespace): - alignments.extend([a]) - objects.append(objects_to_align) - - dur = calc_duration(*objects) - - for i in range(len(objects)): - if alignments[i] == 0: - objects[i].delay = 0 - elif alignments[i] == 1: - objects[i].delay = (dur - calc_duration(objects[i])) / 2 - elif alignments[i] == 2: - objects[i].delay = dur - calc_duration(objects[i]) + objects[i].delay - - return objects diff --git a/pypulseq/block_to_events.py b/pypulseq/block_to_events.py deleted file mode 100644 index 67afd6d..0000000 --- a/pypulseq/block_to_events.py +++ /dev/null @@ -1,36 +0,0 @@ -from types import SimpleNamespace -from typing import Tuple - - -def block_to_events(args: Tuple[SimpleNamespace, ...]) -> Tuple[SimpleNamespace, ...]: - """ - Converts `args` from a block to a list of events. If `args` is already a list of event(s), returns it unmodified. - - Parameters - ---------- - args : list[SimpleNamespace] - Block to be converted into a list of events, or list of events. - - Returns - ------- - events : list[SimpleNamespace] - List of events comprising `args` if it was a block, otherwise `args` unmodified. - """ - if len(args) == 1: # args is a tuple consisting either a block or a single event - x = args[0] - attrs = vars(x).keys() - children = [getattr(x, a) for a in attrs] - if all([isinstance(c, (SimpleNamespace, dict)) for c in children]): # args is a block of events - events = list(x.__dict__.values()) - - # Are any of the events labels? If yes, extract them from dict() - for e in events: - if isinstance(e, dict): - events.remove(e) - events.extend(e.values()) - else: # args is a single event - events = [x] - else: # args is a tuple of events - events = args - - return events diff --git a/pypulseq/calc_duration.py b/pypulseq/calc_duration.py deleted file mode 100644 index 1032109..0000000 --- a/pypulseq/calc_duration.py +++ /dev/null @@ -1,41 +0,0 @@ -from types import SimpleNamespace - -from pypulseq.block_to_events import block_to_events - - -def calc_duration(*args: SimpleNamespace) -> float: - """ - Calculate the cumulative duration of Events. - - Parameters - ---------- - args : list[SimpleNamespace] - List of `SimpleNamespace` objects. Can also be a list containing a single block (see - `pypulseq.Sequence.sequence.plot()`). - - Returns - ------- - duration : float - The cumulative duration of the pulse events in `events`. - """ - events = block_to_events(args) - - duration = 0 - for event in events: - if not isinstance(event, (dict, SimpleNamespace)): - raise TypeError("input(s) should be of type SimpleNamespace or a dict() in case of LABELINC or LABELSET") - - if event.type == 'delay': - duration = max(duration, event.delay) - elif event.type == 'rf': - duration = max(duration, event.delay + event.t[-1]) - elif event.type == 'grad': - duration = max(duration, event.t[-1] + event.t[1] - event.t[0] + event.delay) - elif event.type == 'adc': - duration = max(duration, event.delay + event.num_samples * event.dwell + event.dead_time) - elif event.type == 'trap': - duration = max(duration, event.delay + event.rise_time + event.flat_time + event.fall_time) - elif event.type == 'output' or event.type == 'trigger': - duration = max(duration, event.delay + event.duration) - - return duration diff --git a/pypulseq/calc_ramp.py b/pypulseq/calc_ramp.py deleted file mode 100644 index 114ae5f..0000000 --- a/pypulseq/calc_ramp.py +++ /dev/null @@ -1,320 +0,0 @@ -from typing import Tuple - -import numpy as np - -from pypulseq.opts import Opts - - -def calc_ramp(k0: np.ndarray, k_end: np.ndarray, max_grad: np.ndarray = np.zeros(0), max_points: int = 500, - max_slew: np.ndarray = np.zeros(0), system: Opts = Opts()) -> Tuple[np.ndarray, bool]: - """ - Join the points `k0` and `k_end` in three-dimensional k-space in minimal time, observing the gradient and slew - limits (`max_grad` and `max_slew` respectively), and the gradient strength `G0` before `k0[:, 1]` and `Gend` after - `k_end[:, 1]`. In the context of a fixed gradient dwell time this is a discrete problem with an a priori unknown - number of discretization steps. Therefore this method tries out the optimization with 0 steps, then 1 step, and so - on, until all conditions can be fulfilled, thus yielding a short connection. - - Parameters - ---------- - k0 : numpy.ndarray - Two preceding points in k-space. Shape is `[3, 2]`. From these points, the starting gradient will be calculated. - k_end : numpy.ndarray - Two following points in k-space. Shape is `[3, 2]`. From these points, the target gradient will be calculated. - max_grad : float or array_like, optional, default=0 - Maximum total gradient strength. Either a single value or one value for each coordinate, of shape `[3, 1]`. - max_points : int, optional, default=500 - Maximum number of k-space points to be used in connecting `k0` and `k_end`. - max_slew : float or array_like, optional, default=0 - Maximum total slew rate. Either a single value or one value for each coordinate, of shape `[3, 1]`. - system : Opts, optional, default=Opts() - System limits. - - Returns - ------- - k_out : numpy.ndarray - Connected k-space trajectory. - success : bool - Boolean flag indicating if `k0` and `k_end` were successfully joined. - """ - - def __inside_limits(grad, slew): - if mode == 0: - grad2 = np.sum(np.square(grad), axis=1) - slew2 = np.sum(np.square(slew), axis=1) - ok = np.all(np.max(grad2) <= np.square(max_grad)) and np.all(np.max(slew2) <= np.square(max_slew)) - else: - ok = (np.sum(np.max(np.abs(grad), axis=1) <= max_grad) == 3) and ( - np.sum(np.max(np.abs(slew), axis=1) <= max_slew) == 3) - - return ok - - def __joinleft0(k0, k_end, G0, G_end, use_points): - if use_points == 0: - G = np.stack((G0, (k_end - k0) / grad_raster, G_end)).T - S = (G[:, 1:] - G[:, :-1]) / grad_raster - - k_out_left = np.zeros((3, 0)) - success = __inside_limits(G, S) - - return success, k_out_left - - dk = (k_end - k0) / (use_points + 1) - kopt = k0 + dk - Gopt = (kopt - k0) / grad_raster - Sopt = (Gopt - G0) / grad_raster - - okGopt = np.sum(np.square(Gopt)) <= np.square(max_grad) - okSopt = np.sum(np.square(Sopt)) <= np.square(max_slew) - - if okGopt and okSopt: - k_left = kopt - else: - a = np.multiply(max_grad, grad_raster) - b = np.multiply(max_slew, grad_raster ** 2) - - dkprol = G0 * grad_raster - dkconn = dk - dkprol - - ksl = k0 + dkprol + dkconn / np.linalg.norm(dkconn) * b - Gsl = (ksl - k0) / grad_raster - okGsl = np.sum(np.square(Gsl)) <= np.square(max_grad) - - kgl = k0 + np.multiply(dk / np.linalg.norm(dk), a) - Ggl = (kgl - k0) / grad_raster - Sgl = (Ggl - G0) / grad_raster - okSgl = np.sum(np.square(Sgl)) <= np.square(max_slew) - - if okGsl: - k_left = ksl - elif okSgl: - k_left = kgl - else: - c = np.linalg.norm(dkprol) - c1 = np.divide(np.square(a) - np.square(b) + np.square(c), (2 * c)) - h = np.sqrt(np.square(a) - np.square(c1)) - kglsl = k0 + np.multiply(c1, np.divide(dkprol, np.linalg.norm(dkprol))) - projondkprol = (kgl * dkprol.T) * (dkprol / np.linalg.norm(dkprol)) - hdirection = kgl - projondkprol - kglsl = kglsl + h * hdirection / np.linalg.norm(hdirection) - k_left = kglsl - - success, k = __joinright0(k_left, k_end, (k_left - k0) / grad_raster, G_end, use_points - 1) - if len(k) != 0: - if len(k.shape) == 1: - k = k.reshape((len(k), 1)) - if len(k_left.shape) == 1: - k_left = k_left.reshape((len(k_left), 1)) - k_out_left = np.hstack((k_left, k)) - else: - k_out_left = k_left - - return success, k_out_left - - def __joinleft1(k0, k_end, G0, G_end, use_points): - if use_points == 0: - G = np.stack((G0, (k_end - k0) / grad_raster, G_end)) - S = (G[:, 1:] - G[:, :-1]) / grad_raster - - k_out_left = np.zeros((3, 0)) - success = __inside_limits(G, S) - - return success, k_out_left - - k_left = np.zeros(3) - - dk = (k_end - k0) / (use_points + 1) - kopt = k0 + dk - Gopt = (kopt - k0) / grad_raster - Sopt = (Gopt - G0) / grad_raster - - okGopt = np.abs(Gopt) <= max_grad - okSopt = np.abs(Sopt) <= max_slew - - dkprol = G0 * grad_raster - dkconn = dk - dkprol - - ksl = k0 + dkprol + np.multiply(np.sign(dkconn), max_slew) * grad_raster ** 2 - Gsl = (ksl - k0) / grad_raster - okGsl = np.abs(Gsl) <= max_grad - - kgl = k0 + np.multiply(np.sign(dk), max_grad) * grad_raster ** 2 - Ggl = (kgl - k0) / grad_raster - Sgl = (Ggl - G0) / grad_raster - okSgl = np.abs(Sgl) <= max_slew - - for ii in range(3): - if okGopt[ii] == 1 and okSopt[ii] == 1: - k_left[ii] = kopt[ii] - elif okGsl[ii] == 1: - k_left[ii] = ksl[ii] - elif okSgl[ii] == 1: - k_left[ii] = kgl[ii] - else: - print('Unknown error') - - success, k = __joinright1(k_left, k_end, (k_left - k0) / grad_raster, G_end, use_points - 1) - if len(k) != 0: - if len(k.shape) == 1: - k = k.reshape((len(k), 1)) - if len(k_left.shape) == 1: - k_left = k_left.reshape((len(k_left), 1)) - k_out_left = np.hstack((k_left, k)) - else: - k_out_left = k_left - - return success, k_out_left - - def __joinright0(k0, k_end, G0, G_end, use_points): - if use_points == 0: - G = np.stack((G0, (k_end - k0) / grad_raster, G_end)).T - S = (G[:, 1:] - G[:, :-1]) / grad_raster - - k_out_right = np.zeros((3, 0)) - success = __inside_limits(G, S) - - return success, k_out_right - - dk = (k0 - k_end) / (use_points + 1) - kopt = k_end + dk - Gopt = (k_end - kopt) / grad_raster - Sopt = (G_end - Gopt) / grad_raster - - okGopt = np.sum(np.square(Gopt)) <= np.square(max_grad) - okSopt = np.sum(np.square(Sopt)) <= np.square(max_slew) - - if okGopt and okSopt: - k_right = kopt - else: - a = np.multiply(max_grad, grad_raster) - b = np.multiply(max_slew, grad_raster ** 2) - - dkprol = -G_end * grad_raster - dkconn = dk - dkprol - - ksl = k_end + dkprol + dkconn / np.linalg.norm(dkconn) * b - Gsl = (k_end - ksl) / grad_raster - okGsl = np.sum(np.square(Gsl)) <= np.square(max_grad) - - kgl = k_end + np.multiply(dk / np.linalg.norm(dk), a) - Ggl = (k_end - kgl) / grad_raster - Sgl = (G_end - Ggl) / grad_raster - okSgl = np.sum(np.square(Sgl)) <= np.square(max_slew) - - if okGsl: - k_right = ksl - elif okSgl: - k_right = kgl - else: - c = np.linalg.norm(dkprol) - c1 = np.divide(np.square(a) - np.square(b) + np.square(c), (2 * c)) - h = np.sqrt(np.square(a) - np.square(c1)) - kglsl = k_end + np.multiply(c1, np.divide(dkprol, np.linalg.norm(dkprol))) - projondkprol = (kgl * dkprol.T) * (dkprol / np.linalg.norm(dkprol)) - hdirection = kgl - projondkprol - kglsl = kglsl + h * hdirection / np.linalg.norm(hdirection) - k_right = kglsl - - success, k = __joinleft0(k0, k_right, G0, (k_end - k_right) / grad_raster, use_points - 1) - if len(k) != 0: - if len(k.shape) == 1: - k = k.reshape((len(k), 1)) - if len(k_right.shape) == 1: - k_right = k_right.reshape((len(k_right), 1)) - k_out_right = np.hstack((k, k_right)) - else: - k_out_right = k_right - - return success, k_out_right - - def __joinright1(k0, k_end, G0, G_end, use_points): - if use_points == 0: - G = np.stack((G0, (k_end - k0) / grad_raster, G_end)) - S = (G[:, 1:] - G[:, :-1]) / grad_raster - - k_out_right = np.zeros((3, 0)) - success = __inside_limits(G, S) - - return success, k_out_right - - k_right = np.zeros(3) - - dk = (k0 - k_end) / (use_points + 1) - kopt = k_end + dk - Gopt = (k_end - kopt) / grad_raster - Sopt = (G_end - Gopt) / grad_raster - - okGopt = np.abs(Gopt) <= max_grad - okSopt = np.abs(Sopt) <= max_slew - - dkprol = -G_end * grad_raster - dkconn = dk - dkprol - - ksl = k_end + dkprol + np.multiply(np.sign(dkconn), max_slew) * grad_raster ** 2 - Gsl = (k_end - ksl) / grad_raster - okGsl = np.abs(Gsl) <= max_grad - - kgl = k_end + np.multiply(np.sign(dk), max_grad) * grad_raster - Ggl = (k_end - kgl) / grad_raster - Sgl = (G_end - Ggl) / grad_raster - okSgl = np.abs(Sgl) <= max_slew - - for ii in range(3): - if okGopt[ii] == 1 and okSopt[ii] == 1: - k_right[ii] = kopt[ii] - elif okGsl[ii] == 1: - k_right[ii] = ksl[ii] - elif okSgl[ii] == 1: - k_right[ii] = kgl[ii] - else: - print('Unknown error') - - success, k = __joinleft1(k0, k_right, G0, (k_end - k_right) / grad_raster, use_points - 1) - if len(k) != 0: - if len(k.shape) == 1: - k = k.reshape((len(k), 1)) - if len(k_right.shape) == 1: - k_right = k_right.reshape((len(k_right), 1)) - k_out_right = np.hstack((k, k_right)) - else: - k_out_right = k_right - - return success, k_out_right - - # ========= - # MAIN FUNCTION - # ========= - if np.all(np.where(max_grad <= 0)): - max_grad = [system.max_grad] - if np.all(np.where(max_slew <= 0)): - max_slew = [system.max_slew] - - grad_raster = system.grad_raster_time - - if len(max_grad) == 1 and len(max_slew) == 1: - mode = 0 - elif len(max_grad) == 3 and len(max_slew) == 3: - mode = 1 - else: - raise ValueError('Input value max grad or max slew in invalid format.') - - G0 = (k0[:, 1] - k0[:, 0]) / grad_raster - G_end = (k_end[:, 1] - k_end[:, 0]) / grad_raster - k0 = k0[:, 1] - k_end = k_end[:, 0] - - success = 0 - k_out = np.zeros((3, 0)) - use_points = 0 - - while success == 0 and use_points <= max_points: - if mode == 0: - if np.linalg.norm(G0) > max_grad or np.linalg.norm(G_end) > max_grad: - break - success, k_out = __joinleft0(k0, k_end, G0, G_end, use_points) - else: - if np.abs(G0) > np.abs(max_grad) or np.abs(G_end) > np.abs(max_grad): - break - success, k_out = __joinleft1(k0, k_end, G0, G_end, use_points) - use_points += 1 - - return k_out, success diff --git a/pypulseq/calc_rf_center.py b/pypulseq/calc_rf_center.py deleted file mode 100644 index 2862bdf..0000000 --- a/pypulseq/calc_rf_center.py +++ /dev/null @@ -1,31 +0,0 @@ -from types import SimpleNamespace -from typing import Tuple - -import numpy as np - - -def calc_rf_center(rf: SimpleNamespace) -> Tuple[float, float]: - """ - Calculate the time point of the effective rotation calculated as the peak of the radio-frequency amplitude for the - shaped pulses and the center of the pulse for the block pulses. Zero padding in the radio-frequency pulse is - considered as a part of the shape. Delay field of the radio-frequency object is not taken into account. - - Parameters - ---------- - rf : SimpleNamespace - Radio-frequency pulse event. - - Returns - ------- - time_center : float - Time point of the center of the radio-frequency pulse. - id_center : float - Corresponding position of `time_center` in the radio-frequency pulse's envelope. - """ - # 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 diff --git a/pypulseq/check_timing.py b/pypulseq/check_timing.py deleted file mode 100644 index f7fb6f1..0000000 --- a/pypulseq/check_timing.py +++ /dev/null @@ -1,104 +0,0 @@ -from types import SimpleNamespace -from typing import Tuple - -import numpy as np - -from pypulseq.calc_duration import calc_duration -from pypulseq.opts import Opts - - -def check_timing(system: Opts, *events: SimpleNamespace) -> Tuple[bool, str, float]: - """ - Checks if timings of events `events` are aligned with gradient raster time `system.grad_raster_time`. - - Parameters - ---------- - system : Opts - System limits object. - events : iterable of SimpleNamespace - Events. - - Returns - ------- - is_ok : bool - Boolean flag indicating if timing of events `events` are aligned with gradient raster time - `system.grad_raster_time`. - text_err : str - Error string, if timings are not aligned. - - Raises - ------ - ValueError - Wrong data type of variable arguments. - """ - if len(events) == 0: - text_err = 'Empty or damaged block detected' - is_ok = False - total_duration = 0. - return is_ok, text_err, total_duration - - total_duration = calc_duration(*events) - is_ok = __div_check(total_duration, system.grad_raster_time) - text_err = '' if is_ok else f'Total duration: {total_duration * 1e6} us' - - for i in range(len(events)): - e = events[i] - if not isinstance(e, SimpleNamespace): - raise ValueError('Wrong data type of variable arguments, list[SimpleNamespace] expected.') - ok = True - if isinstance(e, list) and len(e) > 1: - # From Pulseq 1.3.1: For now this is only the case for arrays of extensions, but we cannot actually check - # extensions anyway... - continue - if hasattr(e, 'type') and e.type == 'adc' or e.type == 'rf': - raster = system.rf_raster_time - else: - raster = system.grad_raster_time - - if hasattr(e, 'delay'): - eps = np.finfo(float).eps # np.float deprecated - if e.delay < -eps: - ok = False - if not __div_check(e.delay, raster): - ok = False - - if hasattr(e, 'duration'): - if not __div_check(e.duration, raster): - ok = False - - if hasattr(e, 'dwell'): - if e.dwell < raster: - ok = False - - if hasattr(e, 'type') and e.type == 'trap': - if not __div_check(e.rise_time, system.grad_raster_time) or \ - not __div_check(e.flat_time, system.grad_raster_time) or \ - not __div_check(e.fall_time, system.grad_raster_time): - ok = False - - if not ok: - is_ok = False - - text_err = '[' - if hasattr(e, 'type'): - text_err += f'type: {e.type} ' - if hasattr(e, 'delay'): - text_err += f'delay: {e.delay * 1e6} us ' - if hasattr(e, 'duration'): - text_err += f'duration: {e.duration * 1e6} us' - if hasattr(e, 'dwell'): - text_err += f'dwell: {e.dwell * 1e9} ns' - if hasattr(e, 'type') and e.type == 'trap': - text_err += f'rise time: {e.rise_time * 1e6} flat time: {e.flat_time * 1e6} ' \ - f'fall time: {e.fall_time * 1e6} us' - text_err += ']' - - return is_ok, text_err, total_duration - - -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 diff --git a/pypulseq/compress_shape.py b/pypulseq/compress_shape.py deleted file mode 100644 index 2d128ae..0000000 --- a/pypulseq/compress_shape.py +++ /dev/null @@ -1,44 +0,0 @@ -from types import SimpleNamespace - -import numpy as np - - -def compress_shape(decompressed_shape: np.ndarray) -> SimpleNamespace: - """ - Returns a run-length encoded compressed shape. - - Parameters - ---------- - decompressed_shape : numpy.ndarray - Decompressed shape. - - Returns - ------- - compressed_shape : SimpleNamespace - A `SimpleNamespace` object containing the compressed data and corresponding shape. - """ - quant_factor = 1e-7 - decompressed_shape_scaled = decompressed_shape / quant_factor - datq = np.round(np.insert(np.diff(decompressed_shape_scaled), 0, decompressed_shape_scaled[0])) - qerr = decompressed_shape_scaled - np.cumsum(datq) - qcor = np.insert(np.diff(np.round(qerr)), 0, 0) - datd = datq + qcor - mask_changes = np.insert(np.asarray(np.diff(datd) != 0, dtype=int), 0, 1) # np.int deprecated - vals = datd[mask_changes.nonzero()[0]] * quant_factor - - k = np.append(mask_changes, 1).nonzero()[0] - n = np.diff(k) - - n_extra = (n - 2).astype(np.float32) # Cast as float for nan assignment to work - vals2 = np.copy(vals) - vals2[n_extra < 0] = np.nan - n_extra[n_extra < 0] = np.nan - v = np.stack((vals, vals2, n_extra)) - v = v.T[np.isfinite(v).T] # Use transposes to match Matlab's Fortran indexing order - v[abs(v) < 1e-10] = 0 - - compressed_shape = SimpleNamespace() - compressed_shape.num_samples = len(decompressed_shape) - compressed_shape.data = v - - return compressed_shape diff --git a/pypulseq/convert.py b/pypulseq/convert.py deleted file mode 100644 index a91b5fd..0000000 --- a/pypulseq/convert.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Iterable, Union - -import numpy as np - - -def convert(from_value: Union[float, Iterable], from_unit: str, to_unit: str = str(), - gamma: float = 42.576e6) -> Union[float, Iterable]: - """" - Converts gradient amplitude or slew rate from unit `from_unit` to unit `to_unit` with gyromagnetic ratio `gamma`. - - Parameters - ---------- - from_value : float - Gradient amplitude or slew rate to convert from. - from_unit : str - Unit of gradient amplitude or slew rate to convert from. - to_unit : str, optional, default='' - Unit of gradient amplitude or slew rate to convert to. - gamma : float, optional, default=42.576e6 - Gyromagnetic ratio. Default is 42.576e6, for Hydrogen. - - Returns - ------- - out : float - Converted gradient amplitude or slew rate. - - Raises - ------ - ValueError - If an invalid `from_unit` is passed. Must be one of 'Hz/m', 'mT/m', or 'rad/ms/mm'. - If an invalid `to_unit` is passed. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms'. - """ - valid_grad_units = ['Hz/m', 'mT/m', 'rad/ms/mm'] - valid_slew_units = ['Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms'] - valid_units = valid_grad_units + valid_slew_units - - if from_unit not in valid_units: - raise ValueError("Invalid from_unit. Must be one of 'Hz/m', 'mT/m', or 'rad/ms/mm' for gradients;" - "or must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms' for slew rate.") - - if to_unit != '' and to_unit not in valid_units: - raise ValueError("Invalid to_unit. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms' for gradients;" - "or must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms' for slew rate..") - - if to_unit == '': - if from_unit in valid_grad_units: - to_unit = valid_grad_units[0] - elif from_unit in valid_slew_units: - to_unit = valid_slew_units[0] - - # Convert to standard units - # Grad units - if from_unit == 'Hz/m': - standard = from_value - elif from_unit == 'mT/m': - standard = from_value * 1e-3 * gamma - elif from_unit == 'rad/ms/mm': - standard = from_value * 1e6 / (2 * np.pi) - # Slew units - elif from_unit == 'Hz/m/s': - standard = from_value - elif from_unit == 'mT/m/ms' or from_unit == 'T/m/s': - standard = from_value * gamma - elif from_unit == 'rad/ms/mm/ms': - standard = from_value * 1e9 / (2 * np.pi) - - # Convert from standard units - # Grad units - if to_unit == 'Hz/m': - out = standard - elif to_unit == 'mT/m': - out = 1e3 * standard / gamma - elif to_unit == 'rad/ms/mm': - out = standard * 2 * np.pi * 1e-6 - # Slew units - elif to_unit == 'Hz/m/s': - out = standard - elif to_unit == 'mT/m/ms' or to_unit == 'T/m/s': - out = standard / gamma - elif to_unit == 'rad/ms/mm/ms': - out = standard * 2 * np.pi * 1e-9 - - return out diff --git a/pypulseq/decompress_shape.py b/pypulseq/decompress_shape.py deleted file mode 100644 index 6d84ed1..0000000 --- a/pypulseq/decompress_shape.py +++ /dev/null @@ -1,39 +0,0 @@ -from types import SimpleNamespace - -import numpy as np - - -def decompress_shape(compressed_shape: SimpleNamespace) -> np.ndarray: - """ - Decompresses a run-length encoded shape. - - Parameters - ---------- - compressed_shape : SimpleNamespace - Run-length encoded shape. - - Returns - ------- - decompressed_shape : numpy.ndarray - Decompressed shape. - """ - data_pack, num_samples = compressed_shape.data, int(compressed_shape.num_samples) - decompressed_shape = np.zeros(num_samples) - - count_pack, count_unpack = 0, 0 - while count_pack < max(data_pack.shape) - 1: - if data_pack[count_pack] != data_pack[count_pack + 1]: - decompressed_shape[count_unpack] = data_pack[count_pack] - count_unpack += 1 - count_pack += 1 - else: - rep = int(data_pack[count_pack + 2] + 2) - decompressed_shape[count_unpack:(count_unpack + rep)] = data_pack[count_pack] - count_pack += 3 - count_unpack += rep - - if count_pack == max(data_pack.shape) - 1: - decompressed_shape[count_unpack] = data_pack[count_pack] - - decompressed_shape = np.cumsum(decompressed_shape) - return decompressed_shape diff --git a/pypulseq/event_lib.py b/pypulseq/event_lib.py deleted file mode 100644 index f69df54..0000000 --- a/pypulseq/event_lib.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import Tuple - -import numpy as np - - -class EventLibrary: - """ - Defines an event library. Provides methods to insert new data and find existing data. - - Attributes - ---------- - keys : dict{str, int} - Key-value pairs of event keys and corresponding... event keys. - data : dict{str: numpy.array} - Key-value pairs of event keys and corresponding data. - lengths : dict{str, int} - Key-value pairs of event keys and corresponding length of data values in `self.data`. - type : dict{str, str} - Key-value pairs of event keys and corresponding event types. - keymap : dict{str, int} - Key-value pairs of data values and corresponding event keys. - """ - - def __init__(self): - self.keys, self.data, self.lengths, self.type, self.keymap = dict(), dict(), dict(), dict(), dict() - - def __str__(self): - s = "EventLibrary:" - s += "\nkeys: " + str(len(self.keys)) - s += "\ndata: " + str(len(self.data)) - s += "\nlengths: " + str(len(self.lengths)) - s += "\ntype: " + str(len(self.type)) - return s - - def find(self, new_data: np.ndarray) -> Tuple[int, bool]: - """ - Finds data `new_data` in event library. - - Parameters - ---------- - new_data : numpy.ndarray - Data to be found in event library. - - Returns - ------- - key_id : int - Key of `new_data` in event library, if found. - found : bool - If `new_data` was found in the event library or not. - """ - new_data = np.array(new_data) - data_string = np.array2string(new_data, formatter={'float': lambda x: f'{x:.6g}'}) - data_string = data_string.replace('[', '') - data_string = data_string.replace(']', '') - try: - key_id = self.keymap[data_string] - found = True - except KeyError: - key_id = 1 if len(self.keys) == 0 else max(self.keys) + 1 - found = False - - return key_id, found - - def insert(self, key_id: int, new_data: np.ndarray, data_type: str = str()) -> None: - """ - Inserts `new_data` of data type `data_type` into the event library with key `key_id`. - - Parameters - ---------- - key_id : int - Key of `new_data`. - new_data : numpy.ndarray - Data to be inserted into event library. - data_type : str, default=str() - Data type of `new_data`. - """ - new_data = np.array(new_data) - self.keys[key_id] = key_id - self.data[key_id] = new_data - self.lengths[key_id] = max(new_data.shape) - data_string = np.array2string(new_data, formatter={'float_kind': lambda x: "%.6g" % x}) - data_string = data_string.replace('[', '') - data_string = data_string.replace(']', '') - self.keymap[data_string] = key_id - if data_type != '': - self.type[key_id] = data_type - - def get(self, key_id: int): - return {'key': self.keys[key_id], 'data': self.data[key_id], 'length': self.lengths[key_id], - 'type': self.type[key_id]} diff --git a/pypulseq/make_adc.py b/pypulseq/make_adc.py deleted file mode 100644 index 2505846..0000000 --- a/pypulseq/make_adc.py +++ /dev/null @@ -1,56 +0,0 @@ -from types import SimpleNamespace - -from pypulseq.opts import Opts - - -def make_adc(num_samples: int, system: Opts = Opts(), dwell: float = 0, duration: float = 0, delay: float = 0, - freq_offset: float = 0, phase_offset: float = 0) -> SimpleNamespace: - """ - Creates an ADC readout event. - - Parameters - ---------- - num_samples: int - Number of readout samples. - system : Opts, optional, default=Opts() - System limits. Default is a system limits object initialised to default values. - dwell : float, optional, default=0 - ADC dead time in milliseconds (ms) after sampling. - duration : float, optional, default=0 - Duration in milliseconds (ms) of ADC readout event with `num_samples` number of samples. - delay : float, optional, default=0 - Delay in milliseconds (ms) of ADC readout event. - freq_offset : float, optional, default=0 - Frequency offset of ADC readout event. - phase_offset : float, optional, default=0 - Phase offset of ADC readout event. - - Returns - ------- - adc : SimpleNamespace - ADC readout event. - - Raises - ------ - ValueError - If neither `dwell` nor `duration` are defined. - """ - 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.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 duration > 0: - adc.dwell = duration / num_samples - - if dwell > 0: - adc.duration = dwell * num_samples - - return adc diff --git a/pypulseq/make_arbitrary_grad.py b/pypulseq/make_arbitrary_grad.py deleted file mode 100644 index c58a844..0000000 --- a/pypulseq/make_arbitrary_grad.py +++ /dev/null @@ -1,67 +0,0 @@ -from types import SimpleNamespace - -import numpy as np - -from pypulseq.opts import Opts - - -def make_arbitrary_grad(channel: str, waveform: np.ndarray, system: Opts = Opts(), max_grad: float = 0, - max_slew: float = 0, delay: float = 0) -> SimpleNamespace: - """ - Creates a gradient event with arbitrary waveform. - - Parameters - ---------- - channel : str - Orientation of gradient event of arbitrary shape. Must be one of `x`, `y` or `z`. - waveform : numpy.ndarray - Arbitrary waveform. - system : Opts, optional, default=Opts() - System limits. - max_grad : float, optional, default=0 - Maximum gradient strength. - max_slew : float, optional, default=0 - Maximum slew rate. - delay : float, optional, default=0 - Delay in milliseconds (ms). - - Returns - ------- - grad : SimpleNamespace - Gradient event with arbitrary waveform. - - Raises - ------ - ValueError - If invalid `channel` is passed. Must be one of x, y or z. - If slew rate is violated. - If gradient amplitude is violated. - """ - 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 <= 0: - max_grad = system.max_grad - - if max_slew <= 0: - max_slew = system.max_slew - - g = waveform - slew = np.squeeze(np.subtract(g[1:], g[:-1]) / system.grad_raster_time) - if max(abs(slew)) >= max_slew: - raise ValueError(f'Slew rate violation {max(abs(slew)) / max_slew * 100}') - if max(abs(g)) >= max_grad: - raise ValueError(f'Gradient amplitude violation {max(abs(g)) / max_grad * 100}') - - grad = SimpleNamespace() - grad.type = 'grad' - grad.channel = channel - grad.waveform = g - grad.delay = delay - grad.t = np.arange(len(g)) * system.grad_raster_time - # True timing and aux shape data - grad.tt = (np.arange(1, len(g) + 1) - 0.5) * system.grad_raster_time - grad.first = (3 * g[0] - g[1]) * 0.5 # Extrapolate by 1/2 gradient rasters - grad.last = (g[-1] * 3 - g[-2]) * 0.5 # Extrapolate by 1/2 gradient rasters - - return grad diff --git a/pypulseq/make_arbitrary_rf.py b/pypulseq/make_arbitrary_rf.py deleted file mode 100644 index 350770c..0000000 --- a/pypulseq/make_arbitrary_rf.py +++ /dev/null @@ -1,124 +0,0 @@ -import math -from types import SimpleNamespace -from typing import Tuple, Union - -import numpy as np - -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - - -def make_arbitrary_rf(signal: np.ndarray, flip_angle: float, bandwidth: float = 0, delay: float = 0, - freq_offset: float = 0, max_grad: float = 0, max_slew: float = 0, phase_offset: float = 0, - return_gz: bool = False, slice_thickness: float = 0, system: Opts = Opts(), - time_bw_product: float = 0, - use: str = str()) -> Union[SimpleNamespace, Tuple[SimpleNamespace, SimpleNamespace]]: - """ - Creates a radio-frequency pulse event with arbitrary pulse shape and optionally an accompanying slice select - trapezoidal gradient event. - - Parameters - ---------- - signal : numpy.ndarray - Arbitrary waveform. - flip_angle : float - Flip angle in radians. - bandwidth : float, default=0 - Bandwidth in Hertz (Hz). - delay : float, default=0 - Delay in milliseconds (ms) of accompanying slice select trapezoidal event. - freq_offset : float, default=0 - Frequency offset in Hertz (Hz). - max_grad : float, default=system.max_grad - Maximum gradient strength of accompanying slice select trapezoidal event. - max_slew : float, default=system.max_slew - Maximum slew rate of accompanying slice select trapezoidal event. - phase_offset : float, default=0 - Phase offset in Hertz (Hz).a - return_gz : bool, default=False - Boolean flag to indicate if slice-selective gradient has to be returned. - slice_thickness : float, default=0 - Slice thickness of accompanying slice select trapezoidal event. The slice thickness determines the area of the - slice select event. - system : Opts, default=Opts() - System limits. - time_bw_product : float, default=4 - Time-bandwidth product. - use : str, default=str() - Use of arbitrary radio-frequency pulse event. Must be one of 'excitation', 'refocusing' or 'inversion'. - - Returns - ------- - rf : SimpleNamespace - Radio-frequency pulse event with arbitrary pulse shape. - gz : SimpleNamespace, if return_gz=True - Slice select trapezoidal gradient event accompanying the arbitrary radio-frequency pulse event. - - Raises - ------ - ValueError - If invalid `use` parameter is passed. Must be one of 'excitation', 'refocusing' or 'inversion'. - If `signal` with ndim > 1 is passed. - If `return_gz=True`, and `slice_thickness` and `bandwith` are not passed. - """ - valid_use_pulses = ['excitation', 'refocusing', 'inversion'] - 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}") - - signal = np.squeeze(signal) - if signal.ndim > 1: - raise ValueError(f'signal should have ndim=1. Passed ndim={signal.ndim}') - 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 - t = np.arange(1, N + 1) * system.rf_raster_time - - rf = SimpleNamespace() - rf.type = 'rf' - rf.signal = signal - rf.t = t - rf.freq_offset = freq_offset - rf.phase_offset = phase_offset - rf.dead_time = system.rf_dead_time - rf.ringdown_time = system.rf_ringdown_time - rf.delay = delay - - if use != '': - rf.use = use - - if rf.dead_time > rf.delay: - rf.delay = rf.dead_time - - if return_gz: - if slice_thickness <= 0: - raise ValueError('Slice thickness must be provided.') - if bandwidth <= 0: - raise ValueError('Bandwidth of pulse must be provided.') - - if max_grad > 0: - system.max_grad = max_grad - if max_slew > 0: - system.max_slew = max_slew - - BW = bandwidth - if time_bw_product > 0: - BW = time_bw_product / duration - - amplitude = BW / slice_thickness - area = amplitude * duration - gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) - - if rf.delay > gz.rise_time: - gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time - - if rf.delay < (gz.rise_time + gz.delay): - rf.delay = gz.rise_time + gz.delay - - if rf.ringdown_time > 0: - t_fill = np.arange(1, round(rf.ringdown_time / 1e-6) + 1) * 1e-6 - 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 diff --git a/pypulseq/make_block_pulse.py b/pypulseq/make_block_pulse.py deleted file mode 100644 index f527b97..0000000 --- a/pypulseq/make_block_pulse.py +++ /dev/null @@ -1,124 +0,0 @@ -import math -from types import SimpleNamespace -from typing import Tuple, Union - -import numpy as np - -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - - -def make_block_pulse(flip_angle: float, bandwidth: float = 0, delay: float = 0, duration: float = 0, - freq_offset: float = 0, max_grad: float = 0, max_slew: float = 0, phase_offset: float = 0, - return_gz: bool = False, system: Opts = Opts(), slice_thickness: float = 0, - time_bw_product: float = 0, - use: str = str()) -> Union[SimpleNamespace, Tuple[SimpleNamespace, SimpleNamespace]]: - """ - Creates a radio-frequency block pulse event and optionally an accompanying slice select trapezoidal gradient event. - - Parameters - ---------- - flip_angle : float - Flip angle in radians. - bandwidth : float, default=0 - Bandwidth in Hertz (hz). - delay : float, default=0 - Delay in milliseconds (ms) of accompanying slice select trapezoidal event. - duration : float, default=0 - Duration in milliseconds (ms). - freq_offset : float, default=0 - Frequency offset in Hertz (Hz). - max_grad : float, default=0 - Maximum gradient strength of accompanying slice select trapezoidal event. - max_slew : float, default=0 - Maximum slew rate of accompanying slice select trapezoidal event. - phase_offset : float, default=0 - Phase offset Hertz (Hz). - return_gz : bool, default=False - Boolean flag to indicate if slice-selective gradient has to be returned. - slice_thickness : float, default=0 - Slice thickness of accompanying slice select trapezoidal event. The slice thickness determines the area of the - slice select event. - system : Opts, default=Opts() - System limits. - time_bw_product : float, default=0 - Time-bandwidth product. - use : str, default=str() - Use of radio-frequency block pulse event. Must be one of 'excitation', 'refocusing' or 'inversion'. - - Returns - ------- - rf : SimpleNamespace - Radio-frequency block pulse event. - gz : SimpleNamespace - Slice select trapezoidal gradient event accompanying the radio-frequency block pulse event. - - Raises - ------ - ValueError - If invalid `use` parameter is passed. Must be one of 'excitation', 'refocusing' or 'inversion'. - If neither `bandwidth` nor `duration` are passed. - If `return_gz=True`, and `slice_thickness` is not passed. - """ - valid_use_pulses = ['excitation', 'refocusing', 'inversion'] - 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}") - - if duration == 0: - if time_bw_product > 0: - duration = time_bw_product / bandwidth - elif bandwidth > 0: - duration = 1 / (4 * bandwidth) - else: - raise ValueError('Either bandwidth or duration must be defined') - - BW = 1 / (4 * duration) - N = round(duration / 1e-6) - t = np.arange(1, N + 1) * system.rf_raster_time - signal = flip_angle / (2 * np.pi) / duration * np.ones(len(t)) - - rf = SimpleNamespace() - rf.type = 'rf' - rf.signal = signal - rf.t = t - rf.freq_offset = freq_offset - rf.phase_offset = phase_offset - rf.dead_time = system.rf_dead_time - rf.ringdown_time = system.rf_ringdown_time - rf.delay = delay - - if use != '': - rf.use = use - - if rf.dead_time > rf.delay: - rf.delay = rf.dead_time - - if return_gz: - if slice_thickness < 0: - raise ValueError('Slice thickness must be provided') - - if max_grad > 0: - system.max_grad = max_grad - if max_slew > 0: - system.max_slew = max_slew - - amplitude = BW / slice_thickness - area = amplitude * duration - gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) - - if rf.delay > gz.rise_time: - gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time - - if rf.delay < (gz.rise_time + gz.delay): - rf.delay = gz.rise_time + gz.delay - - if rf.ringdown_time > 0: - t_fill = np.arange(1, round(rf.ringdown_time / 1e-6) + 1) * 1e-6 - rf.t = np.concatenate((rf.t, (rf.t[-1] + t_fill))) - rf.signal = np.concatenate((rf.signal, np.zeros(len(t_fill)))) - - if return_gz: - return rf, gz - else: - return rf diff --git a/pypulseq/make_delay.py b/pypulseq/make_delay.py deleted file mode 100644 index 59c95ae..0000000 --- a/pypulseq/make_delay.py +++ /dev/null @@ -1,30 +0,0 @@ -import numpy as np -from types import SimpleNamespace - - -def make_delay(d: float) -> SimpleNamespace: - """ - Creates a delay event. - - Parameters - ---------- - d : float - Delay time in milliseconds (ms). - - Returns - ------- - delay : SimpleNamespace - Delay event. - - Raises - ------ - ValueError - If delay is invalid (not finite or < 0). - """ - - delay = SimpleNamespace() - if not np.isfinite(d) or d < 0: - raise ValueError('Delay {:.2f} ms is invalid'.format(d * 1e3)) - delay.type = 'delay' - delay.delay = d - return delay diff --git a/pypulseq/make_digital_output_pulse.py b/pypulseq/make_digital_output_pulse.py deleted file mode 100644 index a095cf5..0000000 --- a/pypulseq/make_digital_output_pulse.py +++ /dev/null @@ -1,44 +0,0 @@ -from types import SimpleNamespace - -from pypulseq.opts import Opts - - -def make_digital_output_pulse(channel: str, delay: float = 0, duration: float = 0, - system: Opts = Opts()) -> SimpleNamespace: - """ - Create a digital output pulse event a.k.a. trigger. Creates an output trigger event on a given channel with optional - given delay and duration. - - Parameters - ---------- - channel : str - Must be one of 'osc0','osc1', or 'ext1'. - delay : float, optional, default=0 - Delay, in millis. - duration : float, optional, default=0 - Duration of trigger event, in millis. - system : Opts, optional, default=Opts() - System limits. - - Returns - ------ - trig : SimpleNamespace - Trigger event. - - Raises - ------ - ValueError - If `channel` is invalid. Must be one of 'osc0','osc1', or 'ext1'. - """ - if channel not in ['osc0', 'osc1', 'ext1']: - raise ValueError(f"Channel {channel} is invalid. Must be one of 'osc0','osc1', or 'ext1'.") - - trig = SimpleNamespace() - trig.type = 'output' - trig.channel = channel - trig.delay = delay - trig.duration = duration - if trig.duration <= system.grad_raster_time: - trig.duration = system.grad_raster_time - - return trig diff --git a/pypulseq/make_extended_trapezoid.py b/pypulseq/make_extended_trapezoid.py deleted file mode 100644 index 88d1c1c..0000000 --- a/pypulseq/make_extended_trapezoid.py +++ /dev/null @@ -1,76 +0,0 @@ -from types import SimpleNamespace -from typing import Iterable - -import numpy as np - -from pypulseq.make_arbitrary_grad import make_arbitrary_grad -from pypulseq.opts import Opts -from pypulseq.points_to_waveform import points_to_waveform - - -def make_extended_trapezoid(channel: str, amplitudes: Iterable = np.zeros(1), max_grad: float = 0, - max_slew: float = 0, system: Opts = Opts(), skip_check: bool = False, - times: Iterable = np.zeros(1)) -> SimpleNamespace: - """ - Creates an extend trapezoidal gradient event by defined by amplitude values in `amplitudes` at time indices in - `times`. - - Parameters - ---------- - channel : str - Orientation of extended trapezoidal gradient event. Must be one of 'x', 'y' or 'z'. - amplitudes : numpy.ndarray, optional, default=09 - Values defined at `times` time indices. - max_grad : float, optional, default=0 - Maximum gradient strength. - max_slew : float, optional, default=0 - Maximum slew rate. - system : Opts, optional, default=Opts() - System limits. - skip_check : bool, optional, default=False - Perform check. - times : numpy.ndarray, optional, default=np.zeros(1) - Time points at which `amplitudes` defines amplitude values. - - Returns - ------- - grad : SimpleNamespace - Extended trapezoid gradient event. - - Raises - ------ - ValueError - If invalid `channel` is passed. Must be one of 'x', 'y' or 'z'. - If all elements in `times` are zero. - If elements in `times` are not in ascending order or not distinct. - If all elements in `amplitudes` are zero. - If first amplitude of a gradient is non-ero and does not connect to a previous block. - """ - if channel not in ['x', 'y', 'z']: - raise ValueError(f"Invalid channel. Must be one of 'x', 'y' or 'z'. Passed: {channel}") - - if not np.any(times): - raise ValueError('At least one of the given times must be non-zero') - - if np.any(np.diff(times) <= 0): - raise ValueError('Times must be in ascending order and all times must be distinct') - - if not np.any(amplitudes): - raise ValueError('At least one of the given amplitudes must be non-zero') - - if skip_check is False and times[0] > 0 and amplitudes[0] != 0: - raise ValueError('If first amplitude of a gradient is non-zero, it must connect to previous block') - - if max_grad <= 0: - max_grad = system.max_grad - - if max_slew <= 0: - max_slew = system.max_slew - - waveform = points_to_waveform(times=times, amplitudes=amplitudes, grad_raster_time=system.grad_raster_time) - grad = make_arbitrary_grad(channel=channel, waveform=waveform, system=system, max_grad=max_grad, max_slew=max_slew, - delay=times[0]) - grad.first = amplitudes[0] - grad.last = amplitudes[-1] - - return grad diff --git a/pypulseq/make_extended_trapezoid_area.py b/pypulseq/make_extended_trapezoid_area.py deleted file mode 100644 index cc2647e..0000000 --- a/pypulseq/make_extended_trapezoid_area.py +++ /dev/null @@ -1,98 +0,0 @@ -import math -from types import SimpleNamespace -from typing import Tuple - -import numpy as np -from scipy.optimize import minimize - -from pypulseq.make_extended_trapezoid import make_extended_trapezoid -from pypulseq.opts import Opts - - -def make_extended_trapezoid_area(channel: str, Gs: float, Ge: float, A: float, - system: Opts) -> Tuple[SimpleNamespace, np.array, np.array]: - """ - Makes shortest possible extended trapezoid with a given area. - - Parameters - ---------- - channel : str - Orientation of extended trapezoidal gradient event. Must be one of 'x', 'y' or 'z'. - Gs : float - Starting non-zero gradient value. - Ge : float - Ending non-zero gradient value. - A : float - Area of extended trapezoid. - system: Opts - System limits. - - Returns - ------- - grad : SimpleNamespace - Extended trapezoid event. - times : numpy.ndarray - amplitude : numpy.ndarray - - Raises - ------ - ValueError - - """ - SR = system.max_slew * 0.99 - - Tp = 0 - obj1 = lambda x: (A - __testGA(x, 0, SR, system.grad_raster_time, Gs, Ge)) ** 2 - res = minimize(fun=obj1, x0=0, method='Nelder-Mead') - Gp, obj1val = *res.x, res.fun - - if obj1val > 1e-3 or abs(Gp) > system.max_grad: # Search did not converge - Gp = system.max_grad * np.sign(Gp) - obj2 = lambda x: (A - __testGA(Gp, x, SR, system.grad_raster_time, Gs, Ge)) ** 2 - res2 = minimize(fun=obj2, x0=0, method='Nelder-Mead') - T, obj2val = *res2.x, res2.fun - assert obj2val < 1e-2 - - Tp = math.ceil(T / system.grad_raster_time) * system.grad_raster_time - - # Fix the ramps - Tru = math.ceil(abs(Gp - Gs) / SR / system.grad_raster_time) * system.grad_raster_time - Trd = math.ceil(abs(Gp - Ge) / SR / system.grad_raster_time) * system.grad_raster_time - obj3 = lambda x: (A - __testGA1(x, Tru, Tp, Trd, Gs, Ge)) ** 2 - - res = minimize(fun=obj3, x0=Gp, method='Nelder-Mead') - Gp, obj3val = *res.x, res.fun - assert obj3val < 1e-3 - - if Tp > 0: - times = np.cumsum([0, Tru, Tp, Trd]) - amplitudes = [Gs, Gp, Gp, Ge] - else: - Tru = math.ceil(abs(Gp - Gs) / SR / system.grad_raster_time) * system.grad_raster_time - Trd = math.ceil(abs(Gp - Ge) / SR / system.grad_raster_time) * system.grad_raster_time - - if Trd > 0: - if Tru > 0: - times = np.cumsum([0, Tru, Trd]) - amplitudes = np.array([Gs, Gp, Ge]) - else: - times = np.cumsum([0, Trd]) - amplitudes = np.array([Gs, Ge]) - else: - times = np.cumsum([0, Tru]) - amplitudes = np.array([Gs, Ge]) - - grad = make_extended_trapezoid(channel=channel, system=system, times=times, amplitudes=amplitudes) - - return grad, times, amplitudes - - -def __testGA(Gp, Tp, SR, dT, Gs, Ge): - Tru = math.ceil(abs(Gp - Gs) / SR / dT) * dT - Trd = math.ceil(abs(Gp - Ge) / SR / dT) * dT - ga = __testGA1(Gp, Tru, Tp, Trd, Gs, Ge) - return ga - - -def __testGA1(Gp, Tru, Tp, Trd, Gs, Ge): - return 0.5 * Tru * (Gp + Gs) + Gp * Tp + 0.5 * (Gp + Ge) * Trd diff --git a/pypulseq/make_gauss_pulse.py b/pypulseq/make_gauss_pulse.py deleted file mode 100644 index 8a7dcc7..0000000 --- a/pypulseq/make_gauss_pulse.py +++ /dev/null @@ -1,140 +0,0 @@ -import math -from types import SimpleNamespace -from typing import Tuple, Union - -import numpy as np - -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - - -def make_gauss_pulse(flip_angle: float, apodization: float = 0, bandwidth: float = 0, center_pos: float = 0.5, - delay: float = 0, duration: float = 0, freq_offset: float = 0, max_grad: float = 0, - max_slew: float = 0, phase_offset: float = 0, return_gz: bool = False, slice_thickness: float = 0, - system: Opts = Opts(), time_bw_product: float = 4, - use: str = str()) -> Union[SimpleNamespace, - Tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace]]: - """ - Creates a radio-frequency gauss pulse event and optionally accompanying slice select and slice select rephasing - trapezoidal gradient events. - - Parameters - ---------- - flip_angle : float - Flip angle in radians. - apodization : float, optional, default=0 - Apodization. - bandwidth : float, optional, default=0 - Bandwidth in Hertz (Hz). - center_pos : float, optional, default=0.5 - Position of peak. - delay : float, optional - Delay in milliseconds (ms). - duration : float, optional, default=0 - Duration in milliseconds (ms). - freq_offset : float, optional, default=0 - Frequency offset in Hertz (Hz). - max_grad : float, optional, default=0 - Maximum gradient strength of accompanying slice select trapezoidal event. - max_slew : float, optional, default=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=False - Boolean flag indicating 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 - slice select event. - system : Opts, optional, default=Opts() - System limits. - time_bw_product : int, optional, default=4 - Time-bandwidth product. - use : str, optional, default=str() - Use of radio-frequency gauss pulse event. Must be one of 'excitation', 'refocusing' or 'inversion'. - - Returns - ------- - rf : SimpleNamespace - Radio-frequency gauss pulse event. - gz : SimpleNamespace - Accompanying slice select trapezoidal gradient event. - gzr : SimpleNamespace - Accompanying slice select rephasing trapezoidal gradient event. - - Raises - ------ - ValueError - If invalid `use` is passed. Must be one of 'excitation', 'refocusing' or 'inversion'. - If `return_gz=True` and `slice_thickness` was not passed. - """ - valid_use_pulses = ['excitation', 'refocusing', 'inversion'] - 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}") - - if bandwidth == 0: - BW = time_bw_product / duration - else: - BW = bandwidth - alpha = apodization - N = int(round(duration / 1e-6)) - t = np.arange(1, N + 1) * system.rf_raster_time - tt = t - (duration * center_pos) - window = 1 - alpha + alpha * np.cos(2 * np.pi * tt / duration) - signal = np.multiply(window, __gauss(BW * tt)) - flip = np.sum(signal) * system.rf_raster_time * 2 * np.pi - signal = signal * flip_angle / flip - - rf = SimpleNamespace() - rf.type = 'rf' - rf.signal = signal - rf.t = t - rf.freq_offset = freq_offset - rf.phase_offset = phase_offset - rf.dead_time = system.rf_dead_time - rf.ringdown_time = system.rf_ringdown_time - rf.delay = delay - if use != '': - rf.use = use - - if rf.dead_time > rf.delay: - rf.delay = rf.dead_time - - if return_gz: - if slice_thickness == 0: - raise ValueError('Slice thickness must be provided') - - if max_grad > 0: - system.max_grad = max_grad - - if max_slew > 0: - system.max_slew = max_slew - - amplitude = BW / slice_thickness - area = amplitude * duration - gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) - gzr = make_trapezoid(channel='z', system=system, area=-area * (1 - center_pos) - 0.5 * (gz.area - area)) - - if rf.delay > gz.rise_time: - gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time - - if rf.delay < (gz.rise_time + gz.delay): - rf.delay = gz.rise_time + gz.delay - - if rf.ringdown_time > 0: - t_fill = np.arange(1, round(rf.ringdown_time / 1e-6) + 1) * 1e-6 - rf.t = np.concatenate((rf.t, rf.t[-1] + t_fill)) - rf.signal = np.concatenate((rf.signal, np.zeros(len(t_fill)))) - - # Following 2 lines of code are workarounds for numpy returning 3.14... for np.angle(-0.00...) - negative_zero_indices = np.where(rf.signal == -0.0) - rf.signal[negative_zero_indices] = 0 - - if return_gz: - return rf, gz, gzr - else: - return rf - - -def __gauss(x): - return np.exp(-np.pi * np.square(x)) diff --git a/pypulseq/make_label.py b/pypulseq/make_label.py deleted file mode 100644 index 814be9c..0000000 --- a/pypulseq/make_label.py +++ /dev/null @@ -1,50 +0,0 @@ -from types import SimpleNamespace -from typing import Union - -from pypulseq import supported_labels - - -def make_label(type: str, label: str, value: Union[bool, float, int]) -> SimpleNamespace: - """ - Parameters - ---------- - type : str - Label type. Must be one of 'SET' or 'INC'. - label : str - Must be one of 'SLC', 'SEG', 'REP', 'AVG', 'SET', 'ECO', 'PHS', 'LIN', 'PAR', 'NAV', 'REV', or 'SMS'. - value : bool, float or int - Label value. - - Returns - ------- - out : SimpleNamespace - Label object. - - Raises - ------ - ValueError - If a valid `label` was not passed. Must be one of 'SLC', 'SEG', 'REP', 'AVG', 'SET', 'ECO', 'PHS', 'LIN', 'PAR', - NAV', 'REV', or 'SMS'. - If a valid `type` was not passed. Must be one of 'SET' or 'INC'. - If `value` was not a valid numerical or logical value. - """ - arr_supported_labels = supported_labels.get_supported_labels() - - if label not in arr_supported_labels: - raise ValueError("Invalid label. Must be one of 'SLC', 'SEG', 'REP', 'AVG', 'SET', 'ECO', 'PHS', 'LIN', 'PAR', " - "NAV', 'REV', or 'SMS'.") - if type not in ['SET', 'INC']: - raise ValueError("Invalid type. Must be one of 'SET' or 'INC'.") - if not isinstance(value, (bool, float, int)): - raise ValueError('Must supply a valid numerical or logical value.') - - out = SimpleNamespace() - if type == 'SET': - out.type = 'labelset' - elif type == 'INC': - out.type = 'labelinc' - - out.label = label - out.value = value - - return out diff --git a/pypulseq/make_sinc_pulse.py b/pypulseq/make_sinc_pulse.py deleted file mode 100644 index 022c6f2..0000000 --- a/pypulseq/make_sinc_pulse.py +++ /dev/null @@ -1,131 +0,0 @@ -import math -from types import SimpleNamespace -from typing import Tuple, Union - -import numpy as np - -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - - -def make_sinc_pulse(flip_angle: float, apodization: float = 0, delay: float = 0, duration: float = 0, - freq_offset: float = 0, center_pos: float = 0.5, max_grad: float = 0, max_slew: float = 0, - phase_offset: float = 0, return_gz: bool = False, slice_thickness: float = 0, system: Opts = Opts(), - time_bw_product: float = 4, use: str = str()) -> Union[SimpleNamespace, - Tuple[SimpleNamespace, SimpleNamespace, - SimpleNamespace]]: - """ - Creates a radio-frequency sinc pulse event and optionally accompanying slice select and slice select rephasing - trapezoidal gradient events. - - Parameters - ---------- - flip_angle : float - Flip angle in radians. - apodization : float, optional, default=0 - Apodization. - center_pos : float, optional, default=0.5 - Position of peak.5 (midway). - delay : float, optional, default=0 - Delay in milliseconds (ms). - duration : float, optional, default=0 - Duration in milliseconds (ms). - freq_offset : float, optional, default=0 - Frequency offset in Hertz (Hz). - max_grad : float, optional, default=0 - Maximum gradient strength of accompanying slice select trapezoidal event. - max_slew : float, optional, default=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=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 - slice select event. - system : Opts, optional - System limits. Default is a system limits object initialised to default values. - time_bw_product : float, optional, default=4 - Time-bandwidth product. - use : str, optional, default=str() - Use of radio-frequency sinc pulse. Must be one of 'excitation', 'refocusing' or 'inversion'. - - Returns - ------- - rf : SimpleNamespace - Radio-frequency sinc pulse event. - gz : SimpleNamespace, optional - Accompanying slice select trapezoidal gradient event. Returned only if `slice_thickness` is provided. - gzr : SimpleNamespace, optional - Accompanying slice select rephasing trapezoidal gradient event. Returned only if `slice_thickness` is provided. - - Raises - ------ - ValueError - If invalid `use` parameter was passed. Must be one of 'excitation', 'refocusing' or 'inversion'. - If `return_gz=True` and `slice_thickness` was not provided. - """ - valid_use_pulses = ['excitation', 'refocusing', 'inversion'] - 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}") - - BW = time_bw_product / duration - alpha = apodization - N = int(round(duration / 1e-6)) - t = np.arange(1, N + 1) * system.rf_raster_time - tt = t - (duration * center_pos) - window = 1 - alpha + alpha * np.cos(2 * np.pi * tt / duration) - signal = np.multiply(window, np.sinc(BW * tt)) - flip = np.sum(signal) * system.rf_raster_time * 2 * np.pi - signal = signal * flip_angle / flip - - rf = SimpleNamespace() - rf.type = 'rf' - rf.signal = signal - rf.t = t - rf.freq_offset = freq_offset - rf.phase_offset = phase_offset - rf.dead_time = system.rf_dead_time - rf.ringdown_time = system.rf_ringdown_time - rf.delay = delay - if use != '': - rf.use = use - - if rf.dead_time > rf.delay: - rf.delay = rf.dead_time - - if return_gz: - if slice_thickness == 0: - raise ValueError('Slice thickness must be provided') - - if max_grad > 0: - system.max_grad = max_grad - - if max_slew > 0: - system.max_slew = max_slew - - amplitude = BW / slice_thickness - area = amplitude * duration - gz = make_trapezoid(channel='z', system=system, flat_time=duration, flat_area=area) - gzr = make_trapezoid(channel='z', system=system, area=-area * (1 - center_pos) - 0.5 * (gz.area - area)) - - if rf.delay > gz.rise_time: - gz.delay = math.ceil((rf.delay - gz.rise_time) / system.grad_raster_time) * system.grad_raster_time - - if rf.delay < (gz.rise_time + gz.delay): - rf.delay = gz.rise_time + gz.delay - - if rf.ringdown_time > 0: - t_fill = np.arange(1, round(rf.ringdown_time / 1e-6) + 1) * 1e-6 - rf.t = np.concatenate((rf.t, rf.t[-1] + t_fill)) - rf.signal = np.concatenate((rf.signal, np.zeros(len(t_fill)))) - - # Following 2 lines of code are workarounds for numpy returning 3.14... for np.angle(-0.00...) - negative_zero_indices = np.where(rf.signal == -0.0) - rf.signal[negative_zero_indices] = 0 - - if return_gz: - return rf, gz, gzr - else: - return rf diff --git a/pypulseq/make_trap_pulse.py b/pypulseq/make_trap_pulse.py deleted file mode 100644 index 351ac95..0000000 --- a/pypulseq/make_trap_pulse.py +++ /dev/null @@ -1,136 +0,0 @@ -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 = -1, max_grad: float = 0, max_slew: float = 0, - rise_time: float = 0, system: Opts = Opts()) -> SimpleNamespace: - """ - Creates a trapezoidal gradient event. - - Parameters - ---------- - channel : str, optional - Orientation of trapezoidal gradient event. Must be one of `x`, `y` or `z`. - amplitude : float, optional, default=0 - Amplitude. - area : float, optional, default=None - Area. - delay : float, optional, default=0 - Delay in milliseconds (ms). - duration : float, optional, default=0 - Duration in milliseconds (ms). - flat_area : float, optional, default=0 - Flat area. - 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 - Maximum slew rate. - rise_time : float, optional, default=0 - Rise time in milliseconds (ms). - system : Opts, optional, 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 - """ - 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 <= 0: - max_grad = system.max_grad - - if max_slew <= 0: - max_slew = system.max_slew - - if rise_time <= 0: - rise_time = system.rise_time - - if area is None and flat_area == 0 and amplitude == 0: - raise ValueError("Must supply either 'area', 'flat_area' or 'amplitude'.") - - if flat_time != -1: - if amplitude != 0: - amplitude2 = amplitude - else: - amplitude2 = flat_area / flat_time - - if rise_time == 0: - rise_time = abs(amplitude2) / max_slew - rise_time = math.ceil(rise_time / system.grad_raster_time) * system.grad_raster_time - fall_time, flat_time = rise_time, flat_time - elif duration > 0: - amplitude2 = amplitude - if amplitude == 0: - if rise_time == 0: - dC = 1 / abs(2 * max_slew) + 1 / abs(2 * max_slew) - possible = duration ** 2 > 4 * abs(area) * dC - amplitude2 = (duration - math.sqrt(duration ** 2 - 4 * abs(area) * dC)) / (2 * dC) - else: - amplitude2 = area / (duration - rise_time) - possible = duration > 2 * rise_time and abs(amplitude2) < max_grad - - if not possible: - raise ValueError('Requested area is too large for this gradient') - - if rise_time == 0: - rise_time = math.ceil( - abs(amplitude2) / max_slew / system.grad_raster_time) * system.grad_raster_time - if rise_time == 0: - rise_time = system.grad_raster_time - - fall_time = rise_time - flat_time = duration - rise_time - fall_time - - if amplitude == 0: - amplitude2 = area / (rise_time / 2 + fall_time / 2 + flat_time) - else: - 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 = np.divide(area, rise_time) # To handle nan - t_eff = rise_time - - if abs(amplitude2) > max_grad: - t_eff = math.ceil(abs(area) / max_grad / system.grad_raster_time) * system.grad_raster_time - amplitude2 = area / t_eff - rise_time = math.ceil( - abs(amplitude2) / max_slew / system.grad_raster_time) * system.grad_raster_time - - flat_time = t_eff - rise_time - fall_time = rise_time - - if abs(amplitude2) > max_grad: - raise ValueError("Amplitude violation.") - - 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 - - return grad diff --git a/pypulseq/make_trigger.py b/pypulseq/make_trigger.py deleted file mode 100644 index 9a220b8..0000000 --- a/pypulseq/make_trigger.py +++ /dev/null @@ -1,45 +0,0 @@ -# inserted for trigger support by mveldmann - -from types import SimpleNamespace - -from pypulseq.opts import Opts - - -def make_trigger(channel: str, delay: float = 0, duration: float = 0, system: Opts = Opts()) -> SimpleNamespace: - """ - Creates a trigger event. - - Parameters - ---------- - channel : str - Must be one of 'physio1' or 'physio2'. - delay : float, default=0 - Delay in seconds - duration: float, default=0 - Duration in seconds. - system : Opts, default=Opts() - System limits. - - Returns - ------- - trigger : SimpleNamespace - Trigger event. - - Raises - ------ - ValueError - If invalid `channel` is passed. Must be one of 'physio1' or 'physio2'. - """ - - if channel not in ['physio1', 'physio2']: - raise ValueError(f"Channel {channel} is invalid. Must be one of 'physio1' or 'physio2'.") - - trigger = SimpleNamespace() - trigger.type = 'trigger' - trigger.channel = channel - trigger.delay = delay - trigger.duration = duration - if trigger.duration <= system.grad_raster_time: - trigger.duration = system.grad_raster_time - - return trigger diff --git a/pypulseq/opts.py b/pypulseq/opts.py deleted file mode 100644 index ded3b78..0000000 --- a/pypulseq/opts.py +++ /dev/null @@ -1,89 +0,0 @@ -from pypulseq.convert import convert - - -class Opts: - """ - System limits of an MR scanner. - - Attributes - ---------- - adc_dead_time : float, default=0 - Dead time for ADC readout pulses. - gamma : float, default=42.576e6 - Gyromagnetic ratio. Default gamma is specified for Hydrogen. - grad_raster_time : float, default=10e-6 - Raster time for gradient waveforms. - grad_unit : str, default='Hz/m' - Unit of maximum gradient amplitude. Must be one of 'Hz/m', 'mT/m' or 'rad/ms/mm'. - max_grad : float, default=0 - Maximum gradient amplitude. - max_slew : float, default=0 - Maximum slew rate. - rf_dead_time : float, default=0 - Dead time for radio-frequency pulses. - rf_raster_time : float, default=1e-6 - Raster time for radio-frequency pulses. - rf_ringdown_time : float, default=0 - Ringdown time for radio-frequency pulses. - rise_time : float, default=0 - Rise time for gradients. - slew_unit : str, default='Hz/m/s' - Unit of maximum slew rate. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s' or 'rad/ms/mm/ms'. - - Raises - ------ - ValueError - If invalid `grad_unit` is passed. Must be one of 'Hz/m', 'mT/m' or 'rad/ms/mm'. - If invalid `slew_unit` is passed. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s' or 'rad/ms/mm/ms'. - """ - - def __init__(self, adc_dead_time: float = 0, gamma: float = 42.576e6, grad_raster_time: float = 10e-6, - grad_unit: str = 'Hz/m', max_grad: float = 0, max_slew: float = 0, rf_dead_time: float = 0, - rf_raster_time: float = 1e-6, rf_ringdown_time: float = 0, rise_time: float = 0, - slew_unit: str = 'Hz/m/s'): - valid_grad_units = ['Hz/m', 'mT/m', 'rad/ms/mm'] - valid_slew_units = ['Hz/m/s', 'mT/m/ms', 'T/m/s', 'rad/ms/mm/ms'] - - if grad_unit not in valid_grad_units: - raise ValueError(f"Invalid gradient unit. Must be one of 'Hz/m', 'mT/m' or 'rad/ms/mm'. " - f"Passed: {grad_unit}") - - if slew_unit not in valid_slew_units: - raise ValueError(f"Invalid slew rate unit. Must be one of 'Hz/m/s', 'mT/m/ms', 'T/m/s' or 'rad/ms/mm/ms'. " - f"Passed: {slew_unit}") - - if max_grad == 0: - max_grad = convert(from_value=40, from_unit='mT/m', gamma=gamma) - else: - max_grad = convert(from_value=max_grad, from_unit=grad_unit, to_unit='Hz/m', gamma=gamma) - - if max_slew == 0: - max_slew = convert(from_value=170, from_unit='T/m/s', gamma=gamma) - else: - max_slew = convert(from_value=max_slew, from_unit=slew_unit, to_unit='Hz/m', gamma=gamma) - - if rise_time != 0: - max_slew = 0 - - self.max_grad = max_grad - self.max_slew = max_slew - self.rise_time = rise_time - self.rf_dead_time = rf_dead_time - self.rf_ringdown_time = rf_ringdown_time - self.adc_dead_time = adc_dead_time - self.rf_raster_time = rf_raster_time - self.grad_raster_time = grad_raster_time - self.gamma = gamma - - def __str__(self): - s = "System limits:" - s += "\nmax_grad: " + str(self.max_grad) + str(self.grad_unit) - s += "\nmax_slew: " + str(self.max_slew) + str(self.slew_unit) - s += "\nrise_time: " + str(self.rise_time) - s += "\nrf_dead_time: " + str(self.rf_dead_time) - s += "\nrf_ring_time: " + str(self.rf_ringdown_time) - s += "\nadc_dead_time: " + str(self.adc_dead_time) - s += "\nrf_raster_time: " + str(self.rf_raster_time) - s += "\ngrad_raster_time: " + str(self.grad_raster_time) - s += "\ngamma: " + str(self.gamma) - return s diff --git a/pypulseq/points_to_waveform.py b/pypulseq/points_to_waveform.py deleted file mode 100644 index c29e039..0000000 --- a/pypulseq/points_to_waveform.py +++ /dev/null @@ -1,27 +0,0 @@ -import numpy as np - - -def points_to_waveform(amplitudes: np.ndarray, grad_raster_time: float, times: np.ndarray) -> np.ndarray: - """ - 1D interpolate amplitude values `amplitudes` at time indices `times` as per the gradient raster time - `grad_raster_time` to generate a gradient waveform. - - Parameters - ---------- - amplitudes : numpy.ndarray - Amplitude values at time indices `times`. - grad_raster_time : float - Gradient raster time. - times : numpy.ndarray - Time indices. - - Returns - ------- - waveform : numpy.ndarray - Gradient waveform. - """ - grd = np.arange(start=round(min(times) / grad_raster_time), - stop=round(max(times) / grad_raster_time)) * grad_raster_time - waveform = np.interp(x=grd + grad_raster_time / 2, xp=times, fp=amplitudes) - - return waveform diff --git a/pypulseq/seq2prospa/__init__.py b/pypulseq/seq2prospa/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pypulseq/seq2prospa/convert_seq.py b/pypulseq/seq2prospa/convert_seq.py deleted file mode 100644 index f0849b6..0000000 --- a/pypulseq/seq2prospa/convert_seq.py +++ /dev/null @@ -1,122 +0,0 @@ -from types import SimpleNamespace - -import numpy as np - -from pypulseq.Sequence.sequence import Sequence - - -def main(seq: Sequence): - prospa = str() - dict_grad_codes = {'x': 'n1', 'y': 'n3', 'z': 'n5'} - dict_grad_shims = {'x': 'n10', 'y': 'n8', 'z': 'n6'} - - prospa += "initpp(dir)\n" - prospa += "gradon(n1, n10)\n" - prospa += "gradon(n3, n8)\n" - prospa += "gradon(n5, n6)\n" - prospa += "delay(d11)\n" - dict_grad_latest_amp = {'x': 'n10', 'y': 'n8', 'z': 'n6'} - dict_grad_new_amp = {'x': 'n2', 'y': 'n4', 'z': 'n6'} - - rf_flips = [] - - for block_counter in range(1, len(seq.dict_block_events) + 1): - block = seq.get_block(block_counter) - attributes = set(dir(block)) - set(dir(SimpleNamespace)) - events = np.array([getattr(block, attr) for attr in attributes]) - ordered_delays = np.array([e.delay for e in events]) - sort_idx = np.argsort(ordered_delays) - ordered_events = events[sort_idx] - - arr_grad_duration = np.zeros(len(ordered_events)) - dict_grad_duration_remaining = dict() - open_grads = [] - time_elapsed = 0 - - for j in range(len(ordered_events)): - event = ordered_events[j] - - delay = event.delay # initial delay - if delay > 0 and delay > time_elapsed: - delay = delay - time_elapsed - delay = delay * 1e6 # Prospa has delay in nanoseconds; minimum is 250 ns - prospa += f'delay({delay})' - prospa += '\n' - - if event.type == 'trap': - latest_amp = dict_grad_latest_amp[event.channel] - new_amp = dict_grad_new_amp[event.channel] - - # Cycle through remaining events in this block and see if there is an ADC - # If yes, we want to gradramp to a special amplitude - idx_to_check = set(range(len(ordered_events))) - {j} - for new_j in idx_to_check: - new_event = ordered_events[new_j] - if new_event.type == 'adc': - new_amp = 'n9' - - prospa += f'gradramp({dict_grad_codes[event.channel]}, ' \ - f'{latest_amp}, ' \ - f'{new_amp}, ' \ - f'n12, d12)' - prospa += '\n' - arr_grad_duration[j] = event.rise_time + event.flat_time + event.fall_time - dict_grad_latest_amp[event.channel] = new_amp - dict_grad_new_amp[event.channel] = latest_amp - - # If we are in the last event and not in the last block in this iteration, then we add delay statement - if j == len(ordered_events) - 1 and block_counter - 1 != len(seq.dict_block_events) - 1: - grad_duration = arr_grad_duration.min() - grad_duration = grad_duration * 1e6 # See earlier comment about delays - prospa += f'delay({grad_duration})' - prospa += '\n' - - dict_grad_duration_remaining[event.channel] = arr_grad_duration.max() - arr_grad_duration.min() - - open_grads.append(event.channel) - elif event.type == 'rf': - signal = event.signal - if not any([np.all(s == signal) for s in rf_flips]): # Have we encountered this RF before? - rf_flips.append(signal) - # Hardcoded as d1 after meeting with Tom on 12/10/2020 because it is a GUI-driven param - duration = event.t[-1] - prospa += f'pulse(mode, a{len(rf_flips)}, p{len(rf_flips)}, d1)' - prospa += '\n' - time_elapsed += duration - elif event.type == 'adc': - num_samples = event.num_samples - prospa += f'acquire("overwrite", {num_samples})' - prospa += '\n' - - for g in open_grads: - if g not in dict_grad_duration_remaining: - latest_amp = dict_grad_latest_amp[g] - new_amp = dict_grad_shims[g] - prospa += f'gradramp({dict_grad_codes[g]}, ' \ - f'{latest_amp}, ' \ - f'{new_amp}, ' \ - f'n12, d12)' - dict_grad_latest_amp[g] = new_amp - dict_grad_new_amp[g] = latest_amp - prospa += '\n' - - for g in open_grads: - if g in dict_grad_duration_remaining: - grad_duration = dict_grad_duration_remaining[g] - grad_duration = grad_duration * 1e6 # See earlier comment about delays - prospa += f'delay({grad_duration})' - prospa += '\n' - - dict_grad_duration_remaining = dict() # clear dictionary - - latest_amp = dict_grad_latest_amp[g] - new_amp = dict_grad_shims[g] - prospa += f'gradramp({dict_grad_codes[g]}, ' \ - f'{latest_amp}, ' \ - f'{new_amp}, ' \ - f'n12, d12)' - dict_grad_latest_amp[g] = new_amp - dict_grad_new_amp[g] = latest_amp - prospa += '\n' - - return prospa diff --git a/pypulseq/seq2prospa/make_gre.py b/pypulseq/seq2prospa/make_gre.py deleted file mode 100644 index e12203c..0000000 --- a/pypulseq/seq2prospa/make_gre.py +++ /dev/null @@ -1,52 +0,0 @@ -import math - -import numpy as np - -from pypulseq.Sequence.sequence import Sequence -from pypulseq.make_adc import make_adc -from pypulseq.make_delay import make_delay -from pypulseq.make_sinc_pulse import make_sinc_pulse -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - - -def main(): - seq = Sequence() - fov = 250e-3 - Nx = 64 - Ny = 64 - alpha = 10 - slice_thickness = 3e-3 - - d1 = 4e-3 # RF duration - d2 = 2e-3 - (250 * 1e-6) # Gy duration - d3 = 2e-3 - (250 * 1e-6) # Subtract grad ramp time from Prospa GUI - adc_dwell = 50 * 1e-6 - delta_k = 1 / fov - - sys = Opts(max_grad=28, grad_unit='mT/m', max_slew=150, slew_unit='T/m/s', rf_ringdown_time=20e-6, - rf_dead_time=0, adc_dead_time=10e-6) - - rf = make_sinc_pulse(flip_angle=alpha * math.pi / 180, duration=d1, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, system=sys, return_gz=False) - - phase_areas = (np.arange(Ny) - Ny / 2) * delta_k - gx1 = make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=d2, system=sys, rise_time=0.25e-3) - gy_pre = make_trapezoid(channel='y', area=phase_areas[0], duration=d2, system=sys, rise_time=0.25e-3) - - gx2 = make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=6.4e-3, rise_time=0.25e-3, system=sys) - d4 = (d2 + (2 * 250 * 1e-6)) - (0.5 * Ny * adc_dwell) - (0.5 * gx2.rise_time) # ADC delay - adc = make_adc(num_samples=Nx, duration=gx2.flat_time - d3, delay=d4, system=sys) - - seq.add_block(rf) - seq.add_block(gy_pre, gx1) - seq.add_block(make_delay(d3)) - seq.add_block(gx2, adc) - - # seq.plot() - - return seq - - -if __name__ == '__main__': - main() diff --git a/pypulseq/seq2prospa/make_se.py b/pypulseq/seq2prospa/make_se.py deleted file mode 100644 index 9b7b1db..0000000 --- a/pypulseq/seq2prospa/make_se.py +++ /dev/null @@ -1,74 +0,0 @@ -import math - -import numpy as np - -from pypulseq.Sequence.sequence import Sequence -from pypulseq.make_adc import make_adc -from pypulseq.make_delay import make_delay -from pypulseq.make_sinc_pulse import make_sinc_pulse -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - - -def main(): - seq = Sequence() - fov = 250e-3 - Nx = 128 - Ny = 128 - alpha = 10 - alpha180 = 180 - slice_thickness = 3e-3 - - """ - n7 = 128 - gradRamp = 250 us - acqTime = 128 * 50 us - pulseLength = 100 us - pulseAmplitude = -18 - """ - acqTime = Nx * 1e-6 - nrPnts = Nx - bandWidth = acqTime / (nrPnts * 1000) - gradAmpDelay = 43 * 1e-6 - gradRamp = 250e-6 - eddyCurrentCorr = (bandWidth / 10000 - 0.5) * acqTime * 1000 / nrPnts - d1 = 100e-6 - d2 = acqTime * 500 - (2 * gradRamp) - d3 = d2 + (gradRamp / 2) - d4 = d3 # echoTime / 2 - d1 - d2 - d3 - 4 * gradRamp - pgo - d5 = 2e-3 - ( - 250 * 1e-6) # echoTime / 2 - d1 / 2 + rxLat - acqTime * 500 - gradRamp - gradAmpDelay - eddyCurrentCorr - d6 = gradAmpDelay + eddyCurrentCorr - - adc_dwell = 50 * 1e-6 - delta_k = 1 / fov - - sys = Opts(max_grad=28, grad_unit='mT/m', max_slew=150, slew_unit='T/m/s', rf_ringdown_time=20e-6, - rf_dead_time=0, adc_dead_time=10e-6) - - rf = make_sinc_pulse(flip_angle=alpha * math.pi / 180, duration=d1, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, system=sys, return_gz=False) - rf2 = make_sinc_pulse(flip_angle=alpha180 * math.pi / 180, duration=d1, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, system=sys, return_gz=False) - - phase_areas = (np.arange(Ny) - Ny / 2) * delta_k - gx1 = make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=d3, system=sys, rise_time=0.25e-3) - gy_pre = make_trapezoid(channel='y', area=phase_areas[0], duration=d2, system=sys, rise_time=0.25e-3) - - gx2 = make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=d6, rise_time=0.25e-3, system=sys) - adc = make_adc(num_samples=Nx, duration=Nx * adc_dwell, system=sys) - - seq.add_block(rf) - seq.add_block(gx1, gy_pre) - seq.add_block(make_delay(d4)) - seq.add_block(rf2) - seq.add_block(make_delay(d5)) - seq.add_block(gx2, adc) - - # seq.plot() - - return seq - - -if __name__ == '__main__': - main() diff --git a/pypulseq/seq2prospa/seq2prospa.py b/pypulseq/seq2prospa/seq2prospa.py deleted file mode 100644 index 7f3cc27..0000000 --- a/pypulseq/seq2prospa/seq2prospa.py +++ /dev/null @@ -1,90 +0,0 @@ -from pypulseq.Sequence.sequence import Sequence -from pypulseq.seq2prospa import convert_seq -from pypulseq.seq2prospa import make_se, make_gre - - -def main(seq: Sequence): - prospa = convert_seq.main(seq) - return prospa - - -if __name__ == '__main__': - # seq = make_gre.main() - seq = make_se.main() - output = main(seq) - - pre = """ - ######################################################## - # - # Gradient-echo imaging - # - ######################################################## - - procedure(pulse_program,dir,mode) - - - # Interface description (name, label, x, y, control_type, variable_type) - interface = ["b1Freq", "B1 Frequency (MHz)", "0","0", "tbw", "freq", - "repTime", "Repetition time (ms)", "0","1", "tbw", "reptime", - "rampTime", "Grad ramp time (us)", "0","2", "tbw", "float,[150,1e3]", - "maxPercent", "% of k-space to collect", "1","0", "tbw", "float,[1,100]", - "FOV", "Field of view (mm)", "1","1", "tb", "float", - "plane", "Imaging plane", "1","2", "tm", "[\\"xy\\",\\"yx\\",\\"xz\\",\\"zx\\",\\"yz\\",\\"zy\\"]", - "90Amplitude", "Pulse amplitude (dB)", "2","0", "tb","pulseamp", - "pulseLength", "Pulse length (us)", "2","1", "tb","pulselength", - "echoTime", "Echotime (us)", "2","2", "tb", "sdelay"] - - - - # Relationships to determine remaining variable values - relationships = ["filterCorr = 6*acqTime*1000/nrPnts + 8.5", - "readGrad = 2*pi*nrPnts/(acqTime*1e-3*gamma*FOV*1e-3)", - "phaseGrad = readGrad", - "bandWidth = nrPnts/(acqTime*1e-3)", - "(n1,n2,n3,n4,n5,n6,n8,n9,n10) = geImaging:setImagingPlane(plane,readGrad,phaseGrad,xshim,yshim,zshim,xcal,ycal,zcal)", - "n12 = 75", #Number of steps in the ramp - "d12 = rampTime/n12", #Has to be bigger than 2 us - "d13 = filterCorr", - "d14 = 43", #Gradient amp delay - "d15 = (bandWidth/10000 - 0.5)*acqTime*1000/nrPnts",#Linear eddy current compensation - "d1 = pulseLength", - "d2 = acqTime*500 - 2*rampTime + d14 + d15", - "d3 = echoTime - (acqTime*500 + 5*rampTime + d1/2 + d2) + rxLat", - "d4 = d14+d15", - "d11 = 250", #delay to settle shim gradients - "n7 = nrPnts", - "a1 = 90Amplitude", - "totPnts = nrPnts", - "totTime = acqTime"] - - - # Define the tabs and their order - tabs = ["Pulse_sequence","Acquisition","Processing_Display_Std","File_Settings"] - - # These parameters will be changed between experiments - variables = ["n4"] - - # dx,dy - dim = [170,26] - """ - post = """ - lst = endpp() # Return parameter list - - # Phase cycle list - phaseList = [0,1,2,3; # 90 phase - 0,1,2,3] # Acquire - - endproc(lst,tabs,interface,relationships,variables,dim,phaseList) - """ - - output = pre + output + post - - print(output) - -""" -n7 = 128 -gradRamp = 250 us -acqTime = 128 * 50 us -pulseLength = 100 us -pulseAmplitude = -18 -""" diff --git a/pypulseq/seq2prospa/set_imaging_plane.py b/pypulseq/seq2prospa/set_imaging_plane.py deleted file mode 100644 index ef26539..0000000 --- a/pypulseq/seq2prospa/set_imaging_plane.py +++ /dev/null @@ -1,67 +0,0 @@ -def main(plane, read_grad, phase_grad, x_shim, y_shim, z_shim, x_cal, y_cal, z_cal): - # Read - n1,n2,n9 (shim) - # Phase - n3,n4,n8 (shim) - # Other - n5,n6(shim) - # Shims are stored in mT/m not T/m - x_shim = x_shim / 1000 - y_shim = y_shim / 1000 - z_shim = z_shim / 1000 - if plane == "xy": - n1 = 3 # x (read)` - n3 = 2 # y (phase) - n5 = 1 # z (shim) - n2 = (x_shim + read_grad) * x_cal - n4 = (phase_grad + y_shim) * y_cal - n6 = z_shim * z_cal - n8 = y_shim * y_cal - n9 = x_shim * x_cal - elif plane == "yx": - n1 = 2 # y - n3 = 3 # x - n5 = 1 # z - n2 = (y_shim + read_grad) * y_cal - n4 = (phase_grad + x_shim) * x_cal - n6 = z_shim * z_cal - n8 = x_shim * x_cal - n9 = y_shim * y_cal - elif plane == "yz": - n1 = 2 # y - n3 = 1 # z - n5 = 3 # x - n2 = (y_shim + read_grad) * y_cal - n4 = (phase_grad + z_shim) * z_cal - n6 = x_shim * x_cal - n8 = z_shim * z_cal - n9 = y_shim * y_cal - elif plane == "zy": - n1 = 1 # z (read) - n3 = 2 # y (phase) - n5 = 3 # x (shim) - n2 = (z_shim + read_grad) * z_cal - n4 = (phase_grad + y_shim) * y_cal - n6 = x_shim * x_cal - n8 = y_shim * y_cal - n9 = z_shim * z_cal - elif plane == "xz": - n1 = 3 # x - n3 = 1 # z - n5 = 2 # y - n2 = (x_shim + read_grad) * x_cal - - n4 = (phase_grad + z_shim) * z_cal - n6 = y_shim * y_cal - n8 = z_shim * z_cal - n9 = x_shim * x_cal - elif plane == "zx": - n1 = 1 # x - n3 = 3 # z - n5 = 2 # y - n2 = (z_shim + read_grad) * z_cal - n4 = (phase_grad + x_shim) * x_cal - n6 = y_shim * y_cal - n8 = x_shim * x_cal - n9 = z_shim * z_cal - else: - raise Exception("Invalid plane") - - return n1, n2, n3, n4, n5, n6, n8, n9 diff --git a/pypulseq/seq_examples/notebooks/write_t2_se.ipynb b/pypulseq/seq_examples/notebooks/write_t2_se.ipynb deleted file mode 100644 index f7b5a58..0000000 --- a/pypulseq/seq_examples/notebooks/write_t2_se.ipynb +++ /dev/null @@ -1,449 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "MiKvRj5u076V" - }, - "source": [ - "## **ABOUT**\n", - "This example illustrates the 2D multi-slice, Spin Echo (SE) acquisition using the `pypulseq` library. This sequence is typically used for T2 weighted imaging. A 2D Fourier transform can be used to reconstruct images from this acquisition. Read more about SE [here](http://mriquestions.com/se-vs-multi-se-vs-fse.html).\n", - "\n", - "**Contact**: For issues, write to ks3621@columbia.edu\n", - "\n", - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Y98YDJr215fa" - }, - "source": [ - "## **INSTALL** `pypulseq`" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "ogKNAZH3TmgA" - }, - "outputs": [], - "source": [ - "!pip install git+https://github.com/imr-framework/pypulseq.git@dev" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "UgqzEwle2xCd" - }, - "source": [ - "## **IMPORT PACKAGES**" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "3X7UsV832B6j" - }, - "outputs": [], - "source": [ - "from math import pi\n", - "\n", - "import numpy as np\n", - "\n", - "from pypulseq.Sequence.sequence import Sequence\n", - "from pypulseq.calc_duration import calc_duration\n", - "from pypulseq.make_adc import make_adc\n", - "from pypulseq.make_delay import make_delay\n", - "from pypulseq.make_sinc_pulse import make_sinc_pulse\n", - "from pypulseq.make_trap_pulse import make_trapezoid\n", - "from pypulseq.opts import Opts" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "UQ4AWw9l4et_" - }, - "source": [ - "## **USER INPUTS**\n", - "\n", - "These parameters are typically on the user interface of the scanner computer console " - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "ssnNwiQH4q_0" - }, - "outputs": [], - "source": [ - "nsa = 1 # Number of averages\n", - "n_slices = 3 # Number of slices\n", - "Nx = 128\n", - "Ny = 128\n", - "fov = 220e-3 # mm\n", - "slice_thickness = 5e-3 # s\n", - "slice_gap = 15e-3 # s\n", - "rf_flip = 90 # degrees\n", - "rf_offset = 0\n", - "print('User inputs setup')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PeYeI0V45ZfD" - }, - "source": [ - "## **SYSTEM LIMITS**\n", - "Set the hardware limits and initialize sequence object" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "XHs1LT965kqg" - }, - "outputs": [], - "source": [ - "system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', \n", - " grad_raster_time=10e-6, rf_ringdown_time=10e-6, \n", - " rf_dead_time=100e-6)\n", - "seq = Sequence(system)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ee-xBrpa7Zyn" - }, - "source": [ - "## **TIME CONSTANTS**" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "u2dW2nRf7obq" - }, - "outputs": [], - "source": [ - "TE = 100e-3 # s\n", - "TR = 3 # s\n", - "tau = TE / 2 # s\n", - "readout_time = 6.4e-3\n", - "pre_time = 8e-4 # s" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "OTw7M03g79bH" - }, - "source": [ - "## **RF**" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "XDZyQrbL8I3Q" - }, - "outputs": [], - "source": [ - "flip90 = round(rf_flip * pi / 180, 3)\n", - "flip180 = 180 * pi / 180\n", - "rf90, gz90, _ = make_sinc_pulse(flip_angle=flip90, system=system, duration=4e-3, \n", - " slice_thickness=slice_thickness, apodization=0.5, \n", - " time_bw_product=4)\n", - "rf180, gz180, _ = make_sinc_pulse(flip_angle=flip180, system=system, \n", - " duration=2.5e-3, \n", - " slice_thickness=slice_thickness, \n", - " apodization=0.5, \n", - " time_bw_product=4, phase_offset=90 * pi/180)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "RFSHuUOG9LHK" - }, - "source": [ - "## **READOUT**\n", - "Readout gradients and related events" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Q8p-CttI9dk9" - }, - "outputs": [], - "source": [ - "delta_k = 1 / fov\n", - "k_width = Nx * delta_k\n", - "gx = make_trapezoid(channel='x', system=system, flat_area=k_width, \n", - " flat_time=readout_time)\n", - "adc = make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "o829kzm8kVFB" - }, - "source": [ - "## **PREPHASE AND REPHASE**" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "m5zA1bMakTVs" - }, - "outputs": [], - "source": [ - "phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_k\n", - "gz_reph = make_trapezoid(channel='z', system=system, area=-gz90.area / 2,\n", - " duration=2.5e-3)\n", - "gx_pre = make_trapezoid(channel='x', system=system, flat_area=k_width / 2, \n", - " flat_time=readout_time / 2)\n", - "gy_pre = make_trapezoid(channel='y', system=system, area=phase_areas[-1], \n", - " duration=2e-3)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "5Css5esAkYHo" - }, - "source": [ - "## **SPOILER**" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "R1DOmoKKkawr" - }, - "outputs": [], - "source": [ - "gz_spoil = make_trapezoid(channel='z', system=system, area=gz90.area * 4,\n", - " duration=pre_time * 4)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "3F5JUpE9-4lo" - }, - "source": [ - "## **DELAYS**\n", - "Echo time (TE) and repetition time (TR). Here, TE is broken down into `delay1` and `delay2`." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "aOKRJclb_mDQ" - }, - "outputs": [], - "source": [ - "delay1 = tau - calc_duration(rf90) / 2 - calc_duration(gx_pre)\n", - "delay1 -= calc_duration(gz_spoil) - calc_duration(rf180) / 2\n", - "delay1 = make_delay(delay1)\n", - "delay2 = tau - calc_duration(rf180) / 2 - calc_duration(gz_spoil)\n", - "delay2 -= calc_duration(gx) / 2\n", - "delay2 = make_delay(delay2)\n", - "delay_TR = TR - calc_duration(rf90) / 2 - calc_duration(gx) / 2 - TE\n", - "delay_TR -= calc_duration(gy_pre)\n", - "delay_TR = make_delay(delay_TR)\n", - "print(f'delay_1: {delay1}')\n", - "print(f'delay_2: {delay1}')\n", - "print(f'delay_TR: {delay_TR}')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6Dq4wT-UAEOR" - }, - "source": [ - "## **CONSTRUCT SEQUENCE**\n", - "Construct sequence for one phase encode and multiple slices" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "B8ZmVkkrAXnK" - }, - "outputs": [], - "source": [ - "# Prepare RF offsets. This is required for multi-slice acquisition\n", - "delta_z = n_slices * slice_gap\n", - "z = np.linspace((-delta_z / 2), (delta_z / 2), n_slices) + rf_offset\n", - "\n", - "for k in range(nsa): # Averages\n", - " for j in range(n_slices): # Slices\n", - " # Apply RF offsets\n", - " freq_offset = gz90.amplitude * z[j]\n", - " rf90.freq_offset = freq_offset\n", - "\n", - " freq_offset = gz180.amplitude * z[j]\n", - " rf180.freq_offset = freq_offset\n", - "\n", - " for i in range(Ny): # Phase encodes\n", - " seq.add_block(rf90, gz90)\n", - " gy_pre = make_trapezoid(channel='y', system=system, \n", - " area=phase_areas[-i -1], duration=2e-3)\n", - " seq.add_block(gx_pre, gy_pre, gz_reph)\n", - " seq.add_block(delay1)\n", - " seq.add_block(gz_spoil)\n", - " seq.add_block(rf180, gz180)\n", - " seq.add_block(gz_spoil)\n", - " seq.add_block(delay2)\n", - " seq.add_block(gx, adc)\n", - " gy_pre = make_trapezoid(channel='y', system=system, \n", - " area=-phase_areas[-j -1], duration=2e-3)\n", - " seq.add_block(gy_pre, gz_spoil)\n", - " seq.add_block(delay_TR)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "l-YP9djBJCpC" - }, - "source": [ - "## **PLOTTING TIMNG DIAGRAM**" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "d_iCUR4nfoH9" - }, - "outputs": [], - "source": [ - "seq.plot(time_range=(0, 0.1))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fYNgdWc_KiK7" - }, - "source": [ - "## **GENERATING `.SEQ` FILE**\n", - "Uncomment the code in the cell below to generate a `.seq` file and download locally." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "6iN0aeuuqKRe" - }, - "outputs": [], - "source": [ - "# seq.write('t2_se_pypulseq_colab.seq') # Save to disk\n", - "# from google.colab import files\n", - "# files.download('t2_se_pypulseq_colab.seq') # Download locally" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "4Q0b5w-lKtfP" - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "write_t2_se.ipynb", - "private_outputs": true, - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py b/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py deleted file mode 100644 index 3560d22..0000000 --- a/pypulseq/seq_examples/scripts/write_2Dt1_mprage.py +++ /dev/null @@ -1,90 +0,0 @@ -from math import pi - -import numpy as np - -import pypulseq as pp - -Nx = 128 -Ny = 128 -n_slices = 3 - -system = pp.Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', grad_raster_time=10e-6, - rf_ringdown_time=10e-6, rf_dead_time=100e-6) -seq = pp.Sequence(system) - -fov = 220e-3 -slice_thickness = 5e-3 -slice_gap = 15e-3 - -delta_z = n_slices * slice_gap -rf_offset = 0 -z = np.linspace((-delta_z / 2), (delta_z / 2), n_slices) + rf_offset - -# ========= -# RF90, RF180 -# ========= -flip = 12 * pi / 180 -rf, gz, _ = pp.make_sinc_pulse(flip_angle=flip, system=system, duration=2e-3, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, return_gz=True) - -flip90 = 90 * pi / 180 -rf90 = pp.make_block_pulse(flip_angle=flip90, system=system, duration=500e-6, slice_thickness=slice_thickness, - time_bw_product=4) - -# ========= -# Readout -# ========= -delta_k = 1 / fov -k_width = Nx * delta_k -readout_time = 6.4e-3 -gx = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time) -adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time) - -# ========= -# Prephase and Rephase -# ========= -phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_k -gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[-1], duration=2e-3) - -gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=2e-3) - -gz_reph = pp.make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=2e-3) - -# ========= -# Spoilers -# ========= -pre_time = 8e-4 -gx_spoil = pp.make_trapezoid(channel='x', system=system, area=gz.area * 4, duration=pre_time * 4) -gy_spoil = pp.make_trapezoid(channel='y', system=system, area=gz.area * 4, duration=pre_time * 4) -gz_spoil = pp.make_trapezoid(channel='z', system=system, area=gz.area * 4, duration=pre_time * 4) - -# ========= -# Delays -# ========= -TE, TI, TR = 13e-3, 140e-3, 65e-3 -delay_TE = TE - pp.calc_duration(rf) / 2 - pp.calc_duration(gy_pre) - pp.calc_duration(gx) / 2 -delay_TE = pp.make_delay(delay_TE) -delay_TI = TI - pp.calc_duration(rf90) / 2 - pp.calc_duration(gx_spoil) -delay_TI = pp.make_delay(delay_TI) -delay_TR = TR - pp.calc_duration(rf) / 2 - pp.calc_duration(gx) / 2 - pp.calc_duration(gy_pre) - TE -delay_TR = pp.make_delay(delay_TR) - -for j in range(n_slices): - freq_offset = gz.amplitude * z[j] - rf.freq_offset = freq_offset - - for i in range(Ny): - seq.add_block(rf90) - seq.add_block(gx_spoil, gy_spoil, gz_spoil) - seq.add_block(delay_TI) - seq.add_block(rf, gz) - gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[i], duration=2e-3) - seq.add_block(gx_pre, gy_pre, gz_reph) - seq.add_block(delay_TE) - seq.add_block(gx, adc) - gy_pre = pp.make_trapezoid(channel='y', system=system, area=-phase_areas[i], duration=2e-3) - seq.add_block(gx_spoil, gy_pre) - seq.add_block(delay_TR) - -seq.set_definition(key='Name', val='2D T1 MPRAGE') -seq.write('2d_mprage_pypulseq.seq') diff --git a/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py b/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py deleted file mode 100644 index 57b5ee3..0000000 --- a/pypulseq/seq_examples/scripts/write_3Dt1_mprage.py +++ /dev/null @@ -1,93 +0,0 @@ -from math import pi - -import numpy as np - -import pypulseq as pp - -Nx = 256 -Ny = 128 -Nz = 32 - -system = pp.Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', grad_raster_time=10e-6, - rf_ringdown_time=10e-6, rf_dead_time=100e-6) -seq = pp.Sequence(system) - -fov = 220e-3 -fov_z = 100e-3 -slice_thickness = 1e-3 -section_thickness = 5e-3 - -# ========= -# RF preparatory, excitation -# ========= -flip_exc = 12 * pi / 180 -rf = pp.make_block_pulse(flip_angle=flip_exc, system=system, duration=250e-6, slice_thickness=slice_thickness, - time_bw_product=4) - -flip_prep = 90 * pi / 180 -rf_prep = pp.make_block_pulse(flip_angle=flip_prep, system=system, duration=500e-6, slice_thickness=section_thickness, - time_bw_product=4) - -# ========= -# Readout -# ========= -delta_k = 1 / fov -k_width = Nx * delta_k -readout_time = 6.4e-3 -gx = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time) -adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time) - -# ========= -# Prephase and Rephase -# ========= -delta_kz = 1 / fov_z -phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_kz -slice_areas = (np.arange(Nz) - (Nz / 2)) * delta_kz - -gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=2e-3) -gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[-1], duration=2e-3) - -# ========= -# Spoilers -# ========= -pre_time = 6.4e-4 -gx_spoil = pp.make_trapezoid(channel='x', system=system, area=(4 * np.pi) / (42.576e6 * delta_k * 1e-3), - duration=pre_time * 4) -gy_spoil = pp.make_trapezoid(channel='y', system=system, area=(4 * np.pi) / (42.576e6 * delta_kz * 1e-3), - duration=pre_time * 4) -gz_spoil = pp.make_trapezoid(channel='z', system=system, area=(4 * np.pi) / (42.576e6 * delta_kz * 1e-3), - duration=pre_time * 4) - -# ========= -# Delays -# ========= -TE, TI, TR = 4e-3, 140e-3, 10e-3 -delay_TE = TE - pp.calc_duration(rf) / 2 - pp.calc_duration(gx_pre) - pp.calc_duration(gx) / 2 -delay_TE = pp.make_delay(delay_TE) -delay_TI = TI - pp.calc_duration(rf_prep) / 2 - pp.calc_duration(gx_spoil) -delay_TI = pp.make_delay(delay_TI) -delay_TR = TR - pp.calc_duration(rf) - pp.calc_duration(gx_pre) - pp.calc_duration(gx) - pp.calc_duration(gx_spoil) -delay_TR = pp.make_delay(delay_TR) - -for i in range(Ny): - gy_pre = pp.make_trapezoid(channel='y', system=system, area=phase_areas[i], duration=2e-3) - - seq.add_block(rf_prep) - seq.add_block(gx_spoil, gy_spoil, gz_spoil) - seq.add_block(delay_TI) - - for j in range(Nz): - gz_pre = pp.make_trapezoid(channel='z', system=system, area=slice_areas[j], duration=2e-3) - gz_reph = pp.make_trapezoid(channel='z', system=system, area=-slice_areas[j], duration=2e-3) - - seq.add_block(rf) - seq.add_block(gx_pre, gy_pre, gz_pre) - seq.add_block(delay_TE) - seq.add_block(gx, adc) - seq.add_block(gx_spoil, gz_reph) - - seq.add_block(delay_TR) - -seq.set_definition(key='Name', val='3D T1 MPRAGE') - -seq.plot() diff --git a/pypulseq/seq_examples/scripts/write_epi.py b/pypulseq/seq_examples/scripts/write_epi.py deleted file mode 100644 index 3e6be74..0000000 --- a/pypulseq/seq_examples/scripts/write_epi.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Demo low-performance EPI sequence which doesn't use ramp-sampling. -""" -import math - -import matplotlib.pyplot as plt -import numpy as np - -from pypulseq.Sequence.sequence import Sequence -from pypulseq.make_adc import make_adc -from pypulseq.make_sinc_pulse import make_sinc_pulse -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - -# ====== -# SETUP -# ====== -seq = Sequence() # Create a new sequence object -# Define FOV and resolution -fov = 220e-3 -Nx = 64 -Ny = 64 -slice_thickness = 3e-3 # Slice thickness -n_slices = 3 - -# Set system limits -system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, - rf_dead_time=100e-6) - -# ====== -# CREATE EVENTS -# ====== -# Create 90 degree slice selection pulse and gradient -rf, gz, _ = make_sinc_pulse(flip_angle=np.pi / 2, system=system, duration=3e-3, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, return_gz=True) - -# Define other gradients and ADC events -delta_k = 1 / fov -k_width = Nx * delta_k -dwell_time = 4e-6 -readout_time = Nx * dwell_time -flat_time = math.ceil(readout_time * 1e5) * 1e-5 # round-up to the gradient raster -gx = make_trapezoid(channel='x', system=system, amplitude=k_width / readout_time, flat_time=flat_time) -adc = make_adc(num_samples=Nx, duration=readout_time, - delay=gx.rise_time + flat_time / 2 - (readout_time - dwell_time) / 2) - -# Pre-phasing gradients -pre_time = 8e-4 -gx_pre = make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=pre_time) -gz_reph = make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=pre_time) -gy_pre = make_trapezoid(channel='y', system=system, area=-Ny / 2 * delta_k, duration=pre_time) - -# Phase blip in shortest possible time -dur = math.ceil(2 * math.sqrt(delta_k / system.max_slew) / 10e-6) * 10e-6 -gy = make_trapezoid(channel='y', system=system, area=delta_k, duration=dur) - -# ====== -# CONSTRUCT SEQUENCE -# ====== -# Define sequence blocks -for s in range(n_slices): - rf.freq_offset = gz.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - seq.add_block(rf, gz) - seq.add_block(gx_pre, gy_pre, gz_reph) - for i in range(Ny): - seq.add_block(gx, adc) # Read one line of k-space - seq.add_block(gy) # Phase blip - gx.amplitude = -gx.amplitude # Reverse polarity of read gradient - -# ====== -# VISUALIZATION -# ====== -seq.plot() # Plot sequence waveforms - -ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc = seq.calculate_kspace() - -# Plot k-spaces -time_axis = np.arange(1, ktraj.shape[1] + 1) * system.grad_raster_time -plt.plot(time_axis, ktraj.T) # Plot the entire k-space trajectory -plt.plot(t_adc, ktraj_adc[0, :], '.') # Sampling points on the kx-axis -plt.figure() -plt.plot(ktraj[0, :], ktraj[1, :], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0, :], ktraj_adc[1, :], 'r.') -plt.show() - -seq.write('epi_pypulseq.seq') diff --git a/pypulseq/seq_examples/scripts/write_epi_se.py b/pypulseq/seq_examples/scripts/write_epi_se.py deleted file mode 100644 index 8000d64..0000000 --- a/pypulseq/seq_examples/scripts/write_epi_se.py +++ /dev/null @@ -1,98 +0,0 @@ -import math - -import numpy as np -from matplotlib import pyplot as plt - -import pypulseq as pp - -# ====== -# SETUP -# ====== -seq = pp.Sequence() # Create a new sequence object -# Define FOV and resolution -fov = 256e-3 -Nx = 64 -Ny = 64 - -# Set system limits -system = pp.Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, - rf_dead_time=100e-6, adc_dead_time=20e-6) - -# ====== -# CREATE EVENTS -# ====== -# Create 90 degree slice selection pulse and gradient -rf, gz, _ = pp.make_sinc_pulse(flip_angle=np.pi / 2, system=system, duration=3e-3, slice_thickness=3e-3, - apodization=0.5, time_bw_product=4, return_gz=True) - -# Define other gradients and ADC events -delta_k = 1 / fov -k_width = Nx * delta_k -readout_time = 3.2e-4 -gx = pp.make_trapezoid(channel='x', system=system, flat_area=k_width, flat_time=readout_time) -adc = pp.make_adc(num_samples=Nx, system=system, duration=gx.flat_time, delay=gx.rise_time) - -# Pre-phasing gradients -pre_time = 8e-4 -gz_reph = pp.make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=pre_time) -# Do not need minus for in-plane prephasers because of the spin-echo (position reflection in k-space) -gx_pre = pp.make_trapezoid(channel='x', system=system, area=gx.area / 2 - delta_k / 2, duration=pre_time) -gy_pre = pp.make_trapezoid(channel='y', system=system, area=Ny / 2 * delta_k, duration=pre_time) - -# Phase blip in shortest possible time -dur = math.ceil(2 * math.sqrt(delta_k / system.max_slew) / 10e-6) * 10e-6 -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') -gz_spoil = pp.make_trapezoid(channel='z', system=system, area=gz.area * 2, duration=3 * pre_time) - -# Calculate delay time -TE = 60e-3 -duration_to_center = (Nx / 2 + 0.5) * pp.calc_duration(gx) + Ny / 2 * pp.calc_duration(gy) -rf_center_incl_delay = rf.delay + pp.calc_rf_center(rf)[0] -rf180_center_incl_delay = rf180.delay + pp.calc_rf_center(rf180)[0] -delay_TE1 = TE / 2 - pp.calc_duration(gz) + rf_center_incl_delay - pre_time - pp.calc_duration( - gz_spoil) - rf180_center_incl_delay -delay_TE2 = TE / 2 - pp.calc_duration(rf180) + rf180_center_incl_delay - pp.calc_duration(gz_spoil) - duration_to_center - -# ====== -# CONSTRUCT SEQUENCE -# ====== -# Define sequence blocks -seq.add_block(rf, gz) -seq.add_block(gx_pre, gy_pre, gz_reph) -seq.add_block(pp.make_delay(delay_TE1)) -seq.add_block(gz_spoil) -seq.add_block(rf180) -seq.add_block(gz_spoil) -seq.add_block(pp.make_delay(delay_TE2)) -for i in range(Ny): - seq.add_block(gx, adc) # Read one line of k-space - seq.add_block(gy) # Phase blip - gx.amplitude = -gx.amplitude # Reverse polarity of read gradient -seq.add_block(pp.make_delay(1e-4)) - -# ====== -# VISUALIZATION -# ====== -seq.plot() - -# Calculate trajectory -ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc = seq.calculate_kspace() - -# Plot k-spaces -time_axis = np.arange(1, ktraj.shape[1] + 1) * system.grad_raster_time -plt.figure() -plt.plot(time_axis, ktraj.T) # Plot entire k-space trajectory -plt.plot(t_adc, ktraj_adc[0], '.') # Plot sampling points on kx-axis -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b', ktraj_adc[0], ktraj_adc[1], 'r.') # 2D plot -plt.axis('equal') -plt.show() - -seq.write('epi_se_pypulseq.seq') - -# Sanity checks -TE_check = (t_refocusing[0] - t_excitation[0]) * 2 -print(f'Intended TE = {TE * 1e3:.03f} ms, actual spin echo TE = {TE_check * 1e3:.03f} ms') diff --git a/pypulseq/seq_examples/scripts/write_epi_se_rs.py b/pypulseq/seq_examples/scripts/write_epi_se_rs.py deleted file mode 100644 index 25041d9..0000000 --- a/pypulseq/seq_examples/scripts/write_epi_se_rs.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -This is an experimental high-performance EPI sequence which uses split gradients to overlap blips with the readout -gradients combined with ramp-sampling. -""" -import math - -import numpy as np -from matplotlib import pyplot as plt - -import pypulseq as pp - -# ====== -# SETUP -# ====== -seq = pp.Sequence() # Create a new sequence object -# Define FOV and resolution -fov = 250e-3 -Nx = 64 -Ny = 64 -slice_thickness = 3e-3 # Clice thickness -n_slices = 3 -TE = 40e-3 - -pe_enable = 1 # Flag to quickly disable phase encoding (1/0) as needed for the delay calibration -ro_os = 1 # Oversampling factor -readout_time = 4.2e-4 # Readout bandwidth -part_fourier_factor = 0.75 # Partial Fourier factor: 1: full sampling; 0: start with ky=0 - -t_RF_ex = 2e-3 -t_RF_ref = 2e-3 -spoil_factor = 1.5 # Spoiling gradient around the pi-pulse (rf180) - -# Set system limits -system = pp.Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, - rf_dead_time=100e-6) - -# ====== -# CREATE EVENTS -# ====== -# Create fat-sat pulse -B0 = 2.89 -sat_ppm = -3.45 -sat_freq = sat_ppm * 1e-6 * B0 * system.gamma -rf_fs = pp.make_gauss_pulse(flip_angle=110 * np.pi / 180, system=system, duration=8e-3, bandwidth=abs(sat_freq), - freq_offset=sat_freq) -gz_fs = pp.make_trapezoid(channel='z', system=system, delay=pp.calc_duration(rf_fs), area=1 / 1e-4) - -# Create 90 degree slice selection pulse and gradient -rf, gz, gz_reph = pp.make_sinc_pulse(flip_angle=np.pi / 2, system=system, duration=t_RF_ex, - slice_thickness=slice_thickness, apodization=0.5, time_bw_product=4, - return_gz=True) - -# Create 90 degree slice refocusing pulse and gradients -rf180, gz180, _ = pp.make_sinc_pulse(flip_angle=np.pi, system=system, duration=t_RF_ref, - slice_thickness=slice_thickness, apodization=0.5, time_bw_product=4, - phase_offset=np.pi / 2, use='refocusing', return_gz=True) -_, gzr1_t, gzr1_a = pp.make_extended_trapezoid_area(channel='z', Gs=0, Ge=gz180.amplitude, A=spoil_factor * gz.area, - system=system) -_, gzr2_t, gzr2_a = pp.make_extended_trapezoid_area(channel='z', Gs=gz180.amplitude, Ge=0, - A=-gz_reph.area + spoil_factor * gz.area, system=system) -if gz180.delay > (gzr1_t[3] - gz180.rise_time): - gz180.delay -= gzr1_t[3] - gz180.rise_time -else: - rf180.delay += (gzr1_t[3] - gz180.rise_time) - gz180.delay -gz180n = pp.make_extended_trapezoid(channel='z', system=system, - times=np.array([*gzr1_t, *gzr1_t[3] + gz180.flat_time + gzr2_t]) + gz180.delay, - amplitudes=np.array([*gzr1_a, *gzr2_a])) - -# Define the output trigger to play out with every slice excitation -trig = pp.make_digital_output_pulse(channel='osc0', duration=100e-6) - -# Define other gradients and ADC events -delta_k = 1 / fov -k_width = Nx * delta_k - -# Phase blip in shortest possible time -# Round up the duration to 2x gradient raster time -blip_duration = np.ceil(2 * np.sqrt(delta_k / system.max_slew) / 10e-6 / 2) * 10e-6 * 2 -# Use negative blips to save one k-space line on our way to center of k-space -gy = pp.make_trapezoid(channel='y', system=system, area=-delta_k, duration=blip_duration) - -# Readout gradient is a truncated trapezoid with dead times at the beginning and at the end each equal to a half of blip -# duration. The area between the blips should be defined by k_width. We do a two-step calculation: we first increase the -# area assuming maximum slew rate and then scale down the amplitude to fix the area -extra_area = blip_duration / 2 * blip_duration / 2 * system.max_slew -gx = pp.make_trapezoid(channel='x', system=system, area=k_width + extra_area, duration=readout_time + blip_duration) -actual_area = gx.area - gx.amplitude / gx.rise_time * blip_duration / 2 * blip_duration / 2 / 2 -actual_area -= gx.amplitude / gx.fall_time * blip_duration / 2 * blip_duration / 2 / 2 -gx.amplitude = gx.amplitude / actual_area * k_width -gx.area = gx.amplitude * (gx.flat_time + gx.rise_time / 2 + gx.fall_time / 2) -gx.flat_area = gx.amplitude * gx.flat_time - -# Calculate ADC -# We use ramp sampling, so we have to calculate the dwell time and the number of samples, which will be quite different -# from Nx and readout_time/Nx, respectively. -adc_dwell_nyquist = delta_k / gx.amplitude / ro_os -# Round-down dwell time to 100 ns -adc_dwell = math.floor(adc_dwell_nyquist * 1e7) * 1e-7 -adc_samples = math.floor(readout_time / adc_dwell / 4) * 4 # Number of samples on Siemens needs to be divisible by 4 -adc = pp.make_adc(num_samples=adc_samples, dwell=adc_dwell, delay=blip_duration / 2) -# Realign the ADC with respect to the gradient -time_to_center = adc_dwell * ((adc_samples - 1) / 2 + 0.5) # Supposedly Siemens samples at center of dwell period -# Adjust delay to align the trajectory with the gradient. We have to align the delay to 1us -adc.delay = round((gx.rise_time + gx.flat_time / 2 - time_to_center) * 1e6) * 1e-6 -# This rounding actually makes the sampling points on odd and even readouts to appear misaligned. However, on the real -# hardware this misalignment is much stronger anyways due to the gradient delays - -# Split the blip into two halves and produnce a combined synthetic gradient -gy_parts = pp.split_gradient_at(grad=gy, time_point=blip_duration / 2, system=system) -gy_blipup, gy_blipdown, _ = pp.align(right=gy_parts[0], left=[gy_parts[1], gx]) -gy_blipdownup = pp.add_gradients((gy_blipdown, gy_blipup), system=system) - -# pe_enable support -gy_blipup.waveform = gy_blipup.waveform * pe_enable -gy_blipdown.waveform = gy_blipdown.waveform * pe_enable -gy_blipdownup.waveform = gy_blipdownup.waveform * pe_enable - -# Phase encoding and partial Fourier -Ny_pre = round(part_fourier_factor * Ny / 2 - 1) # PE steps prior to ky=0, excluding the central line -Ny_post = round(Ny / 2 + 1) # PE lines after the k-space center including the central line -Ny_meas = Ny_pre + Ny_post - -# Pre-phasing gradients -gx_pre = pp.make_trapezoid(channel='x', system=system, area=-gx.area / 2) -gy_pre = pp.make_trapezoid(channel='y', system=system, area=Ny_pre * delta_k) - -gx_pre, gy_pre = pp.align(right=gx_pre, left=gy_pre) -# Relax the PE prephaser to reduce stimulation -gy_pre = pp.make_trapezoid('y', system=system, area=gy_pre.area, duration=pp.calc_duration(gx_pre, gy_pre)) -gy_pre.amplitude = gy_pre.amplitude * pe_enable - -# Calculate delay times -duration_to_center = (Ny_pre + 0.5) * pp.calc_duration(gx) -rf_center_incl_delay = rf.delay + pp.calc_rf_center(rf)[0] -rf180_center_incl_delay = rf180.delay + pp.calc_rf_center(rf180)[0] -delay_TE1 = math.ceil((TE / 2 - pp.calc_duration(rf, - gz) + rf_center_incl_delay - rf180_center_incl_delay) / system.grad_raster_time) * system.grad_raster_time -delay_TE2 = math.ceil((TE / 2 - pp.calc_duration(rf180, - gz180n) + rf180_center_incl_delay - duration_to_center) / system.grad_raster_time) * system.grad_raster_time -assert (delay_TE1 >= 0) -# Now we merge slice refocusing, TE delay and pre-phasers into a single block -delay_TE2 = delay_TE2 + pp.calc_duration(rf180, gz180n) -gx_pre.delay = 0 -gx_pre.delay = delay_TE2 - pp.calc_duration(gx_pre) -assert (gx_pre.delay >= pp.calc_duration(rf180)) # gx_pre may not overlap with the RF -gy_pre.delay = pp.calc_duration(rf180) -assert (pp.calc_duration(gy_pre) <= pp.calc_duration(gx_pre)) # gyPre may not shift the timing - -# ====== -# CONSTRUCT SEQUENCE -# ====== -# Define sequence blocks -for s in range(n_slices): - seq.add_block(rf_fs, gz_fs) - rf.freq_offset = gz.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - rf180.freq_offset = gz180.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - seq.add_block(rf, gz, trig) - seq.add_block(pp.make_delay(delay_TE1)) - seq.add_block(rf180, gz180n, pp.make_delay(delay_TE2), gx_pre, gy_pre) - for i in range(1, Ny_meas + 1): - if i == 1: - seq.add_block(gx, gy_blipup, adc) # Read the first line of k-space with a single half-blip at the end - elif i == Ny_meas: - # Read the last line of k-space with a single half-blip at the beginning - seq.add_block(gx, gy_blipdown, adc) - else: - # Read an intermediate line of k-space with a half-blip at the beginning and a half-blip at the end - seq.add_block(gx, gy_blipdownup, adc) - gx.amplitude = -gx.amplitude # Reverse polarity of read gradient - -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 -# ====== -seq.plot() - -# Trajectory calculation and plotting -ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc = seq.calculate_kspace() - -time_axis = np.arange(1, ktraj.shape[1] + 1) * system.grad_raster_time -plt.figure() -plt.plot(time_axis, ktraj.T) # Plot the entire k-space trajectory -plt.plot(t_adc, ktraj_adc[0], '.') # Plot sampling points on the kx-axis -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0], ktraj_adc[1], 'r.') # Plot sampling points -plt.show() - -# Prepare the sequence output for the scanner -seq.set_definition('FOV', [fov, fov, slice_thickness]) -seq.set_definition('Name', 'epi') - -seq.write('epi_se_rs_pypulseq.seq') - -# Very optional slow step, but useful for testing during development e.g. for the real TE, TR or for staying within -# slewrate limits -rep = seq.test_report() -print(rep) diff --git a/pypulseq/seq_examples/scripts/write_gre.py b/pypulseq/seq_examples/scripts/write_gre.py deleted file mode 100644 index 815d5c6..0000000 --- a/pypulseq/seq_examples/scripts/write_gre.py +++ /dev/null @@ -1,110 +0,0 @@ -import math - -import numpy as np -from matplotlib import pyplot as plt - -import pypulseq as pp - -# ====== -# SETUP -# ====== -# Create a new sequence object -seq = pp.Sequence() - -# Define FOV and resolution -fov = 256e-3 -Nx = 256 -Ny = 256 -alpha = 10 # flip angle -slice_thickness = 3e-3 # slice -# TE = np.array([7.38]) * 1e-3 # give a vector here to have multiple TEs (e.g. for field mapping) -TE = np.array([4.3e-3]) -TR = 10e-3 - -rf_spoiling_inc = 117 # RF spoiling increment - -system = pp.Opts(max_grad=28, grad_unit='mT/m', max_slew=150, slew_unit='T/m/s', rf_ringdown_time=20e-6, - rf_dead_time=100e-6, adc_dead_time=10e-6) - -# ====== -# CREATE EVENTS -# ====== -rf, gz, gzr = pp.make_sinc_pulse(flip_angle=alpha * math.pi / 180, duration=3e-3, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, system=system, return_gz=True) -# Define other gradients and ADC events -delta_k = 1 / fov -gx = pp.make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=3.2e-3, system=system) -adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system) -gx_pre = pp.make_trapezoid(channel='x', area=-gx.area / 2, duration=1e-3, system=system) -gz_reph = pp.make_trapezoid(channel='z', area=-gz.area / 2, duration=1e-3, system=system) -phase_areas = (np.arange(Ny) - Ny / 2) * delta_k - -# gradient spoiling -gx_spoil = pp.make_trapezoid(channel='x', area=2 * Nx * delta_k, system=system) -gz_spoil = pp.make_trapezoid(channel='z', area=4 / slice_thickness, system=system) - -# Calculate timing -delay_TE = np.ceil((TE - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration( - gx) / 2) / seq.grad_raster_time) * seq.grad_raster_time -delay_TR = np.ceil((TR - pp.calc_duration(gz) - pp.calc_duration(gx_pre) - pp.calc_duration( - gx) - delay_TE) / seq.grad_raster_time) * seq.grad_raster_time - -assert np.all(delay_TE >= 0) -assert np.all(delay_TR >= pp.calc_duration(gx_spoil, gz_spoil)) - -rf_phase = 0 -rf_inc = 0 - -# ====== -# CONSTRUCT SEQUENCE -# ====== -# Loop over phase encodes and define sequence blocks -for i in range(Ny): - for j in range(len(TE)): - rf.phase_offset = rf_phase / 180 * np.pi - adc.phase_offset = rf_phase / 180 * np.pi - rf_inc = divmod(rf_inc + rf_spoiling_inc, 360.0)[1] - rf_phase = divmod(rf_phase + rf_inc, 360.0)[1] - - seq.add_block(rf, gz) - gy_pre = pp.make_trapezoid(channel='y', area=phase_areas[i], duration=pp.calc_duration(gx_pre), system=system) - seq.add_block(gx_pre, gy_pre, gz_reph) - seq.add_block(pp.make_delay(delay_TE[j])) - seq.add_block(gx, adc) - gy_pre.amplitude = -gy_pre.amplitude - seq.add_block(pp.make_delay(delay_TR[j]), gx_spoil, gy_pre, gz_spoil) - -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 -# ====== -seq.plot() - -# Trajectory calculation and plotting -ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc = seq.calculate_kspace() -time_axis = np.arange(1, ktraj.shape[1] + 1) * system.grad_raster_time -plt.figure() -plt.plot(time_axis, ktraj.T) # Plot the entire k-space trajectory -plt.plot(t_adc, ktraj_adc[0], '.') # Plot sampling points on the kx-axis -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0], ktraj_adc[1], 'r.') # Plot sampling points -plt.show() - -# Prepare the sequence output for the scanner -seq.set_definition('FOV', [fov, fov, slice_thickness]) -seq.set_definition('Name', 'gre') - -seq.write('gre_pypulseq.seq') - -# Very optional slow step, but useful for testing during development e.g. for the real TE, TR or for staying within -# slew-rate limits -rep = seq.test_report() -print(rep) diff --git a/pypulseq/seq_examples/scripts/write_gre_label.py b/pypulseq/seq_examples/scripts/write_gre_label.py deleted file mode 100644 index 4ff3a16..0000000 --- a/pypulseq/seq_examples/scripts/write_gre_label.py +++ /dev/null @@ -1,113 +0,0 @@ -import math - -import numpy as np -from matplotlib import pyplot as plt - -import pypulseq as pp - -# ====== -# SETUP -seq = pp.Sequence() # Create a new sequence object -# Define FOV and resolution -fov = 224e-3 -Nx = 256 -Ny = Nx -alpha = 10 # Flip angle -slice_thickness = 3e-3 # Slice thickness -n_slices = 1 -TE = 4.3e-3 -TR = 10e-3 - -rf_spoiling_inc = 117 # RF spoiling increment -ro_duration = 3.2e-3 # ADC duration - -# Set system limits -system = pp.Opts(max_grad=28, grad_unit='mT/m', max_slew=150, slew_unit='T/m/s', rf_ringdown_time=20e-6, - rf_dead_time=100e-6, adc_dead_time=10e-6) - -# ====== -# CREATE EVENTS -# ====== -# Create alpha-degree slice selection pulse and gradient -rf, gz, _ = pp.make_sinc_pulse(flip_angle=alpha * np.pi / 180, duration=3e-3, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, system=system, return_gz=True) - -# Define other gradients and ADC events -delta_k = 1 / fov -gx = pp.make_trapezoid(channel='x', flat_area=Nx * delta_k, flat_time=ro_duration, system=system) -adc = pp.make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system) -gx_pre = pp.make_trapezoid(channel='x', area=-gx.area / 2, duration=1e-3, system=system) -gz_reph = pp.make_trapezoid(channel='z', area=-gz.area / 2, duration=1e-3, system=system) -phase_areas = -(np.arange(Ny) - Ny / 2) * delta_k - -# Gradient spoiling -gx_spoil = pp.make_trapezoid(channel='x', area=2 * Nx * delta_k, system=system) -gz_spoil = pp.make_trapezoid(channel='z', area=4 / slice_thickness, system=system) - -# Calculate timing -delay_TE = math.ceil((TE - pp.calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - pp.calc_duration( - gx) / 2) / seq.grad_raster_time) * seq.grad_raster_time -delay_TR = math.ceil((TR - pp.calc_duration(gz) - pp.calc_duration(gx_pre) - pp.calc_duration( - gx) - delay_TE) / seq.grad_raster_time) * seq.grad_raster_time -assert np.all(delay_TE >= 0) -assert np.all(delay_TR >= pp.calc_duration(gx_spoil, gz_spoil)) - -rf_phase = 0 -rf_inc = 0 - -# ====== -# CONSTRUCT SEQUENCE -# ====== -# Loop over slices -for s in range(n_slices): - rf.freq_offset = gz.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - # Loop over phase encodes and define sequence blocks - for i in range(Ny): - rf.phase_offset = rf_phase / 180 * np.pi - adc.phase_offset = rf_phase / 180 * np.pi - rf_inc = divmod(rf_inc + rf_spoiling_inc, 360.0)[1] - rf_phase = divmod(rf_phase + rf_inc, 360.0)[1] - - seq.add_block(rf, gz) - gy_pre = pp.make_trapezoid(channel='y', area=phase_areas[i], duration=pp.calc_duration(gx_pre), system=system) - seq.add_block(gx_pre, gy_pre, gz_reph) - seq.add_block(pp.make_delay(delay_TE)) - seq.add_block(gx, adc) - gy_pre.amplitude = -gy_pre.amplitude - spoil_block_contents = [pp.make_delay(delay_TR), gx_spoil, gy_pre, gz_spoil] - if i != Ny - 1: - spoil_block_contents.append(pp.make_label(type='INC', label='LIN', value=1)) - else: - spoil_block_contents.extend([pp.make_label(type='SET', label='LIN', value=0), - pp.make_label(type='INC', label='SLC', value=1)]) - seq.add_block(*spoil_block_contents) - -ok, error_report = seq.check_timing() - -if ok: - print('Timing check passed successfully') -else: - print('Timing check failed. Error listing follows:') - [print(e) for e in error_report] - -# ====== -# VISUALIZATION -# ====== -seq.plot(label='lin', time_range=np.array([0, 32]) * TR, time_disp='ms') - -# Trajectory calculation and plotting -ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc = seq.calculate_kspace() -time_axis = np.arange(1, ktraj.shape[1] + 1) * system.grad_raster_time -plt.figure() -plt.plot(time_axis, ktraj.T) # Plot the entire k-space trajectory -plt.plot(t_adc, ktraj_adc[0], '.') # Plot sampling points on the kx-axis -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0], ktraj_adc[1], 'r.') # Plot sampling points -plt.show() - -seq.set_definition(key='FOV', val=[fov, fov, slice_thickness * n_slices]) -seq.set_definition(key='Name', val='gre_label') - -seq.write('gre_label_pypulseq.seq') diff --git a/pypulseq/seq_examples/scripts/write_haste.py b/pypulseq/seq_examples/scripts/write_haste.py deleted file mode 100644 index 32cab60..0000000 --- a/pypulseq/seq_examples/scripts/write_haste.py +++ /dev/null @@ -1,212 +0,0 @@ -import math -import warnings - -import matplotlib.pyplot as plt -import numpy as np - -from pypulseq.Sequence.sequence import Sequence -from pypulseq.calc_rf_center import calc_rf_center -from pypulseq.make_adc import make_adc -from pypulseq.make_delay import make_delay -from pypulseq.make_extended_trapezoid import make_extended_trapezoid -from pypulseq.make_sinc_pulse import make_sinc_pulse -from pypulseq.make_trap_pulse import make_trapezoid -from pypulseq.opts import Opts - -# ====== -# SETUP -# ====== -dG = 250e-6 - -# Set system limits -system = Opts(max_grad=30, grad_unit='mT/m', max_slew=170, slew_unit='T/m/s', rf_ringdown_time=100e-6, - rf_dead_time=100e-6, adc_dead_time=10e-6) - -seq = Sequence(system=system) # Create a new sequence object -# Define FOV and resolution -fov = 256e-3 -Ny_pre = 8 -Nx, Ny = 128, 128 -n_echo = int(Ny / 2 + Ny_pre) -n_slices = 1 -rf_flip = 180 -if isinstance(rf_flip, int): - rf_flip = np.zeros(n_echo) + rf_flip -slice_thickness = 5e-3 # Slice thickness -TE = 12e-3 -TR = 2000e-3 -TE_eff = 60e-3 -k0 = round(TE_eff / TE) -PE_type = 'linear' - -readout_time = 6.4e-3 + 2 * system.adc_dead_time -t_ex = 2.5e-3 -t_ex_wd = t_ex + system.rf_ringdown_time + system.rf_dead_time -t_ref = 2e-3 -tf_ref_wd = t_ref + system.rf_ringdown_time + system.rf_dead_time -t_sp = 0.5 * (TE - readout_time - tf_ref_wd) -t_sp_ex = 0.5 * (TE - t_ex_wd - tf_ref_wd) -fspR = 1.0 -fspS = 0.5 - -rfex_phase = math.pi / 2 -rfref_phase = 0 - -# ====== -# CREATE EVENTS -# ====== -# Create 90 degree slice selection pulse and gradient -flipex = 90 * math.pi / 180 -rfex, gz, _ = make_sinc_pulse(flip_angle=flipex, system=system, duration=t_ex, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, phase_offset=rfex_phase, return_gz=True) -GS_ex = make_trapezoid(channel='z', system=system, amplitude=gz.amplitude, flat_time=t_ex_wd, rise_time=dG) - -flipref = rf_flip[0] * math.pi / 180 -rfref, gz, _ = make_sinc_pulse(flip_angle=flipref, system=system, duration=t_ref, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, phase_offset=rfref_phase, use='refocusing', - return_gz=True) -GS_ref = make_trapezoid(channel='z', system=system, amplitude=GS_ex.amplitude, flat_time=tf_ref_wd, rise_time=dG) - -AGS_ex = GS_ex.area / 2 -GS_spr = make_trapezoid(channel='z', system=system, area=AGS_ex * (1 + fspS), duration=t_sp, rise_time=dG) -GS_spex = make_trapezoid(channel='z', system=system, area=AGS_ex * fspS, duration=t_sp_ex, rise_time=dG) - -delta_k = 1 / fov -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 - 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) - -AGR_spr = GR_spr.area -AGR_preph = GR_acq.area / 2 + AGR_spr -GR_preph = make_trapezoid(channel='x', system=system, area=AGR_preph, duration=t_sp_ex, rise_time=dG) - -n_ex = 1 -PE_order = np.arange(-Ny_pre, Ny + 1).T -phase_areas = PE_order * delta_k - -# Split gradients and recombine into blocks -GS1_times = [0, GS_ex.rise_time] -GS1_amp = [0, GS_ex.amplitude] -GS1 = make_extended_trapezoid(channel='z', times=GS1_times, amplitudes=GS1_amp) - -GS2_times = [0, GS_ex.flat_time] -GS2_amp = [GS_ex.amplitude, GS_ex.amplitude] -GS2 = make_extended_trapezoid(channel='z', times=GS2_times, amplitudes=GS2_amp) - -GS3_times = [0, GS_spex.rise_time, GS_spex.rise_time + GS_spex.flat_time, - GS_spex.rise_time + GS_spex.flat_time + GS_spex.fall_time] -GS3_amp = [GS_ex.amplitude, GS_spex.amplitude, GS_spex.amplitude, GS_ref.amplitude] -GS3 = make_extended_trapezoid(channel='z', times=GS3_times, amplitudes=GS3_amp) - -GS4_times = [0, GS_ref.flat_time] -GS4_amp = [GS_ref.amplitude, GS_ref.amplitude] -GS4 = make_extended_trapezoid(channel='z', times=GS4_times, amplitudes=GS4_amp) - -GS5_times = [0, GS_spr.rise_time, GS_spr.rise_time + GS_spr.flat_time, - GS_spr.rise_time + GS_spr.flat_time + GS_spr.fall_time] -GS5_amp = [GS_ref.amplitude, GS_spr.amplitude, GS_spr.amplitude, 0] -GS5 = make_extended_trapezoid(channel='z', times=GS5_times, amplitudes=GS5_amp) - -GS7_times = [0, GS_spr.rise_time, GS_spr.rise_time + GS_spr.flat_time, - GS_spr.rise_time + GS_spr.flat_time + GS_spr.fall_time] -GS7_amp = [0, GS_spr.amplitude, GS_spr.amplitude, GS_ref.amplitude] -GS7 = make_extended_trapezoid(channel='z', times=GS7_times, amplitudes=GS7_amp) - -# Readout gradient -GR3 = GR_preph - -GR5_times = [0, GR_spr.rise_time, GR_spr.rise_time + GR_spr.flat_time, - GR_spr.rise_time + GR_spr.flat_time + GR_spr.fall_time] -GR5_amp = [0, GR_spr.amplitude, GR_spr.amplitude, GR_acq.amplitude] -GR5 = make_extended_trapezoid(channel='x', times=GR5_times, amplitudes=GR5_amp) - -GR6_times = [0, readout_time] -GR6_amp = [GR_acq.amplitude, GR_acq.amplitude] -GR6 = make_extended_trapezoid(channel='x', times=GR6_times, amplitudes=GR6_amp) - -GR7_times = [0, GR_spr.rise_time, GR_spr.rise_time + GR_spr.flat_time, - GR_spr.rise_time + GR_spr.flat_time + GR_spr.fall_time] -GR7_amp = [GR_acq.amplitude, GR_spr.amplitude, GR_spr.amplitude, 0] -GR7 = make_extended_trapezoid(channel='x', times=GR7_times, amplitudes=GR7_amp) - -# Fill-times -tex = GS1.t[-1] + GS2.t[-1] + GS3.t[-1] -tref = GS4.t[-1] + GS5.t[-1] + GS7.t[-1] + readout_time -tend = GS4.t[-1] + GS5.t[-1] -TE_train = tex + n_echo * tref + tend -TR_fill = (TR - n_slices * TE_train) / n_slices # Round to gradient raster - -TR_fill = system.grad_raster_time * round(TR_fill / system.grad_raster_time) -if TR_fill < 0: - TR_fill = 1e-3 - warnings.warn(f'TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms') -else: - print(f'TR fill: {1000 * TR_fill} ms') -delay_TR = make_delay(TR_fill) -delay_end = make_delay(5) - -# ====== -# CONSTRUCT SEQUENCE -# ====== -# Define sequence blocks -for k_ex in range(n_ex): - for s in range(n_slices): - rfex.freq_offset = GS_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - rfref.freq_offset = GS_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - # Align the phase for off-center slices - rfex.phase_offset = rfex_phase - 2 * math.pi * rfex.freq_offset * calc_rf_center(rfex)[0] - rfref.phase_offset = rfref_phase - 2 * math.pi * rfref.freq_offset * calc_rf_center(rfref)[0] - - seq.add_block(GS1) - seq.add_block(GS2, rfex) - seq.add_block(GS3, GR3) - - for k_ech in range(n_echo): - if k_ex >= 0: - phase_area = phase_areas[k_ech] - else: - phase_area = 0 - - GP_pre = make_trapezoid(channel='y', system=system, area=phase_area, duration=t_sp, rise_time=dG) - GP_rew = make_trapezoid(channel='y', system=system, area=-phase_area, duration=t_sp, rise_time=dG) - - seq.add_block(GS4, rfref) - seq.add_block(GS5, GR5, GP_pre) - - if k_ex >= 0: - seq.add_block(GR6, adc) - else: - seq.add_block(GR6) - - seq.add_block(GS7, GR7, GP_rew) - - seq.add_block(GS4) - seq.add_block(GS5) - seq.add_block(delay_TR) - -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 -# ====== -seq.plot() - -ktraj_adc, ktraj, t_excitation, t_refocusing, _ = seq.calculate_kspace() -plt.plot(ktraj.T) # Plot the entire k-space trajectory -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0], ktraj_adc[1], 'r.') -plt.show() - -seq.write('haste_pypulseq.seq') diff --git a/pypulseq/seq_examples/scripts/write_tse.py b/pypulseq/seq_examples/scripts/write_tse.py deleted file mode 100644 index a897374..0000000 --- a/pypulseq/seq_examples/scripts/write_tse.py +++ /dev/null @@ -1,200 +0,0 @@ -import math -import warnings - -import numpy as np -from matplotlib import pyplot as plt - -import pypulseq as pp - -# ====== -# SETUP -# ====== -dG = 250e-6 - -# Set system limits -system = pp.Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=100e-6, - rf_dead_time=100e-6, adc_dead_time=10e-6) - -seq = pp.Sequence(system) # Create a new sequence object -# Define FOV and resolution -fov = 256e-3 -Nx, Ny = 128, 128 -n_echo = 16 -n_slices = 1 -rf_flip = 180 -if isinstance(rf_flip, int): - rf_flip = np.zeros(n_echo) + rf_flip -slice_thickness = 5e-3 -TE = 12e-3 -TR = 2000e-3 -TE_eff = 60e-3 -k0 = round(TE_eff / TE) -pe_type = 'linear' - -readout_time = 6.4e-3 + 2 * system.adc_dead_time -t_ex = 2.5e-3 -t_exwd = t_ex + system.rf_ringdown_time + system.rf_dead_time -t_ref = 2e-3 -t_refwd = t_ref + system.rf_ringdown_time + system.rf_dead_time -t_sp = 0.5 * (TE - readout_time - t_refwd) -t_spex = 0.5 * (TE - t_exwd - t_refwd) -fsp_r = 1 -fsp_s = 0.5 - -rf_ex_phase = np.pi / 2 -rf_ref_phase = 0 - -# ====== -# CREATE EVENTS -# ====== -flip_ex = 90 * np.pi / 180 -rf_ex, gz, _ = pp.make_sinc_pulse(flip_angle=flip_ex, system=system, duration=t_ex, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, phase_offset=rf_ex_phase, return_gz=True) -gs_ex = pp.make_trapezoid(channel='z', system=system, amplitude=gz.amplitude, flat_time=t_exwd, rise_time=dG) - -flip_ref = rf_flip[0] * np.pi / 180 -rf_ref, gz, _ = pp.make_sinc_pulse(flip_angle=flip_ref, system=system, duration=t_ref, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=4, phase_offset=rf_ref_phase, use='refocusing', - return_gz=True) -gs_ref = pp.make_trapezoid(channel='z', system=system, amplitude=gs_ex.amplitude, flat_time=t_refwd, rise_time=dG) - -ags_ex = gs_ex.area / 2 -gs_spr = pp.make_trapezoid(channel='z', system=system, area=ags_ex * (1 + fsp_s), duration=t_sp, rise_time=dG) -gs_spex = pp.make_trapezoid(channel='z', system=system, area=ags_ex * fsp_s, duration=t_spex, rise_time=dG) - -delta_k = 1 / fov -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 - 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) - -agr_spr = gr_spr.area -agr_preph = gr_acq.area / 2 + agr_spr -gr_preph = pp.make_trapezoid(channel='x', system=system, area=agr_preph, duration=t_spex, rise_time=dG) - -# Phase-encoding -n_ex = math.floor(Ny / n_echo) -pe_steps = np.arange(1, n_echo * n_ex + 1) - 0.5 * n_echo * n_ex - 1 -if divmod(n_echo, 2)[1] == 0: - pe_steps = np.roll(pe_steps, -round(n_ex / 2)) -pe_order = pe_steps.reshape((n_ex, n_echo), order='F').T -phase_areas = pe_order * delta_k - -# Split gradients and recombine into blocks -gs1_times = [0, gs_ex.rise_time] -gs1_amp = [0, gs_ex.amplitude] -gs1 = pp.make_extended_trapezoid(channel='z', times=gs1_times, amplitudes=gs1_amp) - -gs2_times = [0, gs_ex.flat_time] -gs2_amp = [gs_ex.amplitude, gs_ex.amplitude] -gs2 = pp.make_extended_trapezoid(channel='z', times=gs2_times, amplitudes=gs2_amp) - -gs3_times = [0, gs_spex.rise_time, gs_spex.rise_time + gs_spex.flat_time, - gs_spex.rise_time + gs_spex.flat_time + gs_spex.fall_time] -gs3_amp = [gs_ex.amplitude, gs_spex.amplitude, gs_spex.amplitude, gs_ref.amplitude] -gs3 = pp.make_extended_trapezoid(channel='z', times=gs3_times, amplitudes=gs3_amp) - -gs4_times = [0, gs_ref.flat_time] -gs4_amp = [gs_ref.amplitude, gs_ref.amplitude] -gs4 = pp.make_extended_trapezoid(channel='z', times=gs4_times, amplitudes=gs4_amp) - -gs5_times = [0, gs_spr.rise_time, gs_spr.rise_time + gs_spr.flat_time, - gs_spr.rise_time + gs_spr.flat_time + gs_spr.fall_time] -gs5_amp = [gs_ref.amplitude, gs_spr.amplitude, gs_spr.amplitude, 0] -gs5 = pp.make_extended_trapezoid(channel='z', times=gs5_times, amplitudes=gs5_amp) - -gs7_times = [0, gs_spr.rise_time, gs_spr.rise_time + gs_spr.flat_time, - gs_spr.rise_time + gs_spr.flat_time + gs_spr.fall_time] -gs7_amp = [0, gs_spr.amplitude, gs_spr.amplitude, gs_ref.amplitude] -gs7 = pp.make_extended_trapezoid(channel='z', times=gs7_times, amplitudes=gs7_amp) - -# Readout gradient -gr3 = gr_preph - -gr5_times = [0, gr_spr.rise_time, gr_spr.rise_time + gr_spr.flat_time, - gr_spr.rise_time + gr_spr.flat_time + gr_spr.fall_time] -gr5_amp = [0, gr_spr.amplitude, gr_spr.amplitude, gr_acq.amplitude] -gr5 = pp.make_extended_trapezoid(channel='x', times=gr5_times, amplitudes=gr5_amp) - -gr6_times = [0, readout_time] -gr6_amp = [gr_acq.amplitude, gr_acq.amplitude] -gr6 = pp.make_extended_trapezoid(channel='x', times=gr6_times, amplitudes=gr6_amp) - -gr7_times = [0, gr_spr.rise_time, gr_spr.rise_time + gr_spr.flat_time, - gr_spr.rise_time + gr_spr.flat_time + gr_spr.fall_time] -gr7_amp = [gr_acq.amplitude, gr_spr.amplitude, gr_spr.amplitude, 0] -gr7 = pp.make_extended_trapezoid(channel='x', times=gr7_times, amplitudes=gr7_amp) - -# Fill-times -t_ex = gs1.t[-1] + gs2.t[-1] + gs3.t[-1] -t_ref = gs4.t[-1] + gs5.t[-1] + gs7.t[-1] + readout_time -t_end = gs4.t[-1] + gs5.t[-1] -TE_train = t_ex + n_echo * t_ref + t_end -TR_fill = (TR - n_slices * TE_train) / n_slices -TR_fill = system.grad_raster_time * round(TR_fill / system.grad_raster_time) # Round to gradient raster -if TR_fill < 0: - TR_fill = 1e-3 - warnings.warn(f'TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms') -else: - print(f'TR fill: {1000 * TR_fill} ms') -delay_TR = pp.make_delay(TR_fill) - -# ====== -# CONSTRUCT SEQUENCE -# ====== -for k_ex in range(n_ex + 1): - for s in range(n_slices): - rf_ex.freq_offset = gs_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - rf_ref.freq_offset = gs_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) - rf_ex.phase_offset = rf_ex_phase - 2 * np.pi * rf_ex.freq_offset * pp.calc_rf_center(rf_ex)[0] - rf_ref.phase_offset = rf_ref_phase - 2 * np.pi * rf_ref.freq_offset * pp.calc_rf_center(rf_ref)[0] - - seq.add_block(gs1) - seq.add_block(gs2, rf_ex) - seq.add_block(gs3, gr3) - - for k_echo in range(n_echo): - if k_ex > 0: - phase_area = phase_areas[k_echo, k_ex - 1] - else: - phase_area = 0.0 # 0.0 and not 0 because -phase_area should successfully result in negative zero - - gp_pre = pp.make_trapezoid(channel='y', system=system, area=phase_area, duration=t_sp, rise_time=dG) - gp_rew = pp.make_trapezoid(channel='y', system=system, area=-phase_area, duration=t_sp, rise_time=dG) - seq.add_block(gs4, rf_ref) - seq.add_block(gs5, gr5, gp_pre) - if k_ex > 0: - seq.add_block(gr6, adc) - else: - seq.add_block(gr6) - - seq.add_block(gs7, gr7, gp_rew) - - seq.add_block(gs4) - 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 -# ====== -seq.plot() - -# Plot k-spaces -ktraj_adc, ktraj, t_excitation, t_refocusing, _ = seq.calculate_kspace() -plt.plot(ktraj.T) # Plot the entire k-space trajectory -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0], ktraj_adc[1], 'r.') -plt.show() - -seq.write('tse_pypulseq.seq') diff --git a/pypulseq/seq_examples/scripts/write_ute.py b/pypulseq/seq_examples/scripts/write_ute.py deleted file mode 100644 index 3c175c9..0000000 --- a/pypulseq/seq_examples/scripts/write_ute.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -A very basic UTE-like sequence, without ramp-sampling, ramp-RF. Achieves TE in the range of 300-400 us -""" -from copy import copy - -import numpy as np -from matplotlib import pyplot as plt - -import pypulseq as pp - -# ====== -# SETUP -# ====== -seq = pp.Sequence() # Create a new sequence object -# Define FOV and resolution -fov = 250e-3 -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 # 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 - -rf_spoiling_inc = 117 # RF spoiling increment - -# Set system limits -system = pp.Opts(max_grad=28, grad_unit='mT/m', max_slew=100, slew_unit='T/m/s', rf_ringdown_time=20e-6, - rf_dead_time=100e-6, adc_dead_time=10e-6) - -# ====== -# CREATE EVENTS -# ====== -# Create alpha-degree slice selection pulse and gradient -rf, gz, gz_reph = pp.make_sinc_pulse(flip_angle=alpha * np.pi / 180, duration=1e-3, slice_thickness=slice_thickness, - apodization=0.5, time_bw_product=2, center_pos=1, system=system, return_gz=True) - -# Align RO asymmetry to ADC samples -Nxo = np.round(ro_os * Nx) -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) -gx_pre = pp.make_trapezoid(channel='x', area=-(gx.area - ro_area) / 2 - ro_area / 2 * (1 - ro_asymmetry), system=system) - -# Gradient spoiling -gx_spoil = pp.make_trapezoid(channel='x', area=0.2 * Nx * delta_k, system=system) - -# Calculate timing -TE = gz.fall_time + pp.calc_duration(gx_pre, gz_reph) + gx.rise_time + adc.dwell * Nxo / 2 * (1 - ro_asymmetry) -delay_TR = np.ceil((TR - pp.calc_duration(gx_pre, gz_reph) - pp.calc_duration(gz) - pp.calc_duration( - gx)) / seq.grad_raster_time) * seq.grad_raster_time -assert np.all(delay_TR >= pp.calc_duration(gx_spoil)) - -print(f'TE = {TE * 1e6:.0f} us') - -if pp.calc_duration(gz_reph) > pp.calc_duration(gx_pre): - gx_pre.delay = pp.calc_duration(gz_reph) - pp.calc_duration(gx_pre) - -rf_phase = 0 -rf_inc = 0 - -# ====== -# CONSTRUCT SEQUENCE -# ====== -for i in range(Nr): - for c in range(2): - rf.phase_offset = rf_phase / 180 * np.pi - adc.phase_offset = rf_phase / 180 * np.pi - rf_inc = np.mod(rf_inc + rf_spoiling_inc, 360.0) - rf_phase = np.mod(rf_phase + rf_inc, 360.0) - - gz.amplitude = -gz.amplitude # Alternate GZ amplitude - gz_reph.amplitude = -gz_reph.amplitude - - seq.add_block(rf, gz) - phi = delta * i - - gpc = copy(gx_pre) - gps = copy(gx_pre) - gpc.amplitude = gx_pre.amplitude * np.cos(phi) - gps.amplitude = gx_pre.amplitude * np.sin(phi) - gps.channel = 'y' - - grc = copy(gx) - grs = copy(gx) - grc.amplitude = gx.amplitude * np.cos(phi) - grs.amplitude = gx.amplitude * np.sin(phi) - grs.channel = 'y' - - gsc = copy(gx_spoil) - gss = copy(gx_spoil) - gsc.amplitude = gx_spoil.amplitude * np.cos(phi) - gss.amplitude = gx_spoil.amplitude * np.sin(phi) - gss.channel = 'y' - - seq.add_block(gpc, gps, gz_reph) - 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 -# ====== -seq.plot() - -# Plot gradients to check for gaps and optimality of the timing -gw = seq.gradient_waveforms() -plt.plot(gw.T) # Plot the entire gradient shape - -# Trajectory calculation -ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc = seq.calculate_kspace() - -# Plot k-spaces -time_axis = np.arange(ktraj.shape[1]) * seq.grad_raster_time -plt.figure() -plt.plot(time_axis, ktraj.T) # Plot the entire k-space trajectory -plt.plot(t_adc, ktraj_adc[0], '.') # Sampling points on the kx-axis -plt.figure() -plt.plot(ktraj[0], ktraj[1], 'b') # 2D plot -plt.axis('equal') # Enforce aspect ratio for the correct trajectory display -plt.plot(ktraj_adc[0], ktraj_adc[1], 'r.') # Plot the sampling points - -plt.show() - -seq.set_definition('FOV', [fov, fov, slice_thickness]) -seq.set_definition('Name', 'UTE') - -seq.write('ute_pypulseq.seq') diff --git a/pypulseq/split_gradient.py b/pypulseq/split_gradient.py deleted file mode 100644 index c400678..0000000 --- a/pypulseq/split_gradient.py +++ /dev/null @@ -1,71 +0,0 @@ -from types import SimpleNamespace -from typing import Tuple - -import numpy as np - -from pypulseq.calc_duration import calc_duration -from pypulseq.make_extended_trapezoid import make_extended_trapezoid -from pypulseq.opts import Opts - - -def split_gradient(grad: SimpleNamespace, - system: Opts = Opts()) -> Tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace]: - """ - Split gradient waveform `grad` into two gradient waveforms at the center. - - Parameters - ---------- - grad : array_like - Gradient waveform to be split into two gradient waveforms. - system : Opts, optional, default=Opts() - System limits. - - Returns - ------- - grad1, grad2 : numpy.ndarray - Split gradient waveforms. - - Raises - ------ - ValueError - If arbitrary gradients are passed. - If non-gradient event is passed. - """ - grad_raster_time = system.grad_raster_time - total_length = calc_duration(grad) - - if grad.type == 'trap': - ch = grad.channel - grad.delay = round(grad.delay / grad_raster_time) * grad_raster_time - grad.rise_time = round(grad.rise_time / grad_raster_time) * grad_raster_time - grad.flat_time = round(grad.flat_time / grad_raster_time) * grad_raster_time - grad.fall_time = round(grad.fall_time / grad_raster_time) * grad_raster_time - - times = [0, grad.rise_time] - amplitudes = [0, grad.amplitude] - ramp_up = make_extended_trapezoid(channel=ch, system=system, times=times, amplitudes=amplitudes, - skip_check=True) - ramp_up.delay = grad.delay - - times = [0, grad.fall_time] - amplitudes = [grad.amplitude, 0] - ramp_down = make_extended_trapezoid(channel=ch, system=system, times=times, amplitudes=amplitudes, - skip_check=True) - ramp_down.delay = total_length - grad.fall_time - ramp_down.t = ramp_down.t * grad_raster_time - - flat_top = SimpleNamespace() - flat_top.type = 'grad' - flat_top.channel = ch - flat_top.delay = grad.delay + grad.rise_time - flat_top.t = np.arange(step=grad_raster_time, - stop=ramp_down.delay - grad_raster_time - grad.delay - grad.rise_time) - flat_top.waveform = grad.amplitude * np.ones(len(flat_top.t)) - flat_top.first = grad.amplitude - flat_top.last = grad.amplitude - - return ramp_up, flat_top, ramp_down - elif grad.type == 'grad': - raise ValueError('Splitting of arbitrary gradients is not implemented yet.') - else: - raise ValueError('Splitting of unsupported event.') diff --git a/pypulseq/split_gradient_at.py b/pypulseq/split_gradient_at.py deleted file mode 100644 index b1ad4b9..0000000 --- a/pypulseq/split_gradient_at.py +++ /dev/null @@ -1,91 +0,0 @@ -from types import SimpleNamespace -from typing import Tuple, Union - -import numpy as np - -from pypulseq.make_extended_trapezoid import make_extended_trapezoid -from pypulseq.opts import Opts - - -def split_gradient_at(grad: SimpleNamespace, time_point: float, - system: Opts = Opts()) -> Union[SimpleNamespace, Tuple[SimpleNamespace, SimpleNamespace]]: - """ - Split gradient waveform `grad` into two at time point `time_point`. - - Parameters - ---------- - grad : SimpleNamespace - Gradient event to be split into two gradient events. - time_point : float - Time point at which `grad` will be split into two gradient waveforms. - system : Opts, optional, default=Opts() - System limits. - - Returns - ------- - grad1, grad2 : SimpleNamespace - Gradient waveforms after splitting. - - Raises - ------ - ValueError - If non-gradient event is passed. - """ - grad_raster_time = system.grad_raster_time - - time_index = round(time_point / grad_raster_time) - time_point = round(time_index * grad_raster_time, 6) # Work around floating-point arithmetic limitation - time_index += 1 - - if grad.type == 'trap': - ch = grad.channel - grad.delay = round(grad.delay / grad_raster_time) * grad_raster_time - grad.rise_time = round(grad.rise_time / grad_raster_time) * grad_raster_time - grad.flat_time = round(grad.flat_time / grad_raster_time) * grad_raster_time - grad.fall_time = round(grad.fall_time / grad_raster_time) * grad_raster_time - - if grad.flat_time == 0: - times = [0, grad.rise_time, grad.rise_time + grad.fall_time] - amplitudes = [0, grad.amplitude, 0] - else: - times = [0, grad.rise_time, grad.rise_time + grad.flat_time, - grad.rise_time + grad.flat_time + grad.fall_time] - amplitudes = [0, grad.amplitude, grad.amplitude, 0] - - if time_point < grad.delay: - times = np.insert(grad.delay + times, 0, 0) - amplitudes = [0, amplitudes] - grad.delay = 0 - - amplitudes = np.array(amplitudes) - times = np.array(times).round(6) # Work around floating-point arithmetic limitation - - amp_tp = np.interp(x=time_point, xp=times, fp=amplitudes) - times1 = np.append(times[np.where(times < time_point)], time_point) - amplitudes1 = np.append(amplitudes[np.where(times < time_point)], amp_tp) - times2 = np.insert(times[times > time_point], 0, time_point) - time_point - amplitudes2 = np.insert(amplitudes[times > time_point], 0, amp_tp) - - grad1 = make_extended_trapezoid(channel=ch, system=system, times=times1, amplitudes=amplitudes1, - skip_check=True) - grad1.delay = grad.delay - grad2 = make_extended_trapezoid(channel=ch, system=system, times=times2, amplitudes=amplitudes2, - skip_check=True) - grad2.delay = time_point - return grad1, grad2 - elif grad.type == 'grad': - if time_index == 1 or time_index >= len(grad.t): - return grad - else: - grad1 = grad - grad2 = grad - 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:] - time_point - grad2.waveform = grad.waveform[time_index:] - return grad1, grad2 - else: - raise ValueError('Splitting of unsupported event.') diff --git a/pypulseq/supported_labels.py b/pypulseq/supported_labels.py deleted file mode 100644 index bb3e18c..0000000 --- a/pypulseq/supported_labels.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Tuple - - -def get_supported_labels() -> Tuple[str, str, str, str, str, str, str, str, str, str, str, str]: - """ - Returns - ------- - tuple - Tuple of supported labels. - """ - return 'SLC', 'SEG', 'REP', 'AVG', 'SET', 'ECO', 'PHS', 'LIN', 'PAR', 'NAV', 'REV', 'SMS' diff --git a/pypulseq/traj_to_grad.py b/pypulseq/traj_to_grad.py deleted file mode 100644 index f5d5e04..0000000 --- a/pypulseq/traj_to_grad.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Tuple - -import numpy as np - -from pypulseq.opts import Opts - - -def traj_to_grad(k: np.ndarray, raster_time: float = Opts().grad_raster_time) -> Tuple[np.ndarray, np.ndarray]: - """ - Convert k-space trajectory `k` into gradient waveform in compliance with `raster_time` gradient raster time. - - Parameters - ---------- - k : numpy.ndarray - K-space trajectory to be converted into gradient waveform. - raster_time : float, optional, default=Opts().grad_raster_time - Gradient raster time. - - Returns - ------- - g : numpy.ndarray - Gradient waveform. - sr : numpy.ndarray - Slew rate. - """ - g = (k[1:] - k[:-1]) / raster_time - sr0 = (g[1:] - g[:-1]) / raster_time - sr = np.zeros(len(sr0) + 1) - sr[0] = sr0[0] - sr[1:-1] = 0.5 * (sr0[-1] + sr0[1:]) - sr[-1] = sr0[-1] - - return g, sr diff --git a/recon/B0Correction/B0Corrector.py b/recon/B0Correction/B0Corrector.py old mode 100644 new mode 100755 diff --git a/recon/B0Correction/OCTOPUS/ORC.py b/recon/B0Correction/OCTOPUS/ORC.py old mode 100644 new mode 100755 diff --git a/recon/B0Correction/OCTOPUS/__init__.py b/recon/B0Correction/OCTOPUS/__init__.py old mode 100644 new mode 100755 diff --git a/recon/B0Correction/OCTOPUS/imtransforms.py b/recon/B0Correction/OCTOPUS/imtransforms.py old mode 100644 new mode 100755 diff --git a/recon/B0Correction/__init__.py b/recon/B0Correction/__init__.py old mode 100644 new mode 100755 diff --git a/recon/DICOM/DICOM_utils.py b/recon/DICOM/DICOM_utils.py old mode 100644 new mode 100755 index 7e61ee7..b6da7b9 --- a/recon/DICOM/DICOM_utils.py +++ b/recon/DICOM/DICOM_utils.py @@ -43,13 +43,9 @@ def write_dicom( instance_counter = 1 print(f"Writing {ndarray_dims[-1]} DICOMs") - - val_max = np.max(np.abs(image_ndarray)) - for slc_id in range(ndarray_dims[-1]): - # Generate magnitude image per slice - # Normalize the value range to avoid clipping (max 32k) - pixel_data = np.abs(image_ndarray[..., slc_id]) / val_max * 30000 + """Generate magnitude image per slice""" + pixel_data = 100 * np.abs(image_ndarray[..., slc_id]) pixel_data = np.uint16(pixel_data) """ Create and populate the DICOM header """ diff --git a/recon/__init__.py b/recon/__init__.py old mode 100644 new mode 100755 diff --git a/recon/gradient_delay.py b/recon/gradient_delay.py old mode 100644 new mode 100755 diff --git a/recon/image_filters/__init__.py b/recon/image_filters/__init__.py old mode 100644 new mode 100755 diff --git a/recon/image_filters/denoise.py b/recon/image_filters/denoise.py old mode 100644 new mode 100755 diff --git a/recon/ismrmrd/numpy_to_ismrmrd.py b/recon/ismrmrd/numpy_to_ismrmrd.py old mode 100644 new mode 100755 diff --git a/recon/ismrmrd/simulation.py b/recon/ismrmrd/simulation.py old mode 100644 new mode 100755 diff --git a/recon/ismrmrd/test.h5 b/recon/ismrmrd/test.h5 old mode 100644 new mode 100755 diff --git a/recon/ismrmrd/transform.py b/recon/ismrmrd/transform.py old mode 100644 new mode 100755 diff --git a/recon/kspaceFiltering/__init__.py b/recon/kspaceFiltering/__init__.py old mode 100644 new mode 100755 diff --git a/recon/kspaceFiltering/kspace_filtering.py b/recon/kspaceFiltering/kspace_filtering.py old mode 100644 new mode 100755 diff --git a/recon/recon_utils/__init__.py b/recon/recon_utils/__init__.py old mode 100644 new mode 100755 diff --git a/recon/recon_utils/imaging.py b/recon/recon_utils/imaging.py old mode 100644 new mode 100755 diff --git a/recon/recon_utils/kspace2img.py b/recon/recon_utils/kspace2img.py old mode 100644 new mode 100755 diff --git a/recon/recon_utils/visualization.py b/recon/recon_utils/visualization.py old mode 100644 new mode 100755 diff --git a/recon/test.py b/recon/test.py old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index 9465240..8e913f1 --- a/requirements.txt +++ b/requirements.txt @@ -32,15 +32,14 @@ deap~=1.4.1 imageio~=2.19.3 numpy==1.25 tqdm~=4.66.1 -numba==0.58.0 - +pypulseq==1.4.2 # Dependencies of the GUI service PyQt5~=5.15.9 qtawesome~=1.2.3 pyqtdarktheme~=2.1.0 sip~=6.7.12 pyqtgraph~=0.13.3 -GDCM +# GDCM pylibjpeg pylibjpeg-libjpeg diff --git a/run_acq.py b/run_acq.py old mode 100644 new mode 100755 index 251dab9..773edde --- a/run_acq.py +++ b/run_acq.py @@ -2,3 +2,4 @@ if __name__ == "__main__": services.acq.main.run() + diff --git a/run_recon.py b/run_recon.py old mode 100644 new mode 100755 diff --git a/run_ui.py b/run_ui.py old mode 100644 new mode 100755 diff --git a/sequences/FID.py b/sequences/FID.py old mode 100644 new mode 100755 index 7425db4..207fc18 --- a/sequences/FID.py +++ b/sequences/FID.py @@ -17,9 +17,7 @@ import common.logger as logger from common.types import ResultItem import common.helper as helper - log = logger.get_logger() - from common.ipc import Communicator ipc_comm = Communicator(Communicator.ACQ) @@ -30,6 +28,7 @@ class SequenceFID(PulseqSequence, registry_key=Path(__file__).stem): param_FA: int = 90 param_ADC_samples: int = 4096 param_ADC_duration: int = 6400 + param_NSA: int = 1 @classmethod def get_readable_name(self) -> str: @@ -49,6 +48,7 @@ def get_parameters(self) -> dict: "FA": self.param_FA, "ADC_samples": self.param_ADC_samples, "ADC_duration": self.param_ADC_duration, + "NSA": self.param_NSA } @classmethod @@ -67,6 +67,7 @@ def set_parameters(self, parameters, scan_task) -> bool: self.param_FA = parameters["FA"] self.param_ADC_samples = parameters["ADC_samples"] self.param_ADC_duration = parameters["ADC_duration"] + self.param_NSA = parameters["NSA"] except: self.problem_list.append("Invalid parameters provided") return False @@ -76,6 +77,7 @@ def write_parameters_to_ui(self, widget) -> bool: widget.FA_SpinBox.setValue(self.param_FA) widget.ADC_samples_SpinBox.setValue(self.param_ADC_samples) widget.ADC_duration_SpinBox.setValue(self.param_ADC_duration) + widget.NSA_SpinBox.setValue(self.param_NSA) return True def read_parameters_from_ui(self, widget, scan_task) -> bool: @@ -83,6 +85,7 @@ def read_parameters_from_ui(self, widget, scan_task) -> bool: self.param_FA = widget.FA_SpinBox.value() self.param_ADC_samples = widget.ADC_samples_SpinBox.value() self.param_ADC_duration = widget.ADC_duration_SpinBox.value() + self.param_NSA = widget.NSA_SpinBox.value() self.validate_parameters(scan_task) return self.is_valid() @@ -109,7 +112,7 @@ def run_sequence(self, scan_task) -> bool: rxd, rx_t = run_pulseq( seq_file=self.seq_file_path, - rf_center=cfg.LARMOR_FREQ, + rf_center=scan_task.adjustment.rf.larmor_frequency, tx_t=1, grad_t=10, tx_warmup=100, @@ -125,25 +128,85 @@ def run_sequence(self, scan_task) -> bool: raw_filename="raw", ) - log.info("Plotting results...") + # Display the data + + # Compute the average + rxd_rs = np.reshape(rxd, (int(rxd.shape[0]/self.param_NSA), self.param_NSA), order='F') + log.info("New shape of rx data:", rxd_rs.shape) + rxd_avg = (np.average(rxd_rs, axis=1)) + log.info("Done running sequence " + self.get_name()) + log.info("Plotting figures") + plt.clf() - plt.title("ADC Signal") + plt.title(f"ADC Signal") plt.grid(True, color="#333") - plt.plot(np.abs(rxd)) - - file = open(self.get_working_folder() + "/other/fid.plot", "wb") + log.info("Plotting averaged raw signal") + plt.plot(np.abs(rxd_avg)) + + file = open(self.get_working_folder() + "/other/adc.plot", "wb") fig = plt.gcf() pickle.dump(fig, file) file.close() - result = ResultItem() result.name = "ADC" - result.description = "Recorded ADC signal" + result.description = "Acquired ADC signal" result.type = "plot" - result.primary = True result.autoload_viewer = 1 - result.file_path = "other/fid.plot" - scan_task.results.append(result) + result.file_path = "other/adc.plot" + scan_task.results.insert(0, result) + + plt.clf() + plt.title(f"FFT of Signal") + recon = np.fft.fftshift(np.fft.ifft(np.fft.fftshift(rxd_avg))) + plt.grid(True, color="#333") + plt.plot(np.abs(recon)) + file = open(self.get_working_folder() + "/other/fft.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + result = ResultItem() + result.name = "FFT" + result.description = "FFT of ADC signal" + result.type = "plot" + result.autoload_viewer = 2 + result.primary = True + result.file_path = "other/fft.plot" + scan_task.results.insert(1, result) + + # Save the raw data file + log.info("Saving rawdata, sequence " + self.get_name()) + self.raw_file_path = self.get_working_folder() + "/rawdata/raw.npy" + np.save(self.raw_file_path, rxd) + + + # file = open(self.get_working_folder() + "/other/fid.plot", "wb") + # fig = plt.gcf() + # pickle.dump(fig, file) + # file.close() + + # result = ResultItem() + # result.name = "ADC" + # result.description = "Recorded ADC signal" + # result.type = "plot" + # result.primary = True + # result.autoload_viewer = 1 + # result.file_path = "other/fid.plot" + # scan_task.results.insert(result) + + # file = open(self.get_working_folder() + "/other/fid.plot", "wb") + # fig = plt.gcf() + # pickle.dump(fig, file) + # file.close() + + # result = ResultItem() + # result.name = "ADC" + # result.description = "Recorded ADC signal" + # result.type = "plot" + # result.primary = True + # result.autoload_viewer = 1 + # result.file_path = "other/fid.plot" + # scan_task.results.append(result) + log.info("Done running sequence " + self.get_name()) return True @@ -152,7 +215,7 @@ def generate_pulseq(self) -> bool: output_file = self.seq_file_path alpha1 = self.param_FA - alpha1_duration = 200e-6 + alpha1_duration = 50e-6 adc_num_samples = self.param_ADC_samples adc_duration = self.param_ADC_duration / 1e6 # us to s @@ -182,7 +245,7 @@ def generate_pulseq(self) -> bool: # ====== rf1 = pp.make_block_pulse( - flip_angle=alpha1 * math.pi / 180, + flip_angle = alpha1 * math.pi / 180, # change this back by removing the 2 duration=alpha1_duration, delay=0e-6, system=system, diff --git a/sequences/FID/interface.ui b/sequences/FID/interface.ui old mode 100644 new mode 100755 index 52ba1a5..f90b7bf --- a/sequences/FID/interface.ui +++ b/sequences/FID/interface.ui @@ -6,114 +6,165 @@ 0 0 - 820 - 440 + 785 + 468 Form - - - - - - - μs - - - - - - - 99999 - - - - - - - 8192 - - - - - - - - - - - - - - ADC Duration - - - - - - - - 100 - 0 - - - - ADC Samples - - - - - - - Qt::Vertical - - - - 20 - 352 - - - - - - - - Flip Angle - - - - - - - 360 - - - - - - - deg - - - - - - - - - Qt::Horizontal - - - - 338 - 20 - - - - - + + + + + + + 10 + 100 + 82 + 26 + + + + Averages + + + + + + 110 + 100 + 83 + 26 + + + + 1 + + + + + + 10 + 10 + 68 + 17 + + + + Flip Angle + + + + + + 116 + 10 + 53 + 26 + + + + 360 + + + + + + 192 + 10 + 26 + 17 + + + + deg + + + + + + 10 + 42 + 100 + 17 + + + + + 100 + 0 + + + + ADC Samples + + + + + + 116 + 42 + 61 + 26 + + + + 8192 + + + + + + 192 + 42 + 16 + 17 + + + + + + + + + + 10 + 74 + 94 + 17 + + + + ADC Duration + + + + + + 116 + 74 + 70 + 26 + + + + 99999 + + + + + + 192 + 74 + 16 + 17 + + + + μs + + diff --git a/sequences/__init__.py b/sequences/__init__.py old mode 100644 new mode 100755 diff --git a/sequences/adj_frequency.py b/sequences/adj_frequency.py old mode 100644 new mode 100755 index a83ab08..c56b9d0 --- a/sequences/adj_frequency.py +++ b/sequences/adj_frequency.py @@ -1,5 +1,5 @@ from pathlib import Path - +import matplotlib.pyplot as plt import external.seq.adjustments_acq.config as cfg from external.seq.adjustments_acq.calibration import ( larmor_cal, @@ -7,10 +7,13 @@ load_plot_in_ui, ) from sequences.common.util import reading_json_parameter, writing_json_parameter - +import numpy as np from sequences import PulseqSequence # type: ignore from sequences.common import make_rf_se # type: ignore import common.logger as logger +import pickle +from common.types import ResultItem +import external.seq.adjustments_acq.scripts as scr log = logger.get_logger() @@ -20,7 +23,7 @@ class AdjFrequency(PulseqSequence, registry_key=Path(__file__).stem): param_TE: int = 20 param_TR: int = 250 param_NSA: int = 1 - param_ADC_samples: int = 2048 + param_ADC_samples: int = 256 param_ADC_duration: int = 6400 @classmethod @@ -40,8 +43,8 @@ def calculate_sequence(self, scan_task) -> bool: "NSA": self.param_NSA, "ADC_samples": self.param_ADC_samples, "ADC_duration": self.param_ADC_duration, - "FA1": cfg.DBG_FA_EXC, - "FA2": cfg.DBG_FA_REF, + "FA1": 90, + "FA2": 180, }, check_timing=True, output_file=self.seq_file_path, @@ -50,6 +53,15 @@ def calculate_sequence(self, scan_task) -> bool: self.calculated = True log.info("Done calculating sequence " + self.get_name()) return True + + def get_freq_offset(self, x, t): + adc_time = t[-1] + X = np.fft.fft(x) + fmax = 1 / adc_time / 2 # -BW/2 to + BW/2 + f = np.linspace(-fmax, fmax,t.shape[0]) + del_f = f[-1] - f[-2] + freq_offset = (np.argmax(np.abs(X)) - int(0.5 * t.shape[0])) * del_f + return freq_offset def run_sequence(self, scan_task) -> bool: log.info("Running sequence " + self.get_name()) @@ -60,126 +72,81 @@ def run_sequence(self, scan_task) -> bool: working_folder = self.get_working_folder() # TODO: Convert to classes later (using external packages for now) - - # max_freq, max_snr_freq, data_dict, fig_signal1, fig_noise1 = larmor_step_search( - # seq_file=self.seq_file_path, - # # step_search_center=configuration_data.rf_parameters.larmor_frequency_MHz, - # # Debug: Always start at 1.83 - # step_search_center=1.83, - # steps=20, - # # step_bw_MHz=10e-3, - # step_bw_MHz=1e-3, - # plot=True, # For Debug - # shim_x=cfg.SHIM_X, - # shim_y=cfg.SHIM_Y, - # shim_z=cfg.SHIM_Z, - # delay_s=1, - # gui_test=False, - # ) - - # plot_result_signal1 = load_plot_in_ui( - # working_folder=working_folder, - # file_name="plot_result_signal1", - # fig=fig_signal1, - # ) - # scan_task.results.append(plot_result_signal1) - # plot_result_noise1 = load_plot_in_ui( - # working_folder=working_folder, - # file_name="plot_result_noise1", - # fig=fig_noise1, - # ) - # scan_task.results.append(plot_result_noise1) - - # ( - # opt_max_freq, - # opt_max_snr_freq, - # data_dict, - # fig_signal2, - # fig_noise2, - # ) = larmor_step_search( - # seq_file=self.seq_file_path, - # step_search_center=max_freq, - # steps=20, - # # step_bw_MHz=5e-3, - # step_bw_MHz=0.5e-3, - # plot=True, # For Debug - # shim_x=cfg.SHIM_X, - # shim_y=cfg.SHIM_Y, - # shim_z=cfg.SHIM_Z, - # delay_s=1, - # gui_test=False, - # ) - - # log.info(f"Intermedite frequency = {opt_max_freq}") - - # plot_result_signal2 = load_plot_in_ui( - # working_folder=working_folder, - # file_name="plot_result_signal2", - # fig=fig_signal2, - # ) - # scan_task.results.append(plot_result_signal2) - # plot_result_noise2 = load_plot_in_ui( - # working_folder=working_folder, - # file_name="plot_result_noise2", - # fig=fig_noise2, - # ) - # scan_task.results.append(plot_result_noise2) - - opt_max_freq = 1.831 - opt_max_freq = 1.828 - - larmor_freq, data_dict, fig1 = larmor_cal( - seq_file=self.seq_file_path, - larmor_start=opt_max_freq, - iterations=20, - delay_s=1, - echo_count=1, - # step_size=0.6, - # step_size=0.1, - step_size=0.1, - plot=True, # For debug + # Run the experiment from seq file + rxd, rx_t = scr.run_pulseq( + self.seq_file_path, + rf_center=scan_task.adjustment.rf.larmor_frequency, + tx_t=1, + grad_t=10, + tx_warmup=100, shim_x=cfg.SHIM_X, shim_y=cfg.SHIM_Y, shim_z=cfg.SHIM_Z, - gui_test=False, + grad_cal=False, + save_np=False, + save_mat=False, + ) + + x = rxd + t = rx_t + adc_time = self.param_ADC_duration + X = np.fft.fft(x) + fmax = 1 / adc_time / 2 # -BW/2 to + BW/2 + f = np.linspace(-fmax, fmax,rxd.shape[0]) + del_f = f[-1] - f[-2] + freq_offset = (np.argmax(np.abs(X)) - int(0.5 * rxd.shape[0])) * del_f + + log.info( + f"Frequency offset (using peak signal): {freq_offset} MHz" ) - # plot_result1 = load_plot_in_ui( - # working_folder=working_folder, file_name="plot_result1", fig=fig1 - # ) - # scan_task.results.append(plot_result1) - - calibrated_larmor_freq, data_dict, fig2 = larmor_cal( - seq_file=self.seq_file_path, - larmor_start=larmor_freq, - iterations=20, - delay_s=1, - echo_count=1, - # step_size=0.2, - step_size=0.1, - plot=True, # For debug + + # freq_offset = get_freq_offset(self, rx_signal, rx_t) + scan_task.adjustment.rf.larmor_frequency += freq_offset + # Now scan again with the new frequency + rxd, rx_t = scr.run_pulseq( + self.seq_file_path, + rf_center=scan_task.adjustment.rf.larmor_frequency, + tx_t=1, + grad_t=10, + tx_warmup=100, shim_x=cfg.SHIM_X, shim_y=cfg.SHIM_Y, shim_z=cfg.SHIM_Z, - gui_test=False, + grad_cal=False, + save_np=False, + save_mat=False, ) - # plot_result2 = load_plot_in_ui( - # working_folder=working_folder, file_name="plot_result2", fig=fig2 - # ) - # scan_task.results.append(plot_result2) - + # rx_signal = data_dict["rxd"] + rx_signal = rxd + log.info('Read the signal') + plt.clf() + plt.title("ADC Signal Final") + plt.grid(True, color="#333") + plt.plot(np.abs(rx_signal)) + file = open(self.get_working_folder() + "/other/peak_frequency.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + + result = ResultItem() + result.name = "ADC" + result.description = "Recorded ADC signal" + result.type = "plot" + result.primary = True + result.autoload_viewer = 1 + result.file_path = "other/peak_frequency.plot" + scan_task.results.append(result) log.info( - f"Final Larmor frequency (using peak signal): {calibrated_larmor_freq} MHz" + f"Final Larmor frequency (using peak signal): {scan_task.adjustment.rf.larmor_frequency} MHz" ) - scan_task.adjustment.rf.larmor_frequency = calibrated_larmor_freq # Updating the Larmor frequency in the config.json file # TODO: Needs to be reworked - configuration_data.rf_parameters.larmor_frequency_MHz = calibrated_larmor_freq + configuration_data.rf_parameters.larmor_frequency_MHz = scan_task.adjustment.rf.larmor_frequency writing_json_parameter(config_data=configuration_data) # Reload the configuration -- otherwise it does not get updated until the next start cfg.update() - log.info("Done running sequence " + self.get_name()) return True diff --git a/sequences/adj_frequency_snr.py b/sequences/adj_frequency_snr.py old mode 100644 new mode 100755 index 2c65700..b8c0db1 --- a/sequences/adj_frequency_snr.py +++ b/sequences/adj_frequency_snr.py @@ -1,5 +1,4 @@ from pathlib import Path - import external.seq.adjustments_acq.config as cfg from external.seq.adjustments_acq.calibration import ( larmor_cal, @@ -10,28 +9,31 @@ from sequences import PulseqSequence # type: ignore from sequences.common import make_rf_se # type: ignore import common.logger as logger - +import matplotlib.pyplot as plt +import numpy as np +import pickle +from common.types import ResultItem log = logger.get_logger() class AdjFrequency(PulseqSequence, registry_key=Path(__file__).stem): # Sequence parameters - param_TE: int = 20 + param_TE: int = 10 param_TR: int = 250 param_NSA: int = 1 - param_ADC_samples: int = 4096 - param_ADC_duration: int = 6400 + param_ADC_samples: int = 512 + param_ADC_duration: int = 5120 @classmethod def get_readable_name(self) -> str: - return "Adjust Frequency (SNR)" + return "Adjust Frequency (SNR) [on startup]" def calculate_sequence(self, scan_task) -> bool: log.info("Calculating sequence " + self.get_name()) scan_task.processing.recon_mode = "bypass" self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" - + log.info('Working folder:'+ str(self.get_working_folder())) make_rf_se.pypulseq_rfse( inputs={ "TE": self.param_TE, @@ -59,113 +61,165 @@ def run_sequence(self, scan_task) -> bool: working_folder = self.get_working_folder() # TODO: Convert to classes later (using external packages for now) - + log.info("Starting frequency adjustment using SNR - coarse search") ( - max_freq, + max_peak_freq, max_snr_freq, data_dict, fig_snr_signal1, fig_snr_noise1, + best_snr_index, ) = larmor_step_search( seq_file=self.seq_file_path, - step_search_center=configuration_data.rf_parameters.larmor_frequency_MHz, - steps=30, - step_bw_MHz=10e-3, + step_search_center=scan_task.adjustment.rf.larmor_frequency, + steps=10, + step_bw_MHz=1e-3, plot=True, # For Debug shim_x=cfg.SHIM_X, shim_y=cfg.SHIM_Y, shim_z=cfg.SHIM_Z, delay_s=1, gui_test=False, + dummy_scans=1, ) - - plot_snr_result_signal1 = load_plot_in_ui( - working_folder=working_folder, - file_name="plot_snr_result_signal1", - fig=fig_snr_signal1, - ) - scan_task.results.append(plot_snr_result_signal1) - plot_snr_result_noise1 = load_plot_in_ui( - working_folder=working_folder, - file_name="plot_snr_result_noise1", - fig=fig_snr_noise1, - ) - scan_task.results.append(plot_snr_result_noise1) - + + log.info('Starting frequency adjustment using SNR - fine search') #TODO: pick peak frequency instead of SNR at this stage ( - opt_max_freq, - opt_max_snr_freq, - data_dict, - fig_snr_signal2, - fig_snr_noise2, + max_freq_fine, + max_snr_freq_fine, + data_dict_fine, + fig_snr_signal1, + fig_snr_noise1, + best_snr_index, ) = larmor_step_search( seq_file=self.seq_file_path, step_search_center=max_snr_freq, - steps=30, - step_bw_MHz=5e-3, + steps=10, + step_bw_MHz=0.1e-3, plot=True, # For Debug shim_x=cfg.SHIM_X, shim_y=cfg.SHIM_Y, shim_z=cfg.SHIM_Z, delay_s=1, gui_test=False, - ) - - plot_snr_result_signal2 = load_plot_in_ui( - working_folder=working_folder, - file_name="plot_snr_result_signal2", - fig=fig_snr_signal2, - ) - scan_task.results.append(plot_snr_result_signal2) - plot_snr_result_noise2 = load_plot_in_ui( - working_folder=working_folder, - file_name="plot_snr_result_noise2", - fig=fig_snr_noise2, - ) - scan_task.results.append(plot_snr_result_noise2) - - larmor_freq, data_dict, fig_snr1 = larmor_cal( + dummy_scans=1, + ) + + log.info('Starting frequency adjustment using SNR - second fine search') #TODO: pick peak frequency instead of SNR at this stage + ( + max_freq_fine, + max_snr_freq_fine2, + data_dict_fine, + fig_snr_signal1, + fig_snr_noise1, + best_snr_index, + ) = larmor_step_search( seq_file=self.seq_file_path, - larmor_start=opt_max_snr_freq, - iterations=10, - delay_s=1, - echo_count=1, - step_size=0.6, - plot=True, # For debug + step_search_center=max_snr_freq_fine, + steps=10, + step_bw_MHz=0.05e-3, + plot=True, # For Debug shim_x=cfg.SHIM_X, shim_y=cfg.SHIM_Y, shim_z=cfg.SHIM_Z, - gui_test=False, - ) - - plot_snr_result1 = load_plot_in_ui( - working_folder=working_folder, file_name="plot_snr_result1", fig=fig_snr1 - ) - scan_task.results.append(plot_snr_result1) - - calibrated_larmor_freq, data_dict, fig_snr2 = larmor_cal( - seq_file=self.seq_file_path, - larmor_start=larmor_freq, - iterations=10, delay_s=1, - echo_count=1, - step_size=0.2, - plot=True, # For debug - shim_x=cfg.SHIM_X, - shim_y=cfg.SHIM_Y, - shim_z=cfg.SHIM_Z, gui_test=False, - ) - - plot_snr_result2 = load_plot_in_ui( - working_folder=working_folder, file_name="plot_snr_result2", fig=fig_snr2 - ) - scan_task.results.append(plot_snr_result2) - + dummy_scans=1, + ) + + + # calibrated_larmor_freq = max_snr_freq_fine + calibrated_larmor_freq = max_snr_freq_fine2 + data_dict["rx_arr"] = data_dict_fine["rx_arr"] + log.info(f"Calibrated Larmor frequency (using SNR) - Coarse: {max_snr_freq} MHz") + log.info(f"Calibrated Larmor frequency (using SNR) - fine: {calibrated_larmor_freq} MHz") + + + + rx_signal = data_dict["rx_arr"] + log.info(f"Shape of rx signal: {rx_signal.shape}") + log.info('Read the signal') + + plt.clf() + plt.title("ADC Signal Final") + plt.grid(True, color="#333") + plt.plot(np.abs(rx_signal), label="ADC Signal") + plt.xlabel("Time [n]") + plt.ylabel("Amplitude [a.u.]") + file = open(self.get_working_folder() + "/other/plot_snr_result.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + + result = ResultItem() + result.name = "ADC" + result.description = "Recorded ADC signal" + result.type = "plot" + result.primary = True + result.autoload_viewer = 1 + result.file_path = "other/plot_snr_result.plot" + scan_task.results.append(result) + + + # plot_snr_result_signal1 = load_plot_in_ui( + # working_folder=working_folder, + # file_name="plot_snr_result_signal1", + # fig=fig_snr_signal1, + # ) + # scan_task.results.append(plot_snr_result_signal1) + # plot_snr_result_noise1 = load_plot_in_ui( + # working_folder=working_folder, + # file_name="plot_snr_result_noise1", + # fig=fig_snr_noise1, + # ) + # scan_task.results.append(plot_snr_result_noise1) + + # ---------------------- + + # calibrated_larmor_freq_opt, data_dict, fig_snr1 = larmor_cal( + # seq_file=self.seq_file_path, + # larmor_start=max_snr_freq_fine, + # iterations=10, + # delay_s=1, + # echo_count=1, + # step_size=0.6, + # plot=True, # For debug + # shim_x=cfg.SHIM_X, + # shim_y=cfg.SHIM_Y, + # shim_z=cfg.SHIM_Z, + # gui_test=False, + # ) + + # plot_snr_result1 = load_plot_in_ui( + # working_folder=working_folder, file_name="plot_snr_result1", fig=fig_snr1 + # ) + # scan_task.results.append(plot_snr_result1) + + # calibrated_larmor_freq, data_dict, fig_snr2 = larmor_cal( + # seq_file=self.seq_file_path, + # larmor_start=larmor_freq, + # iterations=20, + # delay_s=1, + # echo_count=1, + # step_size=0.2, + # plot=True, # For debug + # shim_x=cfg.SHIM_X, + # shim_y=cfg.SHIM_Y, + # shim_z=cfg.SHIM_Z, + # gui_test=False, + # ) + + # plot_snr_result2 = load_plot_in_ui( + # working_folder=working_folder, file_name="plot_snr_result2", fig=fig_snr2 + # ) + # scan_task.results.append(plot_snr_result2) + + # calibrated_larmor_freq = calibrated_larmor_freq_opt + # calibrated_larmor_freq = opt_max_snr_freq log.info(f"Final Larmor frequency (using SNR): {calibrated_larmor_freq} MHz") - # updating the Larmor frequency in the config.json file configuration_data.rf_parameters.larmor_frequency_MHz = calibrated_larmor_freq + scan_task.adjustment.rf.larmor_frequency = calibrated_larmor_freq writing_json_parameter(config_data=configuration_data) # Reload the configuration -- otherwise it does not get updated until the next start cfg.update() diff --git a/sequences/adj_grad_amplitude.py b/sequences/adj_grad_amplitude.py old mode 100644 new mode 100755 index c924097..d24c9f3 --- a/sequences/adj_grad_amplitude.py +++ b/sequences/adj_grad_amplitude.py @@ -4,7 +4,10 @@ import external.seq.adjustments_acq.config as cfg import common.logger as logger - +import matplotlib.pyplot as plt +import pickle +from common.types import ResultItem +import numpy as np from sequences import PulseqSequence # type: ignore from sequences.common import make_rf_se # type: ignore from sequences.common.util import reading_json_parameter, writing_json_parameter @@ -16,19 +19,20 @@ class CalGradAmplitude(PulseqSequence, registry_key=Path(__file__).stem): @classmethod def get_readable_name(self) -> str: - return "Calibrate Gradients [untested]" + return "Calibrate Gradients [WIP]" @classmethod def get_description(self) -> str: return "Service sequence to calibrate the gradients using a phantom with known dimensions." def calculate_sequence(self, scan_task) -> bool: + scan_task.processing.recon_mode = "bypass" self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" log.info("Calculating sequence " + self.get_name()) make_rf_se.pypulseq_rfse( inputs={ - "TE": 70, + "TE": 3, "TR": 250, "NSA": 1, "ADC_samples": 4096, @@ -51,27 +55,26 @@ def run_sequence(self, scan_task) -> bool: configuration_data = reading_json_parameter() grad_axes = ["x", "y", "z"] - iter = 20 + iter = 10 for iterations in range(iter): for axis in grad_axes: print("test") log.info(f"Calibrating {axis} axis") - grad_max = grad_max_cal( + grad_max, fft_x, rx_fft, hline = grad_max_cal( channel=axis, - phantom_width=10, + phantom_width=30, # 30 mm phantom width larmor_freq=cfg.LARMOR_FREQ, calibration_power=0.8, trs=3, - range=0.05, tr_spacing=2e6, echo_duration=5000, readout_duration=500, rx_period=25 / 3, - RF_PI2_DURATION=50, + RF_PI2_DURATION=100, rf_max=cfg.RF_MAX, trap_ramp_duration=50, trap_ramp_pts=5, - plot=True, + plot=False, ) if axis == "x": @@ -81,6 +84,37 @@ def run_sequence(self, scan_task) -> bool: elif axis == "z": configuration_data.gradients_parameters.gz_maximum = grad_max writing_json_parameter(config_data=configuration_data) + plt.clf() + plt.title(f"Gradient calibration") + plt.grid(True, color="#333") + + + plt.plot(fft_x, np.abs(rx_fft)) + plt.hlines(*hline, "r") + # plt.xlabel('Time (us)') + # plt.ylabel('Signal') + plt.title( + f"FFT -- Magnitude ({(grad_max * 1e-3):.4f} KHz/m gradient max)" + ) + + file = open(self.get_working_folder() + "/other/gradcal.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + result = ResultItem() + result.name = "GradCal" + result.description = "Gradient calibration" + result.type = "plot" + result.autoload_viewer = 1 + result.file_path = "other/gradcal.plot" + scan_task.results.insert(0, result) + + log.info("Done running sequence " + self.get_name()) + + + + + return True diff --git a/sequences/adj_rf_amplitude.py b/sequences/adj_rf_amplitude.py old mode 100644 new mode 100755 index f1c1bc9..1f53b8b --- a/sequences/adj_rf_amplitude.py +++ b/sequences/adj_rf_amplitude.py @@ -4,10 +4,13 @@ from external.seq.adjustments_acq.calibration import rf_max_cal import common.logger as logger - +import matplotlib.pyplot as plt from sequences import PulseqSequence # type: ignore from sequences.common import make_rf_se # type: ignore from sequences.common.util import reading_json_parameter, writing_json_parameter +import numpy as np +import pickle +from common.types import ResultItem log = logger.get_logger() @@ -15,9 +18,10 @@ class AdjRFAmplitude(PulseqSequence, registry_key=Path(__file__).stem): @classmethod def get_readable_name(self) -> str: - return "Adjust RF Amplitude [untested]" + return "Adjust RF Amplitude [per coil]" def calculate_sequence(self, scan_task) -> bool: + scan_task.processing.recon_mode = "bypass" self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" log.info("Calculating sequence " + self.get_name()) @@ -61,9 +65,41 @@ def run_sequence(self, scan_task) -> bool: plot=True, gui_test=False, ) + peak_max_arr = data_dict["peak_max_arr"] + peak_max_arr = peak_max_arr.tolist() + + rf_amp_vals = data_dict["rf_amp_vals"] + rf_amp_vals = rf_amp_vals.tolist() + + rf_pi2_fraction = rf_amp_vals[np.argmax(peak_max_arr)] + # dec_inds = np.where(peak_max_arr[:-1] >= peak_max_arr[1:])[0] + # max_ind = dec_inds[0] + # rf_pi2_fraction = rf_amp_vals[max_ind] + + + + plt.clf() + plt.title("RF Amplitude Calibration") + plt.grid(True, color="#333") + plt.plot(rf_amp_vals, np.abs(peak_max_arr), marker="o") + plt.xlabel("RF pi/2 fraction [a.u.]") + plt.ylabel("Signal [a.u.]") + file = open(self.get_working_folder() + "/other/plot_rf_cal_result.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + + result = ResultItem() + result.name = "RF_cal" + result.description = "Recorded RF calibration signal" + result.type = "plot" + result.primary = True + result.autoload_viewer = 1 + result.file_path = "other/plot_rf_cal_result.plot" + scan_task.results.append(result) # updating the Larmor frequency in the config.json file - configuration_data.rf_parameters.rf_maximum_amplitude_Hze = est_rf_max + # configuration_data.rf_parameters.rf_maximum_amplitude_Hze = est_rf_max configuration_data.rf_parameters.rf_pi2_fraction = rf_pi2_fraction writing_json_parameter(config_data=configuration_data) diff --git a/sequences/adj_rf_duration.py b/sequences/adj_rf_duration.py old mode 100644 new mode 100755 index ba1fe37..ddf5200 --- a/sequences/adj_rf_duration.py +++ b/sequences/adj_rf_duration.py @@ -7,8 +7,14 @@ import external.seq.adjustments_acq.scripts as scr # pylint: disable=import-error from external.seq.adjustments_acq.calibration import rf_duration_cal from sequences.common.util import reading_json_parameter - +from common.ipc import Communicator +ipc_comm = Communicator(Communicator.ACQ) import common.logger as logger +from common.types import ResultItem +import matplotlib.pyplot as plt +import pickle +import time + from sequences import PulseqSequence from sequences.common import make_rf_se @@ -24,21 +30,27 @@ def get_readable_name(self) -> str: rf_duration_vals = [] - def calculate_sequence(self, scan_task) -> bool: - points = 25 # number of steps, to be added as a parameter - rf_min_duration, rf_max_duration = 50e-6, 400e-6 # in seconds + def calculate_sequence(self, scan_task, points=3) -> bool: + scan_task.processing.recon_mode = "bypass" + rf_min_duration, rf_max_duration = 50e-6, 100e-6 # in seconds self.rf_duration_vals = np.linspace( rf_min_duration, rf_max_duration, num=points, endpoint=True ) - + log.info("Durations are: " + str(self.rf_duration_vals)) # Calculating sequence for different RF pulse durations for i in range(points): + self.seq_file_path = ( self.get_working_folder() - + "/seq/rf_duration_calib_" + + "/seq/acq" + str(i + 1) + ".seq" ) + log.info(self.seq_file_path) + print(f"{self.rf_duration_vals[i]:.4f} ({i}/{points})") + ipc_comm.send_status( + f"Adjusting duration: Searching {self.rf_duration_vals[i]:.4f} ({i+1}/{points})" + ) log.info("Calculating sequence " + self.get_name()) make_rf_se.pypulseq_rfse( inputs={ @@ -50,7 +62,7 @@ def calculate_sequence(self, scan_task) -> bool: "FA1": 90, "FA2": 180, }, - check_timing=True, + check_timing=False, output_file=self.seq_file_path, rf_duration=self.rf_duration_vals[i], ) @@ -59,13 +71,13 @@ def calculate_sequence(self, scan_task) -> bool: return True - def run_sequence(self, scan_task) -> bool: + def run_sequence(self, scan_task,points=3) -> bool: log.info("Running RF calibration sequences ") # reading configuration data from config.json configuration_data = reading_json_parameter() - points = 25 # number of steps, to be added as a parameter + tr_spacing = 5 # [us] Time between repetitions # Make sure the TR units are right (in case someone puts in us rather than s) @@ -78,10 +90,11 @@ def run_sequence(self, scan_task) -> bool: # Run sequences for different RF pulse duration print("Running RF duration calibration sequences") rxd_list = [] + peak_max_arr = [] for i in range(points): seq_file = ( self.get_working_folder() - + "/seq/rf_duration_calib_" + + "/seq/acq" + str(i + 1) + ".seq" ) @@ -102,18 +115,44 @@ def run_sequence(self, scan_task) -> bool: gui_test=False, case_path=self.get_working_folder(), ) + peak_max_arr.append(np.max(np.abs(rxd), axis=0, keepdims=False)) rxd_list.append(rxd) time.sleep(tr_spacing) log.info(f"Step {i} / {self.rf_duration_vals[i]}: {np.sum(np.abs(rxd))}") + # Print progress if (i + 1) % 5 == 0: print(f"Finished point {i + 1}/{points}...") # Identify the RF duration corresponding to the maximal echo amplitude - estimated_duration = rf_duration_cal(rxd_list=rxd_list, points=points) + # estimated_duration, rf_duration_vals, peak_max_arr = rf_duration_cal(rxd_list=rxd_list, points=points) + estimated_duration = self.rf_duration_vals[np.argmax(peak_max_arr)] log.info(f"Estimated optimal duration = {estimated_duration}") + plt.clf() + plt.title("RF Amplitude Calibration") + plt.grid(True, color="#333") + plt.plot(self.rf_duration_vals * 1e6, np.abs(peak_max_arr), marker="o") + plt.xlabel("RF Duration [us]") + plt.ylabel("Echo Amplitude [a.u.]") + file = open(self.get_working_folder() + "/other/plot_rf_cal_result.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + + result = ResultItem() + result.name = "RF_cal" + result.description = "Recorded RF calibration signal" + result.type = "plot" + result.primary = True + result.autoload_viewer = 1 + result.file_path = "other/plot_rf_cal_result.plot" + scan_task.results.append(result) + + + + # updating the Larmor frequency in the config.json file # configuration_data.rf_parameters.rf_maximum_amplitude_Hze = rf_duration # writing_json_parameter(config_data=configuration_data) diff --git a/sequences/adj_shim_amplitude.py b/sequences/adj_shim_amplitude.py old mode 100644 new mode 100755 index 7781311..431ef52 --- a/sequences/adj_shim_amplitude.py +++ b/sequences/adj_shim_amplitude.py @@ -3,7 +3,10 @@ from PyQt5 import uic from pathlib import Path - +import numpy as np +import matplotlib.pyplot as plt +import pickle +from common.types import ResultItem from external.seq.adjustments_acq.calibration import shim_cal_linear import external.seq.adjustments_acq.config as cfg @@ -49,11 +52,11 @@ def get_parameters(self) -> dict: @classmethod def get_default_parameters(self) -> dict: return { - "TE": 70, + "TE": 20, "TR": 250, "NSA": 1, - "ADC_samples": 4096, - "ADC_duration": 6400, + "ADC_samples": 512, + "ADC_duration": 5120, "N_ITER": 1, } @@ -106,11 +109,11 @@ def calculate_sequence(self, scan_task) -> bool: log.info("Calculating sequence " + self.get_name()) make_rf_se.pypulseq_rfse( inputs={ - "TE": 70, + "TE": 20, "TR": 250, "NSA": 1, - "ADC_samples": 4096, - "ADC_duration": 6400, + "ADC_samples": 512, + "ADC_duration": 5120, "FA1": 90, "FA2": 180, }, @@ -127,19 +130,19 @@ def run_sequence(self, scan_task) -> bool: axes = ["x", "y", "z"] log.info("Running sequence " + self.get_name()) - shim_range = 0.1 - n_iter_linear = 2 + shim_range = 0.25 + n_iter_linear = 3 for shim_iter in range(int(n_iter_linear)): for channel in axes: log.info(f"Updating {channel} linear shim (iter {shim_iter + 1})") - shim_weight = shim_cal_linear( + shim_weight,fwhm_list, shim_range_plot = shim_cal_linear( seq_file=self.seq_file_path, - larmor_freq=LARMOR_FREQ, + larmor_freq=cfg.LARMOR_FREQ, channel=channel, range=shim_range, shim_points=5, points=2, - iterations=1, + iterations=self.param_N_ITER, zoom_factor=2, shim_x=cfg.SHIM_X, shim_y=cfg.SHIM_Y, @@ -150,6 +153,7 @@ def run_sequence(self, scan_task) -> bool: smooth=True, plot=True, gui_test=False, + grad_t = self.param_ADC_duration / self.param_ADC_samples, ) # write to config file @@ -166,6 +170,25 @@ def run_sequence(self, scan_task) -> bool: shim_range = shim_range / 2 else: shim_range = shim_range + plt.clf() + plt.title("Shim Calibration") + plt.grid(True, color="#333") + plt.plot(shim_range_plot, np.abs(fwhm_list), marker="o") + plt.xlabel("Shim range [a.u.]") + plt.ylabel("FWHM [a.u.]") + file = open(self.get_working_folder() + "/other/plot_shim_cal_result.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + + result = ResultItem() + result.name = "Shim_cal" + result.description = "Recorded Shim calibration signal" + result.type = "plot" + result.primary = True + result.autoload_viewer = 1 + result.file_path = "other/plot_shim_cal_result.plot" + scan_task.results.append(result) log.info("Done running sequence " + self.get_name()) return True diff --git a/sequences/adj_shim_amplitude/interface.ui b/sequences/adj_shim_amplitude/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/adj_shim_amplitude_manual.py b/sequences/adj_shim_amplitude_manual.py old mode 100644 new mode 100755 diff --git a/sequences/adj_shim_amplitude_manual/interface.ui b/sequences/adj_shim_amplitude_manual/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/common/get_trajectory.py b/sequences/common/get_trajectory.py old mode 100644 new mode 100755 diff --git a/sequences/common/make_rf_se.py b/sequences/common/make_rf_se.py old mode 100644 new mode 100755 index 9909513..3d23590 --- a/sequences/common/make_rf_se.py +++ b/sequences/common/make_rf_se.py @@ -10,7 +10,7 @@ def pypulseq_rfse( - inputs=None, check_timing=True, output_file="", rf_duration=80e-6 + inputs=None, check_timing=True, output_file="", rf_duration=100e-6 ) -> bool: if not output_file: log.error("No output file specified") @@ -19,17 +19,14 @@ def pypulseq_rfse( # ====== # DEFAULTS FROM CONFIG FILE TODO: MOVE DEFAULTS TO UI # ====== + LARMOR_FREQ = cfg.LARMOR_FREQ RF_MAX = cfg.RF_MAX RF_PI2_FRACTION = cfg.RF_PI2_FRACTION - alpha1 = inputs["FA1"] # flip angle - # alpha1 = 90 # flip angle + alpha1 = inputs["FA1"] # flip angle # need to remove this two factor alpha1_duration = rf_duration # pulse duration alpha2 = inputs["FA2"] # refocusing flip angle - # alpha2 = 180 # refocusing flip angle alpha2_duration = rf_duration # pulse duration - # adc_num_samples = 4096 - # adc_duration = 6.4e-3 TR = inputs["TR"] / 1000 # ms to s TE = inputs["TE"] / 1000 @@ -77,7 +74,7 @@ def pypulseq_rfse( delay=100e-6, phase_offset=math.pi / 2, system=system, - use="refocusing", + use="excitation", ) # ====== @@ -112,12 +109,15 @@ def pypulseq_rfse( log.info("Timing check failed. Error listing follows:") [print(e) for e in error_report] - log.debug(output_file) + log.info(output_file) + log.info("Writing sequence to file") + try: seq.write(output_file) log.debug("Seq file stored") except: log.error("Could not write sequence file") + log.info("Output file: " + output_file) return False return True diff --git a/sequences/common/make_se_1D.py b/sequences/common/make_se_1D.py old mode 100644 new mode 100755 index dd4fec5..255143f --- a/sequences/common/make_se_1D.py +++ b/sequences/common/make_se_1D.py @@ -1,6 +1,5 @@ import math import numpy as np - import pypulseq as pp # type: ignore import external.seq.adjustments_acq.config as cfg @@ -10,9 +9,10 @@ def pypulseq_1dse( - inputs=None, check_timing=True, output_file="", rf_duration=50e-6 -) -> bool: + inputs=None, check_timing=True, output_file="", system=None, rf_duration=100e-6 +): if not output_file: + log.info("No output file specified") log.error("No output file specified") return False @@ -20,32 +20,24 @@ def pypulseq_1dse( # DEFAULTS FROM CONFIG FILE TODO: MOVE DEFAULTS TO UI # ====== # ====== - rf_duration = 100e-6 - LARMOR_FREQ = cfg.LARMOR_FREQ - RF_MAX = cfg.RF_MAX - RF_PI2_FRACTION = cfg.RF_PI2_FRACTION - alpha1 = cfg.DBG_FA_EXC # flip angle + # LARMOR_FREQ = cfg.LARMOR_FREQ + # RF_MAX = cfg.RF_MAX + # RF_PI2_FRACTION = cfg.RF_PI2_FRACTION + alpha1 = 90 # flip angle alpha1_duration = rf_duration # pulse duration - alpha2 = cfg.DBG_FA_REF # refocusing flip angle + alpha2 = 180 # refocusing flip angle alpha2_duration = rf_duration # pulse duration - # TE = 20e-3 - # TR = 3000e-3 - # num_averages = 1 - # channel = "y" + TR = inputs["TR"] / 1000 # ms to s TE = inputs["TE"] / 1000 num_averages = inputs["NSA"] - fov = inputs["FOV"] / 1000 + fov = inputs["FOV"] / 1000 # mm to m Nx = inputs["Base_Resolution"] BW = inputs["BW"] channel = inputs["Gradient"] + system = inputs["system"] - # fov = 20e-3 # Define FOV and resolution - 37.5e-3 - # Nx = 250 - # BW = 64e3 - # adc_dwell = 1 / BW - # adc_duration = 2.25e-3 # Nx * adc_dwell # 6.4e-3 - prephaser_duration = 5e-3 # TODO: Need to define this behind the scenes and optimze + rise_time = 250e-6 # dG = 200e-6 # Grad rise time # ====== @@ -57,19 +49,28 @@ def pypulseq_1dse( # ====== # SET SYSTEM CONFIG TODO --> ? # ====== + # if channel == "x": + # max_grad = cfg.GX_MAX + # elif channel == "y": + # max_grad = cfg.GY_MAX + # elif channel == "z": + # max_grad = cfg.GZ_MAX + + + # system = pp.Opts( + # max_grad=max_grad, + # grad_unit="Hz/m", # + # max_slew=1000, + # slew_unit="T/m/s", + # #rf_ringdown_time=100e-6, + # rf_ringdown_time=20e-6, + # rf_dead_time=100e-6, + # rf_raster_time=1e-6, + # #adc_dead_time=10e-6, + # adc_dead_time=20e-6, + # grad_raster_time = adc_dwell, + # ) - system = pp.Opts( - max_grad=200, - grad_unit="mT/m", - max_slew=4000, - slew_unit="T/m/s", - # rf_ringdown_time=100e-6, - rf_ringdown_time=20e-6, - rf_dead_time=100e-6, - rf_raster_time=1e-6, - # adc_dead_time=10e-6, - adc_dead_time=20e-6, - ) # ====== # CREATE EVENTS @@ -89,9 +90,13 @@ def pypulseq_1dse( system=system, use="refocusing", ) - # readout_time = 2.5e-3 + (2 * system.adc_dead_time) - readout_time = 8.0e-3 + (2 * system.adc_dead_time) - delta_k = 1 / fov + + # readout_time = (Nx / BW) + (2 * system.adc_dead_time) + readout_time = (Nx / BW) + # readout_time = np.max([readout_time, 4e-3]) # limit readout time based on max gradient strength + prephaser_duration = 0.5 * readout_time + delta_k = 1 / fov + log.info("**Gradient max amplitude**: ", system.max_grad) gx = pp.make_trapezoid( channel=channel, flat_area=Nx * delta_k, @@ -99,6 +104,8 @@ def pypulseq_1dse( rise_time=rise_time, system=system, ) + log.info("**Gradient amplitude**: ", gx.amplitude) + gx_pre = pp.make_trapezoid( channel=channel, area=gx.area / 2, @@ -107,7 +114,7 @@ def pypulseq_1dse( system=system, ) adc = pp.make_adc( - num_samples=Nx, + num_samples=2 * Nx, # oversampling by factor 2 duration=gx.flat_time, delay=gx.rise_time, phase_offset=np.pi / 2, @@ -128,23 +135,8 @@ def pypulseq_1dse( ) ) * seq.grad_raster_time - # tau2 = ( - # math.ceil( - # ( - # TE / 2 - # - 0.5 * (pp.calc_duration(rf2)) - # - pp.calc_duration(gx_pre) - # - 2 * rise_time - # ) - # / seq.grad_raster_time - # ) - # ) * seq.grad_raster_time # TODO: gradient delays need to be calibrated - tau2 = ( - math.ceil( - (TE / 2 - 0.5 * (pp.calc_duration(rf2) + pp.calc_duration(gx))) - / seq.grad_raster_time - ) + math.ceil((TE / 2 - 0.5 * (pp.calc_duration(rf2) + pp.calc_duration(gx))) / seq.grad_raster_time) ) * seq.grad_raster_time delay_TR = TR - TE - (0.5 * readout_time) @@ -157,9 +149,6 @@ def pypulseq_1dse( # ====== # Loop over phase encodes and define sequence blocks - # gx_pre.amplitude = 0 - # gx.amplitude = 0 - for avg in range(num_averages): seq.add_block(rf1) seq.add_block(gx_pre) @@ -169,11 +158,8 @@ def pypulseq_1dse( seq.add_block(gx, adc) # Projection seq.add_block(pp.make_delay(delay_TR)) - # seq.plot(time_range=[0, 2*TR]) - # seq.write("se_1D_local.seq") - # Check whether the timing of the sequence is correct - check_timing = True + check_timing = False if check_timing: ok, error_report = seq.check_timing() if ok: @@ -181,7 +167,7 @@ def pypulseq_1dse( else: print("Timing check failed. Error listing follows:") [print(e) for e in error_report] - + log.debug(output_file) try: seq.write(output_file) @@ -189,5 +175,5 @@ def pypulseq_1dse( except: log.error("Could not write sequence file") return False - + return True diff --git a/sequences/common/make_se_2D.py b/sequences/common/make_se_2D.py old mode 100644 new mode 100755 index 1c11c5b..d45d369 --- a/sequences/common/make_se_2D.py +++ b/sequences/common/make_se_2D.py @@ -1,9 +1,7 @@ import math import numpy as np - import pypulseq as pp # type: ignore import external.seq.adjustments_acq.config as cfg - from sequences.common import view_traj import common.logger as logger @@ -11,7 +9,7 @@ def pypulseq_se2D( - inputs=None, check_timing=True, output_file="", output_folder="") -> bool: + inputs=None, check_timing=True, system=None, output_file="", output_folder="") -> bool: if not output_file: log.error("No output file specified") return False @@ -19,48 +17,50 @@ def pypulseq_se2D( # ====== # DEFAULTS FROM CONFIG FILE TODO: MOVE DEFAULTS TO UI # ====== + rf_duration = 100e-6 LARMOR_FREQ = cfg.LARMOR_FREQ RF_MAX = cfg.RF_MAX RF_PI2_FRACTION = cfg.RF_PI2_FRACTION - - # fov = 140e-3 # Define FOV and resolution - # Nx = 70 alpha1 = 90 # flip angle - alpha1_duration = 100e-6 # pulse duration + alpha1_duration = rf_duration # pulse duration alpha2 = 180 # refocusing flip angle - alpha2_duration = 100e-6 # pulse duration - #num_averages = 1 - prephaser_duration = 3e-3 # TODO: Need to define this behind the scenes and optimze - + alpha2_duration = rf_duration # pulse duration + TR = inputs["TR"] / 1000 TE = inputs["TE"] / 1000 num_averages = inputs["NSA"] Orientation = inputs["Orientation"] + PE_Ordering = inputs["PE_Ordering"] fov = inputs["FOV"] / 1000 Nx = inputs["Base_Resolution"] BW = inputs["BW"] visualize = inputs["view_traj"] - # Trajectory = inputs['Trajectory'] TODO - # PE_Ordering = inputs['PE_Ordering'] TODO - # PF = inputs['PF'] TODO + system = inputs["system"] + adc_duration = (Nx/ BW) # 6.4e-3 - compensating for the oversampling of 2x + readout_time = adc_duration # 8.e-3 + (2 * system.adc_dead_time) + # readout_time = np.max([readout_time, 4e-3]) # limit readout time based on max gradient strength - Ny = Nx - #BW = 32e3 - adc_dwell = 1 / BW - adc_duration = Nx * adc_dwell # 6.4e-3 + prephaser_duration = 0.5 * adc_duration # 5e-3 # TODO: Need to define this behind the scenes and optimze + rise_time = 250e-6 # dG = 200e-6 # Grad rise time + rf_spoiling_inc = 0 # TODO: coordinate the orientation ch0 = "x" ch1 = "y" + if Orientation == "Axial": - ch0 = "y" - ch1 = "z" + ch0 = "z" + ch1 = "x" + system.max_grad = np.min([cfg.GX_MAX, cfg.GZ_MAX]) elif Orientation == "Sagittal": - ch0 = "x" - ch1 = "z" - elif Orientation == "coronal": - ch0 = "x" + ch0 = "z" ch1 = "y" + system.max_grad = np.min([cfg.GY_MAX, cfg.GZ_MAX]) + elif Orientation == "Coronal": + ch0 = "y" + ch1 = "x" + system.max_grad = np.min([cfg.GX_MAX, cfg.GY_MAX]) + log.info('Orientation: Ch0 and Ch1', Orientation, ch0, ch1) # ====== # INITIATE SEQUENCE @@ -68,32 +68,7 @@ def pypulseq_se2D( seq = pp.Sequence() - # ====== - # SET SYSTEM CONFIG TODO --> ? - # ====== - - # system = pp.Opts( - # max_grad=12, - # grad_unit="mT/m", - # max_slew=25, - # slew_unit="T/m/s", - # rf_ringdown_time=20e-6, - # rf_dead_time=100e-6, - # rf_raster_time=1e-6, - # adc_dead_time=20e-6, - # ) - - system = pp.Opts( - max_grad=400, - grad_unit="mT/m", - max_slew=4000, - slew_unit="T/m/s", - rf_ringdown_time=100e-6, - rf_dead_time=100e-6, - rf_raster_time=1e-6, - adc_dead_time=10e-6, - ) - + # ====== # CREATE EVENTS # ====== @@ -109,27 +84,72 @@ def pypulseq_se2D( flip_angle=alpha2 * math.pi / 180, duration=alpha2_duration, delay=100e-6, - phase_offset=math.pi / 2, + phase_offset= math.pi / 2, system=system, use="refocusing", ) # Define other gradients and ADC events + delta_k = 1 / fov + + gx = pp.make_trapezoid( - channel=ch0, flat_area=Nx * delta_k, flat_time=adc_duration, system=system - ) - adc = pp.make_adc( - num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=system + channel=ch0, + flat_area=Nx * delta_k, + flat_time=readout_time, + rise_time=rise_time, + system=system, ) + + gx_pre = pp.make_trapezoid( - channel=ch0, area=gx.area / 2, duration=prephaser_duration, system=system + channel=ch0, + area=gx.area / 2, + duration=prephaser_duration, + rise_time=rise_time, + system=system, ) + gx_rew = pp.make_trapezoid( + channel=ch0, + area=-gx.area / 2, + duration=prephaser_duration, + rise_time=rise_time, + system=system, + ) + + adc = pp.make_adc( + num_samples= 2 * Nx, + duration=gx.flat_time, + delay=gx.rise_time, + phase_offset= math.pi / 2, + system=system, + ) + + + # Ny = int(Nx * 2) # 2 oversampling in phase direction + Ny = Nx # + phase_areas = -(np.arange(Ny) - Ny / 2) * delta_k + + # Generate phase areas for inside-out ordering + if PE_Ordering == "Center_out": + phase_areas = np.zeros(Ny) + pe_table = np.zeros(Ny, dtype=int) + center_index = Ny // 2 + + for i in range(Ny - 1): + if i % 2 == 0: + phase_areas[i] = -(i // 2) * delta_k + pe_table[i] = center_index - (i // 2) + else: + phase_areas[i] = (i // 2 + 1) * delta_k + pe_table[i] = center_index + (i // 2 + 1) + + log.info("Phase encoding table created for inside-out ordering") - phase_areas = -(np.arange(Ny) - Ny / 2) * delta_k # Gradient spoiling -TODO: Need to see if this is really required based on data - gx_spoil = pp.make_trapezoid(channel=ch0, area=2 * Nx * delta_k, system=system) + # gx_spoil = pp.make_trapezoid(channel=ch0, area=2 * Nx * delta_k, system=system) # ====== # CALCULATE DELAYS @@ -152,31 +172,30 @@ def pypulseq_se2D( ) ) * seq.grad_raster_time - delay_TR = ( - math.ceil( - ( - TR - - TE - - pp.calc_duration(gx_pre) - - np.max(pp.calc_duration(gx_spoil, gx_pre)) - ) - / seq.grad_raster_time - ) - ) * seq.grad_raster_time + delay_TR = TR - TE - (0.5 * readout_time)- pp.calc_duration(gx_pre) assert np.all(tau1 >= 0) assert np.all(tau2 >= 0) - assert np.all(delay_TR >= pp.calc_duration(gx_spoil)) + # assert np.all(delay_TR >= pp.calc_duration(gx_spoil)) # ====== # CONSTRUCT SEQUENCE # ====== # Loop over phase encodes and define sequence blocks + dummy_scans = 0 + for dummy in range(dummy_scans): + seq.add_block(rf1) + seq.add_block(pp.make_delay(TR)) + + rf_phase = rf1.phase_offset + rf_inc = 0 + for avg in range(num_averages): for i in range(Ny): - # rf1.phase_offset = rf_phase / 180 * np.pi # TODO: Include later - # adc.phase_offset = rf_phase / 180 * np.pi - # rf_inc = divmod(rf_inc + rf_spoiling_inc, 360.0)[1] - # rf_phase = divmod(rf_phase + rf_inc, 360.0)[1] + rf1.phase_offset = rf_phase / 180 * np.pi # TODO: Include later + adc.phase_offset = rf_phase / 180 * np.pi + rf_inc = divmod(rf_inc + rf_spoiling_inc, 360.0)[1] + rf_phase = divmod(rf_phase + rf_inc, 360.0)[1] + seq.add_block(rf1) gy_pre = pp.make_trapezoid( channel=ch1, @@ -190,7 +209,8 @@ def pypulseq_se2D( seq.add_block(pp.make_delay(tau2)) seq.add_block(gx, adc) gy_pre.amplitude = -gy_pre.amplitude - seq.add_block(gx_spoil, gy_pre) # TODO: Figure if we need spoiling + # seq.add_block(gx_spoil, gy_pre) # TODO: Figure if we need spoiling + seq.add_block(gy_pre, gx_rew) # TODO: Figure if we need spoiling seq.add_block(pp.make_delay(delay_TR)) # Check whether the timing of the sequence is correct @@ -303,7 +323,7 @@ def pypulseq_se2D_radial(inputs=None, check_timing=True, output_file="") -> bool ) # Define other gradients and ADC events - delta_k = 1 / fov # frequency-oversampling is not implemented + delta_k = 1 / fov # frequency-oversampling is not implemented - 2 * fov rather than fov gx = pp.make_trapezoid( channel=ch0, flat_area=Nx * delta_k, flat_time=adc_duration, system=system ) @@ -324,8 +344,11 @@ def pypulseq_se2D_radial(inputs=None, check_timing=True, output_file="") -> bool amp_enc_max = gx.amplitude # Gradient spoiling -TODO: Need to see if this is really required based on data - gx_spoil = pp.make_trapezoid(channel=ch0, area=2 * Nx * delta_k, system=system) - gy_spoil = pp.make_trapezoid(channel=ch1, area=2 * Nx * delta_k, system=system) + # gx_spoil = pp.make_trapezoid(channel=ch0, area=2 * Nx * delta_k, system=system) + # gy_spoil = pp.make_trapezoid(channel=ch1, area=2 * Nx * delta_k, system=system) + + # gx_spoil = pp.make_trapezoid(channel=ch0, area=0.2 * Nx * delta_k, system=system) + # gy_spoil = pp.make_trapezoid(channel=ch1, area=0.2 * Nx * delta_k, system=system) # ====== # CALCULATE DELAYS @@ -354,14 +377,15 @@ def pypulseq_se2D_radial(inputs=None, check_timing=True, output_file="") -> bool TR - TE - pp.calc_duration(gx_pre) - - np.max(pp.calc_duration(gx_spoil, gx_pre)) + # - np.max(pp.calc_duration(gx_spoil, gx_pre)) + - np.max(pp.calc_duration(gx_pre, gx_pre)) ) / seq.grad_raster_time ) ) * seq.grad_raster_time assert np.all(tau1 >= 0) assert np.all(tau2 >= 0) - assert np.all(delay_TR >= pp.calc_duration(gx_spoil)) + # assert np.all(delay_TR >= pp.calc_duration(gx_spoil)) # ====== # CONSTRUCT SEQUENCE @@ -387,7 +411,7 @@ def pypulseq_se2D_radial(inputs=None, check_timing=True, output_file="") -> bool gx.amplitude = amp_enc_max * math.sin(phi) gy.amplitude = amp_enc_max * math.cos(phi) seq.add_block(gx, gy, adc) - seq.add_block(gx_spoil, gy_spoil) # TODO: Figure if we need spoiling + # seq.add_block(gx_spoil, gy_spoil) # TODO: Figure if we need spoiling seq.add_block(pp.make_delay(delay_TR)) seq.plot(time_range=[0, 3 * TR]) diff --git a/sequences/common/make_tse_2D.py b/sequences/common/make_tse_2D.py old mode 100644 new mode 100755 diff --git a/sequences/common/make_tse_3D.py b/sequences/common/make_tse_3D.py old mode 100644 new mode 100755 index 057bbf6..4a897ce --- a/sequences/common/make_tse_3D.py +++ b/sequences/common/make_tse_3D.py @@ -25,10 +25,10 @@ def pypulseq_tse3D( # DEFAULTS FROM CONFIG FILE TODO: MOVE DEFAULTS TO UI # ====== - alpha1 = inputs["FA1"] # flip angle - alpha1_duration = 80e-6 # pulse duration - alpha2 = inputs["FA2"] # refocusing flip angle - alpha2_duration = 80e-6 # pulse duration + alpha1 = 90 # flip angle + alpha1_duration = 120e-6 # pulse duration + alpha2 = 180 # refocusing flip angle + alpha2_duration = 120e-6 # pulse duration TR = inputs["TR"] / 1000 TE = inputs["TE"] / 1000 @@ -37,7 +37,6 @@ def pypulseq_tse3D( fovy = inputs["FOV"] / 1000 # DEBUG! TODO: Expose FOV in Z on UI fovz = inputs["FOV"] / 1000 / 2 - fovz = inputs["FOV"] / 1000 / 4 Nx = inputs["Base_Resolution"] Ny = inputs["Base_Resolution"] Nz = inputs["Slices"] @@ -115,14 +114,14 @@ def pypulseq_tse3D( rf1 = pp.make_block_pulse( flip_angle=alpha1 * math.pi / 180, duration=alpha1_duration, - delay=0 * 100e-6, + delay=100e-6, system=system, use="excitation", ) rf2 = pp.make_block_pulse( flip_angle=alpha2 * math.pi / 180, duration=alpha2_duration, - delay=0 * 100e-6, + delay=100e-6, phase_offset=math.pi / 2, system=system, use="refocusing", @@ -139,14 +138,14 @@ def pypulseq_tse3D( num_samples=2 * Nx, duration=gx.flat_time, delay=gx.rise_time, system=system ) - crusher_moment = gx.area / 2 - # crusher_moment = 0 + # crusher_moment = gx.area / 2 + crusher_moment = 0 gx_pre = pp.make_trapezoid( channel=ch0, area=gx.area / 2 + crusher_moment, system=system, - # duration=pp.calc_duration(gx) / 2, + duration=pp.calc_duration(gx) / 2, ) gx_crush = pp.make_trapezoid( @@ -278,13 +277,13 @@ def pypulseq_tse3D( gy_pre = pp.make_trapezoid( channel=ch1, - area=-1.0 * phase_areas0[pe_idx], + area=-1.0 * phase_areas0[pe_idx] + crusher_moment, duration=pp.calc_duration(gx_pre), system=system, ) gz_pre = pp.make_trapezoid( channel=ch2, - area=-1.0 * phase_areas1[pe_idx], + area=-1.0 * phase_areas1[pe_idx] + crusher_moment, duration=pp.calc_duration(gx_pre), system=system, ) @@ -307,23 +306,23 @@ def pypulseq_tse3D( seq.add_block(pp.make_delay(tau1b)) # seq.add_block(gx_pre, gy_crush, gz_crush) else: - pass - # seq.add_block(pp.make_delay(duration_gx_pre)) + seq.add_block(pp.make_delay(duration_gx_pre)) # seq.add_block(gx_crush, gy_crush, gz_crush) seq.add_block(rf2) + # seq.add_block(gx_crush, gy_pre, gz_pre) seq.add_block(pp.make_delay(tau2a)) - # seq.add_block(gy_pre, gz_pre) - seq.add_block(gx_crush, gy_pre, gz_pre) + seq.add_block(gy_pre, gz_pre) seq.add_block(pp.make_delay(tau2b)) if is_dummyshot: seq.add_block(gx) else: seq.add_block(gx, adc) adc_phase.append(rfspoil_phase) + # gy_pre.amplitude = -gy_pre.amplitude + # gz_pre.amplitude = -gz_pre.amplitude seq.add_block(pp.make_delay(tau2a)) - # seq.add_block(gy_rew, gz_rew) - seq.add_block(gx_crush, gy_rew, gz_rew) + seq.add_block(gy_rew, gz_rew) seq.add_block(pp.make_delay(tau2b)) seq.add_block(gx_spoil, gy_spoil, gz_spoil) diff --git a/sequences/common/post_acq_process.py b/sequences/common/post_acq_process.py old mode 100644 new mode 100755 diff --git a/sequences/common/pydanticConfig.py b/sequences/common/pydanticConfig.py old mode 100644 new mode 100755 index d529648..6531941 --- a/sequences/common/pydanticConfig.py +++ b/sequences/common/pydanticConfig.py @@ -5,8 +5,8 @@ ### rf_parameters section class RfParameters(BaseModel): - larmor_frequency_MHz: float = 15.58 - rf_maximum_amplitude_Hze: float = 7661.29 + larmor_frequency_MHz: float = 15.52 + rf_maximum_amplitude_Hze: float = 10000 #7661.29 rf_pi2_fraction: float = 0.6744 ### gradients_parameters section diff --git a/sequences/common/util.py b/sequences/common/util.py old mode 100644 new mode 100755 diff --git a/sequences/common/view_traj.py b/sequences/common/view_traj.py old mode 100644 new mode 100755 diff --git a/sequences/flash_demo.py b/sequences/flash_demo.py old mode 100644 new mode 100755 diff --git a/sequences/flash_demo/interface.ui b/sequences/flash_demo/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/gre_1D.py b/sequences/gre_1D.py old mode 100644 new mode 100755 index e3703e3..8bf01cc --- a/sequences/gre_1D.py +++ b/sequences/gre_1D.py @@ -161,7 +161,8 @@ def run_sequence(self, scan_task) -> bool: plt.clf() plt.title("ADC Signal") plt.grid(True, color="#333") - plt.plot(np.abs(rxd)) + recon = np.fft.fftshift(np.fft.ifft(np.fft.fftshift(rxd))) + plt.plot(np.abs(recon)) file = open(self.get_working_folder() + "/other/gre_adc.plot", "wb") fig = plt.gcf() diff --git a/sequences/gre_1D/interface.ui b/sequences/gre_1D/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/gre_3D.py b/sequences/gre_3D.py old mode 100644 new mode 100755 index 0cc09ad..0daba9f --- a/sequences/gre_3D.py +++ b/sequences/gre_3D.py @@ -4,8 +4,6 @@ import math import numpy as np from PyQt5 import uic -import matplotlib.pyplot as plt -import pickle import pypulseq as pp # type: ignore import external.seq.adjustments_acq.config as cfg @@ -37,7 +35,7 @@ class SequenceGRE_3D(PulseqSequence, registry_key=Path(__file__).stem): param_BW: int = 32000 param_trajectory: str = "Cartesian" param_ordering: str = "linear_up" - param_dummy_shots: int = 20 + param_dummy_shots: int = 10 @classmethod def get_readable_name(self) -> str: @@ -103,7 +101,7 @@ def get_default_parameters( self, ) -> dict: return { - "TE": 0, + "TE": 15, "TR": 1000, "NSA": 1, "orientation": "Axial", @@ -197,8 +195,6 @@ def run_sequence(self, scan_task) -> bool: / 1000 ) - plot_instructions = True - rxd, rx_t = run_pulseq( seq_file=self.seq_file_path, rf_center=cfg.LARMOR_FREQ, @@ -216,23 +212,7 @@ def run_sequence(self, scan_task) -> bool: case_path=self.get_working_folder(), raw_filename="raw", expected_duration_sec=expected_duration_sec, - plot_instructions=plot_instructions, ) - scan_task.adjustment.rf.larmor_frequency = cfg.LARMOR_FREQ - - if plot_instructions: - file = open(self.get_working_folder() + "/other/seq.plot", "wb") - fig = plt.gcf() - pickle.dump(fig, file) - file.close() - - result = ResultItem() - result.name = "seq_plot" - result.description = "Timing diagram of sequence" - result.type = "plot" - result.file_path = "other/seq.plot" - result.autoload_viewer = 4 - scan_task.results.append(result) log.info("Done running sequence " + self.get_name()) return True @@ -242,7 +222,7 @@ def generate_pulseq(self) -> bool: pe_order_file = self.get_working_folder() + "/rawdata/pe_order.npy" alpha1 = self.param_FA - alpha1_duration = 80e-6 + alpha1_duration = 100e-6 TR = self.param_TR / 1000 TE = self.param_TE / 1000 @@ -250,7 +230,6 @@ def generate_pulseq(self) -> bool: fovy = self.param_FOV / 1000 # DEBUG! TODO: Expose FOV in Z on UI fovz = self.param_FOV / 1000 / 2 - # fovz = self.param_FOV / 1000 / 4 Nx = self.param_baseresolution Ny = self.param_baseresolution Nz = self.param_slices @@ -308,7 +287,7 @@ def generate_pulseq(self) -> bool: rf1 = pp.make_block_pulse( flip_angle=alpha1 * math.pi / 180, duration=alpha1_duration, - delay=0e-6, + delay=100e-6, system=system, use="excitation", ) @@ -326,8 +305,8 @@ def generate_pulseq(self) -> bool: gx_pre = pp.make_trapezoid( channel=ch0, - area=gx.area / 2.0, - # duration=pp.calc_duration(gx) / 2, + area=gx.area / 2, + duration=pp.calc_duration(gx) / 2, system=system, ) gx_pre.amplitude = -1 * gx_pre.amplitude @@ -343,50 +322,25 @@ def generate_pulseq(self) -> bool: phase_areas0 = pe_order[:, 0] * delta_ky phase_areas1 = pe_order[:, 1] * delta_kz - # Dummy calculation to estimate required spacing - gy_pre = pp.make_trapezoid( - channel=ch1, - area=1.0 * np.max(phase_areas0), - system=system, - ) - gz_pre = pp.make_trapezoid( - channel=ch2, - area=-1.0 * np.max(phase_areas1), - system=system, - ) - - pre_duration = max(pp.calc_duration(gy_pre), pp.calc_duration(gz_pre)) - pre_duration = max(pre_duration, pp.calc_duration(gx_pre)) - # Gradient spoiling -TODO: Need to see if this is really required based on data - gx_spoil = pp.make_trapezoid(channel=ch0, area=Nx * delta_kx, system=system) + gx_spoil = pp.make_trapezoid(channel=ch0, area=2 * Nx * delta_kx, system=system) # gy_spoil = pp.make_trapezoid(channel=ch1, area=5 * Nx * delta_kx, system=system) # gz_spoil = pp.make_trapezoid(channel=ch2, area=5 * Nx * delta_kx, system=system) # ====== # CALCULATE DELAYS # ====== - - if TE == 0: - tau1 = 10 * seq.grad_raster_time - TE = ( - tau1 - + 0.5 * pp.calc_duration(rf1) - + pre_duration - + 0.5 * pp.calc_duration(gx) - ) - else: - tau1 = ( - math.ceil( - ( - TE - - 0.5 * pp.calc_duration(rf1) - - pre_duration - - 0.5 * pp.calc_duration(gx) - ) - / seq.grad_raster_time + tau1 = ( + math.ceil( + ( + TE + - 0.5 * pp.calc_duration(rf1) + - pp.calc_duration(gx_pre) + - 0.5 * pp.calc_duration(gx) ) - ) * seq.grad_raster_time + / seq.grad_raster_time + ) + ) * seq.grad_raster_time delay_TR = ( math.ceil( @@ -413,8 +367,7 @@ def generate_pulseq(self) -> bool: adc_phase = [] rfspoil_phase = 0 rfspoil_inc = 0 - rfspoil_incinc = 117.0 - # rfspoil_incinc = 50.0 + rfspoil_incinc = 0.0 # Loop over phase encodes and define sequence blocks for avg in range(num_averages): @@ -440,14 +393,14 @@ def generate_pulseq(self) -> bool: gy_pre = pp.make_trapezoid( channel=ch1, - area=-1.0 * phase_areas0[pe_idx], - duration=pre_duration, + area=1.0 * phase_areas0[pe_idx], + duration=pp.calc_duration(gx_pre), system=system, ) gz_pre = pp.make_trapezoid( channel=ch2, - area=-1.0 * phase_areas1[pe_idx], - duration=pre_duration, + area=1.0 * phase_areas1[pe_idx], + duration=pp.calc_duration(gx_pre), system=system, ) @@ -458,6 +411,7 @@ def generate_pulseq(self) -> bool: seq.add_block(gx) else: seq.add_block(gx, adc) + # adc.phase_offset = rfspoil_phase / 180 * math.pi adc_phase.append(rfspoil_phase) gy_pre.amplitude = -gy_pre.amplitude diff --git a/sequences/gre_3D/interface.ui b/sequences/gre_3D/interface.ui old mode 100644 new mode 100755 index 694fd71..b670e3a --- a/sequences/gre_3D/interface.ui +++ b/sequences/gre_3D/interface.ui @@ -65,7 +65,7 @@ - 0 + 1 10000 diff --git a/sequences/noisescan.py b/sequences/noisescan.py old mode 100644 new mode 100755 diff --git a/sequences/noisescan/interface.ui b/sequences/noisescan/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/prescan_frequency.py b/sequences/prescan_frequency.py old mode 100644 new mode 100755 diff --git a/sequences/rf_se.py b/sequences/rf_se.py old mode 100644 new mode 100755 index 1035b69..1839ee3 --- a/sequences/rf_se.py +++ b/sequences/rf_se.py @@ -21,12 +21,13 @@ class SequenceRF_SE(PulseqSequence, registry_key=Path(__file__).stem): # Sequence parameters - param_TE: int = 70 + param_TE: int = 10 param_TR: int = 250 param_NSA: int = 1 - param_ADC_samples: int = 4096 - param_ADC_duration: int = 6400 + param_ADC_samples: int = 512 + param_ADC_duration: int = 5120 param_debug_plot: bool = True + @classmethod def get_readable_name(self) -> str: @@ -54,12 +55,13 @@ def get_parameters(self) -> dict: @classmethod def get_default_parameters(self) -> dict: return { - "TE": 20, + "TE": 10, "TR": 250, "NSA": 1, - "ADC_samples": 4096, - "ADC_duration": 6400, + "ADC_samples": 512, + "ADC_duration": 5120, "debug_plot": True, + # "TX_Freq": 11.42, # MHz } def set_parameters(self, parameters, scan_task) -> bool: @@ -105,13 +107,6 @@ def calculate_sequence(self, scan_task) -> bool: self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" log.info("Calculating sequence " + self.get_name()) - fa_exc = cfg.DBG_FA_EXC - fa_ref = cfg.DBG_FA_REF - if "FA1" in scan_task.other: - fa_exc = int(scan_task.other["FA1"]) - if "FA2" in scan_task.other: - fa_ref = int(scan_task.other["FA2"]) - make_rf_se.pypulseq_rfse( inputs={ "TE": self.param_TE, @@ -119,8 +114,8 @@ def calculate_sequence(self, scan_task) -> bool: "NSA": self.param_NSA, "ADC_samples": self.param_ADC_samples, "ADC_duration": self.param_ADC_duration, - "FA1": fa_exc, - "FA2": fa_ref, + "FA1": 90, + "FA2": 180, }, check_timing=True, output_file=self.seq_file_path, @@ -132,18 +127,19 @@ def calculate_sequence(self, scan_task) -> bool: def run_sequence(self, scan_task) -> bool: log.info("Running sequence " + self.get_name()) - + # run_sequence_test("prescan_frequency") - rxd, rx_t = run_pulseq( + rxd, _ = run_pulseq( seq_file=self.seq_file_path, - rf_center=cfg.LARMOR_FREQ, + rf_center=cfg.LARMOR_FREQ, # scan_task.adjustment.rf.larmor_frequency, + # rf_center=scan_task.adjustment.rf.larmor_frequency, tx_t=1, - grad_t=10, + grad_t=self.param_ADC_duration / self. param_ADC_samples, tx_warmup=100, - shim_x=0, - shim_y=0, - shim_z=0, + shim_x=cfg.SHIM_X, + shim_y=cfg.SHIM_Y, + shim_z=cfg.SHIM_Z, grad_cal=False, save_np=False, save_mat=False, @@ -151,38 +147,95 @@ def run_sequence(self, scan_task) -> bool: gui_test=False, case_path=self.get_working_folder(), ) - scan_task.adjustment.rf.larmor_frequency = cfg.LARMOR_FREQ - log.info("Pulseq ran, plotting") self.rxd = rxd - - # Debug - Debug = True - if Debug is True: # todo: debug mode - log.info("Plotting figure now") - # view_traj.view_sig(rxd) - - plt.clf() - plt.title("ADC Signal") - plt.grid(True, color="#333") - plt.plot(np.abs(rxd)) - # if self.param_debug_plot: - # plt.show() - - file = open(self.get_working_folder() + "/other/rf_se.plot", "wb") - fig = plt.gcf() - pickle.dump(fig, file) - file.close() - - result = ResultItem() - result.name = "ADC" - result.description = "Recorded ADC signal" - result.type = "plot" - result.primary = True - result.autoload_viewer = 1 - result.file_path = "other/rf_se.plot" - scan_task.results.append(result) + log.info("Shape of rx data:", rxd.shape) + # Compute the average + rxd_rs = np.reshape(rxd, (int(rxd.shape[0]/self.param_NSA), self.param_NSA), order='F') + # log.info("New shape of rx data:", rxd_rs.shape) + rxd_avg = (np.average(rxd_rs, axis=1)) + log.info("Done running sequence " + self.get_name()) + log.info("Ran sequence at " + str(cfg.LARMOR_FREQ) + " MHz") + log.info("Plotting figures") + + plt.clf() + plt.title(f"ADC Signal") + plt.grid(True, color="#333") + log.info("Plotting averaged raw signal") + dt = self.param_ADC_duration / self.param_ADC_samples + log.info("dt: ", dt) + log.info(self.param_ADC_duration) + t = np.arange(0, self.param_ADC_duration, dt).T + plt.plot(t, np.abs(rxd_avg)) + # plt.plot(np.abs(rxd_avg)) + plt.xlabel('Time [us]') + plt.ylabel('Signal') + + file = open(self.get_working_folder() + "/other/adc.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + result = ResultItem() + result.name = "ADC" + result.description = "Acquired ADC signal" + result.type = "plot" + result.autoload_viewer = 1 + result.file_path = "other/adc.plot" + scan_task.results.insert(0, result) + + plt.clf() + plt.title(f"FFT of Signal") + recon = np.fft.fftshift(np.fft.ifft(np.fft.fftshift(rxd_avg))) + plt.grid(True, color="#333") + # dt is already in us so no need to convert + df = 1 / (self.param_ADC_duration) + f = 1e3 * np.arange(-1 / (2 * dt), 1 / (2 * dt), df).T + log.info("df: ", df) + log.info("f shape: ", f.shape) + log.info("recon shape: ", recon.shape) + plt.plot(f, np.abs(recon)) + plt.xlabel('Frequency [kHz]') + plt.ylabel('Signal') + file = open(self.get_working_folder() + "/other/fft.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + result = ResultItem() + result.name = "FFT" + result.description = "FFT of ADC signal" + result.type = "plot" + result.autoload_viewer = 2 + result.primary = True + result.file_path = "other/fft.plot" + scan_task.results.insert(1, result) + + # # Debug + # Debug = True + # if Debug is True: # todo: debug mode + # log.info("Plotting figure now") + # # view_traj.view_sig(rxd) + + # plt.clf() + # plt.title("ADC Signal") + # plt.grid(True, color="#333") + # plt.plot(np.abs(rxd)) + # # if self.param_debug_plot: + # # plt.show() + + # file = open(self.get_working_folder() + "/other/rf_se.plot", "wb") + # fig = plt.gcf() + # pickle.dump(fig, file) + # file.close() + + # result = ResultItem() + # result.name = "ADC" + # result.description = "Recorded ADC signal" + # result.type = "plot" + # result.primary = True + # result.autoload_viewer = 1 + # result.file_path = "other/rf_se.plot" + # scan_task.results.append(result) log.info("Done running sequence " + self.get_name()) return True diff --git a/sequences/rf_se/interface.ui b/sequences/rf_se/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/rf_tse.py b/sequences/rf_tse.py old mode 100644 new mode 100755 index 1bca34b..6652960 --- a/sequences/rf_tse.py +++ b/sequences/rf_tse.py @@ -117,7 +117,6 @@ def run_sequence(self, scan_task) -> bool: gui_test=False, case_path=self.get_working_folder(), ) - scan_task.adjustment.rf.larmor_frequency = cfg.LARMOR_FREQ plt.clf() plt.title("ADC Signal") @@ -148,10 +147,10 @@ def pypulseq_rftse(inputs=None, check_timing=True, output_file="") -> bool: LARMOR_FREQ = cfg.LARMOR_FREQ RF_MAX = cfg.RF_MAX RF_PI2_FRACTION = cfg.RF_PI2_FRACTION - alpha1 = cfg.DBG_FA_EXC # flip angle - alpha1_duration = 80e-6 # pulse duration - alpha2 = cfg.DBG_FA_REF # refocusing flip angle - alpha2_duration = 80e-6 # pulse duration + alpha1 = 90 # flip angle + alpha1_duration = 100e-6 # pulse duration + alpha2 = 180 # refocusing flip angle + alpha2_duration = 100e-6 # pulse duration TE = inputs["TE"] / 1000 # TE = 54e-3 # TODO: Debug -- increase TR to always have enough space TR = 2000e-3 + TE * inputs["ETL"] diff --git a/sequences/rf_tse/interface.ui b/sequences/rf_tse/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/se_1D.py b/sequences/se_1D.py old mode 100644 new mode 100755 index 76a7bae..1cd3904 --- a/sequences/se_1D.py +++ b/sequences/se_1D.py @@ -4,14 +4,11 @@ import numpy as np import matplotlib.pyplot as plt import pickle - from common.types import ResultItem from PyQt5 import uic - import pypulseq as pp # type: ignore import external.seq.adjustments_acq.config as cfg from external.seq.adjustments_acq.scripts import run_pulseq - from sequences import PulseqSequence from sequences.common import make_se_1D import common.logger as logger @@ -22,12 +19,12 @@ class SequenceRF_SE(PulseqSequence, registry_key=Path(__file__).stem): # Sequence parameters - param_TE: int = 50 + param_TE: int = 10 param_TR: int = 3000 param_NSA: int = 1 - param_FOV: int = 20 - param_Base_Resolution: int = 96 - param_BW: int = 32000 + param_FOV: int = 64 + param_Base_Resolution: int = 64 + param_BW: int = 16000 param_Gradient: str = "x" param_debug_plot: bool = True @@ -59,12 +56,12 @@ def get_parameters(self) -> dict: @classmethod def get_default_parameters(self) -> dict: return { - "TE": 20, + "TE": 5, "TR": 1000, "NSA": 1, - "FOV": 15, - "Base_Resolution": 256, - "BW": 32000, + "FOV": 64, + "Base_Resolution": 64, + "BW": 16000, "Gradient": "x", } @@ -111,14 +108,37 @@ def validate_parameters(self, scan_task) -> bool: self.problem_list.append("TE cannot be longer than TR") return self.is_valid() - def calculate_sequence(self, scan_task) -> bool: + def calculate_sequence(self, scan_task)-> bool: log.info("Calculating sequence " + self.get_name()) scan_task.processing.recon_mode = "bypass" self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" plt.clf() plt.title("Sequence") - + channel = self.param_Gradient + if channel == "x": + max_grad = cfg.GX_MAX + elif channel == "y": + max_grad = cfg.GY_MAX + elif channel == "z": + max_grad = cfg.GZ_MAX + + # seq = pp.Sequence() + self.system = pp.Opts( + max_grad=max_grad, + grad_unit="Hz/m", # + max_slew=1000, + slew_unit="T/m/s", + #rf_ringdown_time=100e-6, + rf_ringdown_time=20e-6, + rf_dead_time=100e-6, + rf_raster_time=1e-6, + #adc_dead_time=10e-6, + adc_dead_time=20e-6, + grad_raster_time = 1/self.param_BW, + B0=0.27, + ) + log.info("Using system config: ", self.system) make_se_1D.pypulseq_1dse( inputs={ "TE": self.param_TE, @@ -128,11 +148,12 @@ def calculate_sequence(self, scan_task) -> bool: "Base_Resolution": self.param_Base_Resolution, "BW": self.param_BW, "Gradient": self.param_Gradient, + "system": self.system, }, check_timing=True, output_file=self.seq_file_path, ) - + file = open(self.get_working_folder() + "/other/seq1.plot", "wb") fig = plt.figure(1) pickle.dump(fig, file) @@ -161,30 +182,31 @@ def calculate_sequence(self, scan_task) -> bool: def run_sequence(self, scan_task) -> bool: log.info("Running sequence " + self.get_name()) - rxd, rx_t = run_pulseq( - seq_file=self.seq_file_path, - rf_center=cfg.LARMOR_FREQ, - tx_t=1, - grad_t=10, - tx_warmup=100, - shim_x=-0.0, - shim_y=-0.0, - shim_z=-0.0, - grad_cal=False, - save_np=False, - save_mat=False, - save_msgs=True, - gui_test=False, - case_path=self.get_working_folder(), + rxd, _ = run_pulseq( + seq_file=self.seq_file_path, + rf_center=cfg.LARMOR_FREQ, #scan_task.adjustment.rf.larmor_frequency, + tx_t=1, + grad_t=np.round(self.system.grad_raster_time * 1e6, decimals=0), # us + tx_warmup=100, + shim_x=cfg.SHIM_X, + shim_y=cfg.SHIM_Y, + shim_z=cfg.SHIM_Z, + grad_cal=False, + save_np=False, + save_mat=False, + save_msgs=True, + gui_test=False, + case_path=self.get_working_folder(), + system = self.system ) - scan_task.adjustment.rf.larmor_frequency = cfg.LARMOR_FREQ - - log.info("Done running sequence " + self.get_name()) - - # Compute the average + # Compute the average rxd_rs = np.reshape(rxd, (int(rxd.shape[0]/self.param_NSA), self.param_NSA), order='F') log.info("New shape of rx data:", rxd_rs.shape) rxd_avg = (np.average(rxd_rs, axis=1)) + filtering = False + if filtering is True: + rxd_avg = np.convolve(rxd_avg, np.ones(5)/5, mode='same') + log.info("Done running sequence " + self.get_name()) log.info("Plotting figures") @@ -192,7 +214,13 @@ def run_sequence(self, scan_task) -> bool: plt.title(f"ADC Signal - Grad_{self.param_Gradient}") plt.grid(True, color="#333") log.info("Plotting averaged raw signal") - plt.plot(np.abs(rxd_avg)) + dt = 1e6 / self.param_BW + log.info("dt: ", dt) + + t = np.arange(0, self.param_Base_Resolution * dt * 2, dt).T # oversampling by factor 2 + plt.plot(t, np.abs(rxd_avg)) + plt.xlabel('Time (us)') + plt.ylabel('Signal') file = open(self.get_working_folder() + "/other/adc.plot", "wb") fig = plt.gcf() @@ -210,7 +238,13 @@ def run_sequence(self, scan_task) -> bool: plt.title(f"FFT of Signal - Grad_{self.param_Gradient}") recon = np.fft.fftshift(np.fft.ifft(np.fft.fftshift(rxd_avg))) plt.grid(True, color="#333") - plt.plot(np.abs(recon)) + kmax_half = -self.param_Base_Resolution / self.param_FOV / 2 + k_array = np.linspace(-kmax_half, kmax_half, self.param_Base_Resolution) + + r = np.linspace(-self.param_FOV/2, self.param_FOV/2, self.param_Base_Resolution * 2) + plt.plot(r, np.abs(recon)) + plt.xlabel("Position (mm)") + plt.ylabel("Projection") file = open(self.get_working_folder() + "/other/fft.plot", "wb") fig = plt.gcf() pickle.dump(fig, file) @@ -223,7 +257,7 @@ def run_sequence(self, scan_task) -> bool: result.primary = True result.file_path = "other/fft.plot" scan_task.results.insert(1, result) - + # Save the raw data file log.info("Saving rawdata, sequence " + self.get_name()) self.raw_file_path = self.get_working_folder() + "/rawdata/raw.npy" diff --git a/sequences/se_1D/interface.ui b/sequences/se_1D/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/se_2D.py b/sequences/se_2D.py old mode 100644 new mode 100755 index 532162c..cd666f8 --- a/sequences/se_2D.py +++ b/sequences/se_2D.py @@ -4,37 +4,38 @@ import numpy as np import matplotlib.pyplot as plt from PyQt5 import uic - +import pickle import pypulseq as pp # type: ignore import external.seq.adjustments_acq.config as cfg from external.seq.adjustments_acq.scripts import run_pulseq - +from sequences.common.get_trajectory import choose_pe_order from sequences import PulseqSequence from sequences.common import make_se_2D from sequences.common import view_traj import common.logger as logger from common.types import ResultItem +import sigpy as sp log = logger.get_logger() class SequenceSE_2D(PulseqSequence, registry_key=Path(__file__).stem): # Sequence parameters - param_TE: int = 20 - param_TR: int = 3000 + param_TE: int = 5 + param_TR: int = 1000 param_NSA: int = 1 - param_FOV: int = 20 - param_Orientation: str = "Axial" - param_Base_Resolution: int = 96 - param_BW: int = 32000 - param_Trajectory: str = "Catisian" + param_FOV: int = 128 + param_Orientation: str = "Coronal" + param_Base_Resolution: int = 32 + param_BW: int = 16000 + param_Trajectory: str = "Cartesian" param_PE_Ordering: str = "Center_out" param_PF: int = 1 param_view_traj: bool = True @classmethod def get_readable_name(self) -> str: - return "2D Spin-Echo [untested]" + return "2D Spin-Echo" def setup_ui(self, widget) -> bool: seq_path = os.path.dirname(os.path.abspath(__file__)) @@ -59,17 +60,17 @@ def get_parameters(self) -> dict: @classmethod def get_default_parameters(self) -> dict: return { - "TE": 20, - "TR": 3000, + "TE": 5, + "TR": 100, "NSA": 1, - "FOV": 20, + "FOV": 64, "Orientation": "Axial", - "Base_Resolution": 96, - "BW": 32000, + "Base_Resolution": 64, + "BW": 16000, "Trajectory": "Cartesian", "PE_Ordering": "Center_out", "PF": 1, - "view_traj": True, + "view_traj": False, } def set_parameters(self, parameters, scan_task) -> bool: @@ -130,7 +131,29 @@ def validate_parameters(self, scan_task) -> bool: def calculate_sequence(self, scan_task) -> bool: self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" log.info("Calculating sequence " + self.get_name()) - + # scan_task.processing.dim = 2 + # scan_task.processing.dim_size = f"{self.param_baseresolution},{2*self.param_baseresolution}" + # scan_task.processing.oversampling_read = 2 + # scan_task.processing.recon_mode = "basic2d" + # This needs to be better done, need to user per axis max. otherwise the propotionality of the gradients will be wrong and the trajectory will be distorted. For example, if the x gradient is much stronger than the y gradient, then the trajectory will be stretched in the x direction and compressed in the y direction. This will lead to a distorted image. + max_grad = np.min([cfg.GX_MAX, cfg.GY_MAX, cfg.GZ_MAX]) + log.info(f"***** Using max gradient strength of {max_grad} Hz/m") + self.system = pp.Opts( + max_grad=max_grad, + grad_unit="Hz/m", # + max_slew=1000, + slew_unit="T/m/s", + #rf_ringdown_time=100e-6, + rf_ringdown_time=20e-6, + rf_dead_time=100e-6, + rf_raster_time=1e-6, + #adc_dead_time=10e-6, + adc_dead_time=20e-6, + grad_raster_time = 1/self.param_BW, + B0=0.27, + ) + log.info("Using system config: ", self.system) + # ToDo: if self.Trajectory == "Cartesian": (default) make_se_2D.pypulseq_se2D( inputs={ @@ -145,6 +168,7 @@ def calculate_sequence(self, scan_task) -> bool: "PE_Ordering": self.param_PE_Ordering, "PF": self.param_PF, "view_traj": self.param_view_traj, + "system": self.system, }, check_timing=True, output_file=self.seq_file_path, @@ -170,59 +194,179 @@ def calculate_sequence(self, scan_task) -> bool: scan_task.results.append(result) return True + + def run_sequence(self, scan_task) -> bool: log.info("Running sequence " + self.get_name()) - rxd, rx_t = run_pulseq( + expected_duration_sec = int( + self.param_TR + * (self.param_Base_Resolution) + / 1000 + ) + + rxd, _ = run_pulseq( seq_file=self.seq_file_path, rf_center=cfg.LARMOR_FREQ, tx_t=1, - grad_t=10, + grad_t= np.round(self.system.grad_raster_time * 1e6, decimals=0), tx_warmup=100, - shim_x=0, - shim_y=0, - shim_z=0, + shim_x=cfg.SHIM_X, + shim_y=cfg.SHIM_Y, + shim_z=cfg.SHIM_Z, grad_cal=False, save_np=False, save_mat=False, save_msgs=False, gui_test=False, case_path=self.get_working_folder(), + expected_duration_sec=expected_duration_sec, + system = self.system, ) + # # Compute the average + self.param_oversampling = 2 + rxd_rs = np.reshape(rxd, (self.param_oversampling * self.param_Base_Resolution, int(self.param_Base_Resolution), self.param_NSA), order='F') + log.info("type of rx data:", type(rxd_rs)) + log.info("New shape of rx data:", rxd_rs.shape) + rxd_avg = (np.average(rxd_rs, axis=2)) + rxd_avg = np.squeeze(rxd_avg) + log.info("Shape of averaged rx data:", rxd_avg.shape) log.info("Done running sequence " + self.get_name()) - # test for recon testing - data = rxd.reshape((70, 70)) - plt.figure() - plt.subplot(131) + # Generate phase areas for inside-out ordering + if self.param_PE_Ordering == "Center_out": + Ny = self.param_Base_Resolution + pe_table = np.zeros(Ny, dtype=int) + center_index = Ny // 2 + + for i in range(Ny -1): + if i % 2 == 0: + pe_table[i] = center_index - (i // 2) + else: + pe_table[i] = center_index + (i // 2 + 1) + + log.info('Maximum phase encode value:', np.max(pe_table)) + # reformat the data according to the phase encoding order + if self.param_PE_Ordering == "Center_out": + rxd_avg_ordered = np.zeros_like(rxd_avg) + for i in range(self.param_Base_Resolution): + rxd_avg_ordered[:, pe_table[i]] = rxd_avg[:, i] + rxd_avg = rxd_avg_ordered + + + + + # data = rxd_avg.reshape((2 * self.param_Base_Resolution, self.param_Base_Resolution)) + # log.info("Shape of data:", data.shape) + + + + + filtering = False + filt_type = "Gaussian" # "convolution" or "Gaussian" + if filtering is True: + if filt_type == "convolution": + log.info("Applying convolution filter to data") + # Apply a convolution filter to the data + # rxd_avg = np.convolve(rxd_avg, np.ones(9)/9, mode='same') + # rxd_avg = np.apply_along_axis(lambda m: np.convolve(m, np.ones(9)/9, mode='same'), axis=0, arr=rxd_avg) + for i in range(rxd_avg.shape[1]): + rxd_avg[:, i] = np.convolve(rxd_avg[:, i], np.ones(9)/9, mode='same') + elif filt_type == "Gaussian": + log.info("Applying a 2D Gaussian filter to data") + # Apply a 2D Gaussian filter to the data + x = np.linspace(-1, 1, rxd_avg.shape[0]) + y = np.linspace(-1, 1, rxd_avg.shape[1]) + xv, yv = np.meshgrid(x, y, indexing='ij') + sigma = 0.7 # Standard deviation of the Gaussian + gaussian_filter = np.exp(-((xv**2 + yv**2) / (2 * sigma**2))) + rxd_avg = rxd_avg * gaussian_filter + + + # data = rxd_avg #rxd_avg.reshape((self.param_Base_Resolution, 2 * self.param_Base_Resolution)) + data = rxd_avg.reshape((self.param_Base_Resolution * self.param_oversampling, self.param_Base_Resolution)) + log.info("Plotting figures") + + kspace_chop = False + if kspace_chop is True: + # filter = np.zeros(data.shape) + flat_start = 5 + flat_stop = 90 + data2 = np.zeros(data.shape, dtype=complex) + data2[:, flat_start: flat_stop] = data[:, flat_start: flat_stop] + # filter[:, flat_start:flat_stop] = 1 + # filter[:, 0:flat_start] = np.ones((data.shape[0], 10)) * 0 #np.linspace(0, 1, flat_start) + # filter[:, flat_stop:] = np.ones((data.shape[0], 10)) * 0 #np.linspace(1, 0, flat_start) + # data = np.multiply(data, filter) + data = data2 + + nex_recon = False + if nex_recon is True: + data2 = np.zeros(data.shape, dtype=complex) + mid = data.shape[1]//2 + add_lines=10 + data2[:, :mid + add_lines] = data[:, :mid + add_lines] + data2[:, mid + add_lines:] = np.fliplr(np.conj(data[:, :mid - add_lines])) + data = data2 + + plt.clf() + plt.title(f"k-space data") + # plt.grid(True, color="#333") + #log.info("Plotting averaged raw signal") plt.imshow(np.abs(data)) - plt.title("kspace, abs") - plt.subplot(132) - plt.imshow(np.real(data)) - plt.title("real") - plt.subplot(133) - plt.imshow(np.imag(data)) - plt.title("imag") - plt.show() - - img = np.fft.fft2(data) - plt.figure() - plt.subplot(131) - plt.imshow(np.abs(img)) - plt.title("image, abs") - plt.subplot(132) - plt.imshow(np.real(img)) - plt.title("real") - plt.subplot(133) - plt.imshow(np.imag(img)) - plt.title("imag") - plt.show() + plt.set_cmap('jet') + plt.clim(0,1.2*np.max(abs(data))) + file = open(self.get_working_folder() + "/other/kspace.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + result = ResultItem() + result.name = "k-space" + result.description = "Acquired k-space" + result.type = "plot" + result.autoload_viewer = 1 + result.file_path = "other/kspace.plot" + scan_task.results.insert(0, result) + + plt.clf() + plt.title(f"Image data") + # recon = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(data))) + # recon = np.fft.fftshift(np.fft.fft2(data)) + recon = sp.fft(data, norm='ortho') + # recon = (np.fft.fft2((data))) + + + # plt.grid(True, color="#333") + crop_top = int(self.param_Base_Resolution * 0.25 * 2) + crop_bottom = int(self.param_Base_Resolution * 0.75 * 2) + recon2 = np.squeeze(recon[crop_top:crop_bottom, :]) + + + + + + # recon2 = np.fft.fftshift(np.abs(recon2), axes = 0) + plt.imshow(np.abs(recon)) + plt.set_cmap('gray') + file = open(self.get_working_folder() + "/other/fft.plot", "wb") + fig = plt.gcf() + pickle.dump(fig, file) + file.close() + result = ResultItem() + result.name = "Image" + result.description = "Image data" + result.type = "plot" + result.autoload_viewer = 2 + result.primary = True + result.file_path = "other/fft.plot" + scan_task.results.insert(1, result) + # save the raw data file self.raw_file_path = self.get_working_folder() + "/rawdata/raw.npy" - np.save(self.raw_file_path, rxd) + np.save(self.raw_file_path, data) log.info("Saving rawdata, sequence " + self.get_name()) return True diff --git a/sequences/se_2D/interface.ui b/sequences/se_2D/interface.ui old mode 100644 new mode 100755 diff --git a/sequences/se_3D.py b/sequences/se_3D.py old mode 100644 new mode 100755 diff --git a/sequences/tse3d_demo.py b/sequences/tse3d_demo.py old mode 100644 new mode 100755 diff --git a/sequences/tse_2D.py b/sequences/tse_2D.py old mode 100644 new mode 100755 diff --git a/sequences/tse_3D.py b/sequences/tse_3D.py old mode 100644 new mode 100755 index e8ff929..560fd84 --- a/sequences/tse_3D.py +++ b/sequences/tse_3D.py @@ -183,7 +183,7 @@ def calculate_sequence(self, scan_task) -> bool: log.info("Calculating sequence " + self.get_name()) ipc_comm.send_status(f"Calculating sequence...") - if config.get_config().is_hardware_simulation(): + if config.get_config().hardware_simulation: scan_task.processing.recon_mode = "bypass" else: scan_task.processing.recon_mode = "basic3d" @@ -193,13 +193,6 @@ def calculate_sequence(self, scan_task) -> bool: scan_task.processing.oversampling_read = 2 self.seq_file_path = self.get_working_folder() + "/seq/acq0.seq" - fa_exc = cfg.DBG_FA_EXC - fa_ref = cfg.DBG_FA_REF - if "FA1" in scan_task.other: - fa_exc = int(scan_task.other["FA1"]) - if "FA2" in scan_task.other: - fa_ref = int(scan_task.other["FA2"]) - if not make_tse_3D.pypulseq_tse3D( inputs={ "TE": self.param_TE, @@ -215,8 +208,6 @@ def calculate_sequence(self, scan_task) -> bool: "Ordering": self.param_Ordering, "Plot_Timing": self.param_plot_timing, "dummy_shots": self.param_dummy_shots, - "FA1": fa_exc, - "FA2": fa_ref, }, check_timing=True, output_file=self.seq_file_path, @@ -246,7 +237,7 @@ def run_sequence(self, scan_task) -> bool: plot_instructions = self.param_plot_timing - rxd, rx_t = run_pulseq( + rdx, rx_t = run_pulseq( seq_file=self.seq_file_path, rf_center=cfg.LARMOR_FREQ, tx_t=1, @@ -268,9 +259,8 @@ def run_sequence(self, scan_task) -> bool: raw_filename="raw", expected_duration_sec=expected_duration_sec, plot_instructions=plot_instructions, - hardware_simulation=config.get_config().is_hardware_simulation() == "True", + hardware_simulation=config.get_config().hardware_simulation, ) - scan_task.adjustment.rf.larmor_frequency = cfg.LARMOR_FREQ if plot_instructions: file = open(self.get_working_folder() + "/other/seq.plot", "wb") diff --git a/sequences/tse_3D/interface.ui b/sequences/tse_3D/interface.ui old mode 100644 new mode 100755 diff --git a/services/__init__.py b/services/__init__.py old mode 100644 new mode 100755 diff --git a/services/acq/__init__.py b/services/acq/__init__.py old mode 100644 new mode 100755 diff --git a/services/acq/main.py b/services/acq/main.py old mode 100644 new mode 100755 index 248bd5b..6deee3d --- a/services/acq/main.py +++ b/services/acq/main.py @@ -26,9 +26,6 @@ import common.plotting as plotting import common.config as config -import external.seq.adjustments_acq.config as cfg - - main_loop = None # type: helper.AsyncTimer # type: ignore communicator = Communicator(Communicator.ACQ) @@ -81,12 +78,6 @@ def process_acquisition(scan_name: str) -> bool: mri4all_paths.DATA_ACQ + "/" + scan_name, mri4all_taskdata.SEQ ) - try: - # TODO: Replace with better management of scanner settings - cfg.update() - except: - log.warn("Unable to update configuration") - current_step = "" try: current_step = "instantiation" diff --git a/services/recon/__init__.py b/services/recon/__init__.py old mode 100644 new mode 100755 diff --git a/services/recon/main.py b/services/recon/main.py old mode 100644 new mode 100755 diff --git a/services/recon/reconstruction.py b/services/recon/reconstruction.py old mode 100644 new mode 100755 index 62993fd..0faaeab --- a/services/recon/reconstruction.py +++ b/services/recon/reconstruction.py @@ -8,7 +8,7 @@ from common.constants import * from common.types import ScanTask import services.recon.utils as utils - +import recon.recon_utils as ru from recon.kspaceFiltering.kspace_filtering import * from recon.B0Correction import B0Corrector import recon.DICOM.DICOM_utils as DICOM @@ -103,14 +103,6 @@ def run_reconstruction_basic3d(folder: str, task: ScanTask) -> bool: fft = np.fft.fftshift(np.fft.fftn(np.fft.fftshift(kSpace))) - base_res = fft.shape[0] - for sample in range(0, base_res): - fft[sample, :, :] = fft[sample, :, :] * np.exp( - # np.pi * 1j + base_res / 16 * (sample - base_res / 2) / (2 * base_res) * np.pi * 1j - np.pi * 1j - + (sample - base_res / 2) / 32 * np.pi * 1j - ) - if task.processing.oversampling_read > 0: offset = int(dims[2]) / 4 fft = fft[int(offset) : int(3 * offset), :, :] @@ -127,24 +119,13 @@ def run_reconstruction_basic3d(folder: str, task: ScanTask) -> bool: name="k-Space", primary_result=False, autoload_viewer=2, - result_index=2, - ) - - DICOM.write_dicom( - np.angle(fft), - task, - folder + "/" + mri4all_taskdata.DICOM, - series_offset=2, - name="Phase", - primary_result=False, - result_index=3, - autoload_viewer=3, + result_index=1, ) return True -def run_reconstruction_cartesian(folder: str, task: ScanTask): +def run_reconstruction_cartesian(folder: str, task: ScanTask)-> bool: """ Runs the reconstruction pipeline for Cartesian sampling """ @@ -157,30 +138,42 @@ def run_reconstruction_cartesian(folder: str, task: ScanTask): kData = np.load( folder + "/" + mri4all_taskdata.RAWDATA + "/" + mri4all_scanfiles.RAWDATA ) - kTraj = np.genfromtxt( - folder + "/" + mri4all_taskdata.RAWDATA + "/" + mri4all_scanfiles.TRAJ, - delimiter=",", - ) # pe_table a lot by 2 # check rotation - - if kTraj.shape[0] > 2: - kTraj = np.rot90(kTraj) - # grad_delay_correction(kData, kTraj, delayT, param) - - filterType = "fermi" - kData = kFilter(kData, filterType, center_correction=True) - log.info(f"kSpace {filterType} filtering finished.") - - # Preform B0 correction and reconstruct the image - fname_B0_map = list(filter(lambda x: mri4all_scanfiles.BDATA in x, fnames)) - Y = kData - kt = kTraj - df = np.load(path.join(folder, fname_B0_map[0])) if fname_B0_map else None - Lx = 1 - nonCart = None - params = None - b0_corrector = B0Corrector(Y, kt, df, Lx, nonCart, params) - iData = b0_corrector() - log.info(f"B0 correction finished.") + + # Display k-space and image data + + + # filter kspace data + + + iData = ru.centered_ifft(kData) + + + + log.info("Reconstruction done!") + # kTraj = np.genfromtxt( + # folder + "/" + mri4all_taskdata.RAWDATA + "/" + mri4all_scanfiles.TRAJ, + # delimiter=",", + # ) # pe_table a lot by 2 # check rotation + + # if kTraj.shape[0] > 2: + # kTraj = np.rot90(kTraj) + # # grad_delay_correction(kData, kTraj, delayT, param) + + # filterType = "fermi" + # kData = kFilter(kData, filterType, center_correction=True) + # log.info(f"kSpace {filterType} filtering finished.") + + # # Preform B0 correction and reconstruct the image + # fname_B0_map = list(filter(lambda x: mri4all_scanfiles.BDATA in x, fnames)) + # Y = kData + # kt = kTraj + # df = np.load(path.join(folder, fname_B0_map[0])) if fname_B0_map else None + # Lx = 1 + # nonCart = None + # params = None + # b0_corrector = B0Corrector(Y, kt, df, Lx, nonCart, params) + # iData = b0_corrector() + # log.info(f"B0 correction finished.") # Denoise the image try: @@ -193,10 +186,20 @@ def run_reconstruction_cartesian(folder: str, task: ScanTask): log.error(f"Image denoising failed.") # Create the DICOM file - DICOM.write_dicom(iData, task, folder + "/" + mri4all_taskdata.DICOM) + if len(iData.shape) < 3: # 2D case + log.info(iData.shape) + log.info('Reshaping image data') + sz = iData.shape + iData2 = np.zeros((sz[0], sz[1], 1), dtype=float) + iData2[:, :, 0] = iData + else: + iData2 = iData + + DICOM.write_dicom(iData2, task, folder + "/" + mri4all_taskdata.DICOM) log.info(f"DICOM writting finished.") # Create the ISMRMRD file # TODO: Enable ISMRMRD creation after bug fix - create_ismrmrd(folder, kData, task) - log.info(f"ISMRMRD format writting finished.") + # create_ismrmrd(folder, kData, task) + # log.info(f"ISMRMRD format writting finished.") + return True diff --git a/services/recon/utils.py b/services/recon/utils.py old mode 100644 new mode 100755 diff --git a/services/ui/__init__.py b/services/ui/__init__.py old mode 100644 new mode 100755 diff --git a/services/ui/about.py b/services/ui/about.py old mode 100644 new mode 100755 diff --git a/services/ui/configuration.py b/services/ui/configuration.py old mode 100644 new mode 100755 diff --git a/services/ui/control.py b/services/ui/control.py old mode 100644 new mode 100755 diff --git a/services/ui/custommessagebox.py b/services/ui/custommessagebox.py old mode 100644 new mode 100755 diff --git a/services/ui/dicomexport.py b/services/ui/dicomexport.py old mode 100644 new mode 100755 diff --git a/services/ui/errors.py b/services/ui/errors.py old mode 100644 new mode 100755 diff --git a/services/ui/examination.py b/services/ui/examination.py old mode 100644 new mode 100755 index bbbd70d..cec5be4 --- a/services/ui/examination.py +++ b/services/ui/examination.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta import json -import time from PyQt5 import uic from PyQt5.QtCore import * @@ -36,7 +35,8 @@ import services.ui.control as control from services.ui.errors import SequenceUIFailed, UIException - +# Adjustments import +from sequences.common.util import reading_json_parameter, writing_json_parameter import external.seq.adjustments_acq.config as cfg log = logger.get_logger() @@ -219,18 +219,6 @@ def __init__(self): 5, qta.icon("fa5s.exclamation-circle", color="#E5554F") ) self.scanParametersWidget.setTabVisible(5, False) - self.scanParametersWidget.currentChanged.connect( - self.scanParametersWidgetChanged - ) - self.larmorUpdateButton.setProperty("type", "toolbar") - self.larmorUpdateButton.clicked.connect(self.update_larmor_clicked) - self.larmorUpdateButton.setStyleSheet( - "QPushButton:hover { background-color: #E0A526; color: #FFF }" - ) - self.larmorUpdateButton.setIcon(qta.icon("fa5s.check")) - self.larmorUpdateButton.setText(" Update") - self.larmorUpdateButton.setToolTip("Update the Larmor frequency") - # self.larmorUpdateButton.setIconSize(QSize(24, 24)) self.problemsWidget.setStyleSheet( """ @@ -1374,26 +1362,25 @@ def load_seqparam_to_ui(self, scan_task): def store_seqparam_from_ui(self, scan_task): scan_task.processing.denoising_strength = self.denoisingSlider.value() + ### Introducing set transmit frequency feature in adjustments + + def ui_TX_Freq(self): + value = self.TX_Freq_DoubleSpinBox.value() + + def load_seqparam_to_ui(self, scan_task): + TX_Freq = scan_task.adjustment.rf.larmor_frequency + self.TX_Freq_DoubleSpinBox.setValue(TX_Freq) + + def store_seqparam_from_ui(self, scan_task): + # scan_task.adjustment.rf.larmor_frequency = self.TX_Freq_DoubleSpinBox.value() + configuration_data = reading_json_parameter() + # configuration_data.rf_parameters.larmor_frequency_MHz = scan_task.adjustment.rf.larmor_frequency + writing_json_parameter(config_data=configuration_data) + # Reload the configuration -- otherwise it does not get updated until the next start + cfg.update() + def toggle_flexviewer(self): if self.flexViewerWindow.isVisible(): self.flexViewerWindow.setVisible(False) else: self.flexViewerWindow.setVisible(True) - - def scanParametersWidgetChanged(self, index): - # TODO: This is just a hack to make the Larmor frequency editable from the UI. Needs to be replaced with a cleaner solution. - if index == 2: - cfg.update() - self.larmorSpinBox.setValue(cfg.LARMOR_FREQ) - - def update_larmor_clicked(self): - # TODO: This is just a hack to make the Larmor frequency editable from the UI. Needs to be replaced with a cleaner solution. - from sequences.common.util import reading_json_parameter, writing_json_parameter - - configuration_data = reading_json_parameter() - configuration_data.rf_parameters.larmor_frequency_MHz = ( - self.larmorSpinBox.value() - ) - writing_json_parameter(config_data=configuration_data) - cfg.update() - pass diff --git a/services/ui/flexviewer.py b/services/ui/flexviewer.py old mode 100644 new mode 100755 diff --git a/services/ui/forms/about.ui b/services/ui/forms/about.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/configuration.ui b/services/ui/forms/configuration.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/custommessagebox.ui b/services/ui/forms/custommessagebox.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/examination.ui b/services/ui/forms/examination.ui old mode 100644 new mode 100755 index 47bcfca..12f7caa --- a/services/ui/forms/examination.ui +++ b/services/ui/forms/examination.ui @@ -570,123 +570,68 @@ QTabWidget::Rounded - 1 + 0 + + true + + + + ADJUSTMENTS + + + + 28 + 70 + 100 + 26 + + + + + 100 + 0 + + + + Set freq. + + + + + + 93 + 70 + 111 + 26 + + + + 6 + + + + + + 210 + 70 + 41 + 26 + + + + MHz + + SYSTEM - - - - - 6 - - - - - - 110 - 0 - - - - Larmor Freq. - - - - - - - - - - 4 - - - 1.800000000000000 - - - 1.900000000000000 - - - 0.001000000000000 - - - QAbstractSpinBox::DefaultStepType - - - 1.820000000000000 - - - - - - - MHz - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 12 - 20 - - - - - - - - Update - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - - 20 - 617 - - - - - diff --git a/services/ui/forms/flexviewer.ui b/services/ui/forms/flexviewer.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/logviewer.ui b/services/ui/forms/logviewer.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/protocolbrowser.ui b/services/ui/forms/protocolbrowser.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/registration.ui b/services/ui/forms/registration.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/shimbox.ui b/services/ui/forms/shimbox.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/studyviewer.ui b/services/ui/forms/studyviewer.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/systemstatus.ui b/services/ui/forms/systemstatus.ui old mode 100644 new mode 100755 diff --git a/services/ui/forms/taskviewer.ui b/services/ui/forms/taskviewer.ui old mode 100644 new mode 100755 diff --git a/services/ui/logviewer.py b/services/ui/logviewer.py old mode 100644 new mode 100755 diff --git a/services/ui/main.py b/services/ui/main.py old mode 100644 new mode 100755 index ed721d2..c9ed744 --- a/services/ui/main.py +++ b/services/ui/main.py @@ -131,7 +131,7 @@ def prepare_system() -> bool: rt.set_debug(True) ui_runtime.system_information.name = "dev-system1" - ui_runtime.system_information.model = "Zeugmatron Z1" + ui_runtime.system_information.model = "Tenacity" ui_runtime.system_information.serial_number = "000001" ui_runtime.system_information.software_version = mri4all_version.get_version_string() diff --git a/services/ui/protocolbrowser.py b/services/ui/protocolbrowser.py old mode 100644 new mode 100755 diff --git a/services/ui/registration.py b/services/ui/registration.py old mode 100644 new mode 100755 diff --git a/services/ui/shimbox.py b/services/ui/shimbox.py old mode 100644 new mode 100755 diff --git a/services/ui/studyviewer.py b/services/ui/studyviewer.py old mode 100644 new mode 100755 diff --git a/services/ui/systemstatus.py b/services/ui/systemstatus.py old mode 100644 new mode 100755 diff --git a/services/ui/taskviewer.py b/services/ui/taskviewer.py old mode 100644 new mode 100755 diff --git a/services/ui/ui_runtime.py b/services/ui/ui_runtime.py old mode 100644 new mode 100755 index df77b3d..66ae196 --- a/services/ui/ui_runtime.py +++ b/services/ui/ui_runtime.py @@ -156,7 +156,6 @@ def is_exam_active() -> bool: else: return False - def get_scan_queue_entry(index: int) -> Any: global scan_queue_list @@ -228,20 +227,16 @@ def update_scan_queue_list() -> bool: return True -def create_new_scan(requested_sequence: str, overwrite_name: str = "") -> bool: +def create_new_scan(requested_sequence: str) -> bool: global system_information global exam_information global patient_information exam_information.scan_counter += 1 scan_uid = helper.generate_uid() - - if overwrite_name: - default_protocol_name = overwrite_name - else: - default_protocol_name = SequenceBase.get_sequence( - requested_sequence - ).get_readable_name() + default_protocol_name = SequenceBase.get_sequence( + requested_sequence + ).get_readable_name() default_seq_parameters = SequenceBase.get_sequence( requested_sequence ).get_default_parameters() @@ -286,11 +281,8 @@ def duplicate_sequence(index: int) -> bool: def duplicate_sequence_dir(template_path: str) -> bool: template_scan_data = task.read_task(template_path) - # TODO: Error handling if reading file failed - if not create_new_scan( - template_scan_data.sequence, template_scan_data.protocol_name - ): + if not create_new_scan(template_scan_data.sequence): log.error("Failed to create new scan of same sequence.") return False diff --git a/services/ui/viewerwidget.py b/services/ui/viewerwidget.py old mode 100644 new mode 100755 diff --git a/tests/test_kspace.py b/tests/test_kspace.py old mode 100644 new mode 100755 diff --git a/tests/test_platform.py b/tests/test_platform.py old mode 100644 new mode 100755 diff --git a/tests/test_recon.py b/tests/test_recon.py old mode 100644 new mode 100755 diff --git a/tests/test_sequence_factory.py b/tests/test_sequence_factory.py old mode 100644 new mode 100755 diff --git a/tests/test_sequences.py b/tests/test_sequences.py old mode 100644 new mode 100755 diff --git a/tests/test_sequences_flow.py b/tests/test_sequences_flow.py old mode 100644 new mode 100755