calc_pns.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import math
  2. from types import SimpleNamespace
  3. from typing import Tuple, List
  4. import matplotlib.pyplot as plt
  5. import LF_scanner.pypulseq as pp
  6. import numpy as np
  7. from LF_scanner.pypulseq import Sequence
  8. from LF_scanner.pypulseq.utils.safe_pns_prediction import safe_gwf_to_pns, safe_plot
  9. from LF_scanner.pypulseq.utils.siemens.readasc import readasc
  10. from LF_scanner.pypulseq.utils.siemens.asc_to_hw import asc_to_hw
  11. def calc_pns(
  12. obj: Sequence,
  13. hardware: SimpleNamespace,
  14. time_range: List[float] = None,
  15. do_plots: bool = True
  16. ) -> Tuple[bool, np.array, np.ndarray, np.array]:
  17. """
  18. Calculate PNS using safe model implementation by Szczepankiewicz and Witzel
  19. See http://github.com/filip-szczepankiewicz/safe_pns_prediction
  20. Returns pns levels due to respective axes (normalized to 1 and not to 100#)
  21. Parameters
  22. ----------
  23. hardware : SimpleNamespace
  24. Hardware specifications. See safe_example_hw() from
  25. the safe_pns_prediction package. Alternatively a text file
  26. in the .asc format (Siemens) can be passed, e.g. for Prisma
  27. it is MP_GPA_K2309_2250V_951A_AS82.asc (we leave it as an
  28. exercise to the interested user to find were these files
  29. can be acquired from)
  30. do_plots : bool, optional
  31. Plot the results from the PNS calculations. The default is True.
  32. Returns
  33. -------
  34. ok : bool
  35. Boolean flag indicating whether peak PNS is within acceptable limits
  36. pns_norm : numpy.array [N]
  37. PNS norm over all gradient channels, normalized to 1
  38. pns_components : numpy.array [Nx3]
  39. PNS levels per gradient channel
  40. t_pns : np.array [N]
  41. Time axis for the pns_norm and pns_components arrays
  42. """
  43. dt = obj.grad_raster_time
  44. # Get gradients as piecewise-polynomials
  45. gw_pp = obj.get_gradients(time_range=time_range)
  46. ng = len(gw_pp)
  47. max_t = max(g.x[-1] for g in gw_pp if g != None) - 1e-10
  48. # Determine sampling points
  49. if time_range == None:
  50. nt = int(np.ceil(max_t/dt))
  51. t = (np.arange(nt) + 0.5)*dt
  52. else:
  53. tmax = min(time_range[1], max_t) - max(time_range[0], 0)
  54. nt = int(np.ceil(tmax/dt))
  55. t = max(time_range[0], 0) + (np.arange(nt) + 0.5)*dt
  56. # Sample gradients
  57. gw = np.zeros((t.shape[0], ng))
  58. for i in range(ng):
  59. if gw_pp[i] != None:
  60. gw[:,i] = gw_pp[i](t)
  61. if do_plots:
  62. plt.figure()
  63. for i in range(ng):
  64. if gw_pp[i] != None:
  65. plt.plot(gw_pp[i].x[1:-1], gw_pp[i].c[1,:-1])
  66. plt.title('gradient wave form, in Hz/m')
  67. if type(hardware) == str:
  68. # this loads the parameters from the provided text file
  69. asc, _ = readasc(hardware)
  70. hardware = asc_to_hw(asc)
  71. # use the Szczepankiewicz' and Witzel's implementation
  72. [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 ...
  73. # use the exported RF vector to detect and undo zero-padding
  74. pns_comp = 0.01 * pns_comp[~np.isfinite(res.rf[1:]),:]
  75. # calc pns_norm and the final ok/not_ok
  76. pns_norm = np.sqrt((pns_comp**2).sum(axis=1))
  77. ok = all(pns_norm<1)
  78. # ready
  79. if do_plots:
  80. # plot results
  81. plt.figure()
  82. safe_plot(pns_comp*100, obj.grad_raster_time)
  83. return ok, pns_norm, pns_comp, t