| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- import math
- from types import SimpleNamespace
- from typing import Tuple, List
- import matplotlib.pyplot as plt
- import LF_scanner.pypulseq as pp
- import numpy as np
- from LF_scanner.pypulseq import Sequence
- from LF_scanner.pypulseq.utils.safe_pns_prediction import safe_gwf_to_pns, safe_plot
- from LF_scanner.pypulseq.utils.siemens.readasc import readasc
- from LF_scanner.pypulseq.utils.siemens.asc_to_hw import asc_to_hw
- def calc_pns(
- obj: Sequence,
- hardware: SimpleNamespace,
- time_range: List[float] = None,
- do_plots: bool = True
- ) -> Tuple[bool, np.array, np.ndarray, np.array]:
- """
- Calculate PNS using safe model implementation by Szczepankiewicz and Witzel
- See http://github.com/filip-szczepankiewicz/safe_pns_prediction
-
- Returns pns levels due to respective axes (normalized to 1 and not to 100#)
-
- Parameters
- ----------
- hardware : SimpleNamespace
- Hardware specifications. See safe_example_hw() from
- the safe_pns_prediction package. Alternatively a text file
- in the .asc format (Siemens) can be passed, e.g. for Prisma
- it is MP_GPA_K2309_2250V_951A_AS82.asc (we leave it as an
- exercise to the interested user to find were these files
- can be acquired from)
- do_plots : bool, optional
- Plot the results from the PNS calculations. The default is True.
- Returns
- -------
- ok : bool
- Boolean flag indicating whether peak PNS is within acceptable limits
- pns_norm : numpy.array [N]
- PNS norm over all gradient channels, normalized to 1
- pns_components : numpy.array [Nx3]
- PNS levels per gradient channel
- t_pns : np.array [N]
- Time axis for the pns_norm and pns_components arrays
- """
-
- dt = obj.grad_raster_time
- # Get gradients as piecewise-polynomials
- gw_pp = obj.get_gradients(time_range=time_range)
- ng = len(gw_pp)
- max_t = max(g.x[-1] for g in gw_pp if g != None) - 1e-10
-
- # Determine sampling points
- if time_range == None:
- nt = int(np.ceil(max_t/dt))
- t = (np.arange(nt) + 0.5)*dt
- else:
- tmax = min(time_range[1], max_t) - max(time_range[0], 0)
- nt = int(np.ceil(tmax/dt))
- t = max(time_range[0], 0) + (np.arange(nt) + 0.5)*dt
-
- # Sample gradients
- gw = np.zeros((t.shape[0], ng))
- for i in range(ng):
- if gw_pp[i] != None:
- gw[:,i] = gw_pp[i](t)
-
-
- if do_plots:
- plt.figure()
- for i in range(ng):
- if gw_pp[i] != None:
- plt.plot(gw_pp[i].x[1:-1], gw_pp[i].c[1,:-1])
- plt.title('gradient wave form, in Hz/m')
-
- if type(hardware) == str:
- # this loads the parameters from the provided text file
- asc, _ = readasc(hardware)
- hardware = asc_to_hw(asc)
- # use the Szczepankiewicz' and Witzel's implementation
- [pns_comp,res] = safe_gwf_to_pns(gw/obj.system.gamma, np.nan*np.ones(t.shape[0]), obj.grad_raster_time, hardware) # the RF vector is unused in the code inside but it is zeropaded and exported ...
-
- # use the exported RF vector to detect and undo zero-padding
- pns_comp = 0.01 * pns_comp[~np.isfinite(res.rf[1:]),:]
-
- # calc pns_norm and the final ok/not_ok
- pns_norm = np.sqrt((pns_comp**2).sum(axis=1))
- ok = all(pns_norm<1)
-
- # ready
- if do_plots:
- # plot results
- plt.figure()
- safe_plot(pns_comp*100, obj.grad_raster_time)
- return ok, pns_norm, pns_comp, t
|