Skip to content

[v1.5.x] Add calc_rf_power() - #347

Merged
schuenke merged 16 commits into
pulseq:masterfrom
mcencini:feat_calc_rf_power
Oct 29, 2026
Merged

schuenke merged 16 commits into
pulseq:masterfrom
mcencini:feat_calc_rf_power

Conversation

@mcencini

Copy link
Copy Markdown
Collaborator

This PR adds standalone pypulseq.calc_rf_power() and the corresponding pypulseq.Sequence.calc_rf_power() method.

Compared to upstream MATLAB repository, this function by default directly uses explicitly the rf time vector to handle cases where pulse has not an uniform raster equal to rf raster (e.g., for block pulses). In addition, the Sequence method uses time_range instead of block_range to align with the other PyPulseq functions (e.g., plot().

This does not rely on any v1.5.1 features and can fit v1.5.0.

@mcencini mcencini mentioned this pull request Jan 22, 2026
3 of 8 tasks
@github-actions

github-actions Bot commented Jan 22, 2026 •

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
/home/runner/.local/lib/python3.12/site-packages/pypulseq
   add_gradients.py1372185%44, 52, 61, 85, 92, 125–128, 155, 162, 177, 187–190, 211–218, 225, 230, 261
   add_ramps.py36360%1–92
   align.py35489%41, 45, 69, 73
   calc_duration.py33294%43, 50
   calc_ramp.py2202162%48–359
   calc_rf_bandwidth.py372824%45–81, 85–89
   check_timing.py992872%78, 82, 184, 203, 236, 243, 253–297
   compress_shape.py30197%28
   convert.py40880%42, 48, 66, 72–73, 82, 88–89
   event_lib.py961584%6–9, 48–51, 70–71, 185, 205–210
   make_adc.py1081091%114, 163, 166, 170, 176, 180, 219, 221, 223, 231
   make_adiabatic_pulse.py1503974%214–218, 238–242, 250–251, 274, 280, 349–368, 472–481, 519–527
   make_arbitrary_grad.py62297%124, 145
   make_arbitrary_rf.py832471%145–149, 152–156, 166–185, 188, 191
   make_block_pulse.py53198%132
   make_delay.py9189%27
   make_extended_trapezoid.py64691%74, 77, 137, 154, 156, 159
   make_extended_trapezoid_area.py110496%71, 74, 273, 276
   make_gauss_pulse.py90594%193, 201–203, 218
   make_sigpy_pulse.py1292878%12–13, 134, 137, 186–190, 197–198, 201–202, 217, 224, 229, 267, 270, 295–305, 345, 348, 378–388
   make_sinc_pulse.py84495%196–198, 213
   make_soft_delay.py32197%136
   make_trapezoid.py128795%207, 221, 227, 245, 263, 268, 286
   opts.py66986%78, 83, 102, 142, 166–170
   points_to_waveform.py9189%27
   rotate.py69593%55, 69, 112, 119–120
   rotate_3d.py78297%145–146
   sigpy_pulse_opts.py28775%66–73
   split_gradient.py393121%46–103
   split_gradient_at.py702761%63–90, 110, 114, 118–120, 154–156
   traj_to_grad.py13931%26–40
/home/runner/.local/lib/python3.12/site-packages/pypulseq/SAR
   SAR_calc.py5180%9
/home/runner/.local/lib/python3.12/site-packages/pypulseq/Sequence
   block.py4525488%64, 67, 75, 81, 96, 104, 110, 121, 124, 127, 135, 140, 149, 160, 168, 208, 210, 214, 329, 333, 336, 357, 372, 382, 393, 402, 434, 438, 447, 460, 471, 480–484, 502, 534–541, 558, 568, 594, 632, 671, 685, 712, 791, 828, 852
   calc_grad_spectrum.py81766%68–190
   calc_pns.py403122%45–96
   ext_grad_check.py55296%51, 53
   ext_test_report.py1671193%79, 156, 167–168, 337–343
   install.py754244%31, 52, 69, 71, 112–131, 148, 181–184, 200–212, 254–278
   parula.py4250%19–86
   read_seq.py4084190%44–45, 110, 117, 130, 133–134, 138, 184, 223–224, 242, 415, 436–453, 516, 519, 604, 628, 668, 699, 715–719, 726, 837, 848
   sequence.py71113381%10–13, 108–118, 139–152, 206, 271–274, 321, 365, 441, 468–473, 671, 699–702, 793, 821–830, 842, 864, 905–908, 937, 963–969, 1004, 1015–1016, 1022, 1033, 1039, 1041, 1049, 1082–1090, 1222, 1320, 1326, 1329, 1332, 1371, 1496–1509, 1567, 1589–1591, 1612, 1675, 1683, 1781, 1792–1805, 1874–1875, 1886–1904, 1928, 1958–1966, 2002, 2016–2026
   write_seq.py362798%45, 77–79, 313, 347–349, 499
/home/runner/.local/lib/python3.12/site-packages/pypulseq/utils
   cumsum.py14193%17
   paper_plot.py63588%49–132
   safe_pns_prediction.py12611310%50–87, 102–189, 197–214, 222, 244–250, 279–286, 310–336, 344–383, 396–411, 415
   seq_plot.py40718654%22, 106, 138–139, 159, 176–178, 183–225, 235–242, 249–314

Warning: Comment truncated due to GitHub's 65536 character limit

@schuenke
schuenke self-requested a review March 27, 2026 10:28
@schuenke

Copy link
Copy Markdown
Collaborator

hey @mcencini

I started reviewing this PR as well and noticed a few bugs already that I will fix.

Furthermore, there are a few things to discuss:

  1. time_range vs. block_range: I like the idea, but IMO we should use the same API as in Matlab by default. Therefore, my suggestion would be to have a similar implementation as Matlab / PyPulseq-matlab-like, but in addition allow to specify time_range instead of block_range. Would this be fine for you?
  2. dt=None vs. dt=1e-6 vs. dt=system.rf_raster_time: Same as above, I like the idea of allowing dt=None, but would suggest to use the same default settings as in Matlab. However, hard-coding dt=1e-6 somehow feels wrong to me as well and I would suggest to adjust this in Matlab and Python to use system.rf_raster_time instead. Does this make sense to you?

I will probably start implementing it locally as suggested above, but it would be great to get your feedback on this as soon as possible.

@mcencini

Copy link
Copy Markdown
Collaborator Author

hi @schuenke, thanks for looking into this!

  1. I agree in general, but I suppose we need to collect similar discrepancies in the rest of codebase like @ckolbPTB also suggested. For instance, I think Sequence.plot() also accepts time_range only, and I think the historical reason was that in principle there is no constraint for blocks to be added in order, so we should be a bit careful.

  2. yes, it totally makes sense. I think I also initially thought to replace dt with system directly, but decided to keep dt for consistency with upstream MATLAB codebase. Perhaps it would make sense to also "fix" this in the MATLAB toolbox (@m-a-x-i-m-z)?

@schuenke

Copy link
Copy Markdown
Collaborator

.
1. I agree in general, but I suppose we need to collect similar discrepancies in the rest of codebase like @ckolbPTB also suggested. For instance, I think Sequence.plot() also accepts time_range only, and I think the historical reason was that in principle there is no constraint for blocks to be added in order, so we should be a bit careful.

Yeah, we should definitely do that for the existing code base, but for new implementations (like this one), I would prefer to adjust to Matlab already and, if possible, allow PyPulseq-like arguments (e.g. time_range) optionally. Long term the goal is to bring Matlab and Python closer together and if we introduce new functionality that deviates from Matlab, we have to adjust it at some point in the future leading to the same issues we currently have with seq.plot(), rotate(), make_trapezoid() etc...

@mcencini

Copy link
Copy Markdown
Collaborator Author

agreed - let's use block_range then :D

- replace np.trapezoid with manual calc for numpy<2.0 compatibility
- change dt default from None to 1e-6 to match MATLAB
- always use rf.shape_dur instead of  t[-1] - t[0], which was one dwell time short for arb rf pulses
@schuenke

Copy link
Copy Markdown
Collaborator

I replaced the np.trapezoid() call in 1b0c681 because this requires numpy>=2.0, which requires python>=3.10 ?!, which would be a major change... its still a one liner

- added block_range support to match Matlab implementation keeping time_range as alternative
- add comprehensive input validation for block_range and time_range
- fix bug leading to IndexError if a window had to slide past a non-RF block, e.g. RF block + delay, window < delay
- use self.block_duration instead of calc_duration(get_block(...)) for compatibility with soft delays
- support empty sequences
@schuenke

Copy link
Copy Markdown
Collaborator

This was also tested against MATLAB for various cases (single block/sinc/gauss pulses. different dt, full sequences, different block_ranges, different window_durations. Everything matching perfectly.

A comparison against MATLAB using different time_range values is not possible, because this is currently a Python-only feature.

@mcencini

Copy link
Copy Markdown
Collaborator Author

I replaced the np.trapezoid() call in 1b0c681 because this requires numpy>=2.0, which requires python>=3.10 ?!, which would be a major change... its still a one liner

That's ok - IMO at some point we should let end-of-life python versions and numpy 1.x go, but that probably deserve some long-term planning

@schuenke schuenke left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcencini: Same as for #341 , it would be great if you would review my changes again.

@sairamgeethanath @btasdelen @ckolbPTB @fzimmermann89 maybe one of you also wanna have a quick look at this? I tried to provide helpful infos in the comments.

@mcencini

Copy link
Copy Markdown
Collaborator Author

@schuenke it is ok to be merged to me, unless someone else wants to review it again as well.

@schuenke
schuenke merged commit e92b29d into pulseq:master Oct 29, 2026
4 checks passed
@m-a-x-i-m-z

Copy link
Copy Markdown

Hi!

concerning one of the comments above:

yes, it totally makes sense. I think I also initially thought to replace dt with system directly, but decided to keep dt for consistency with upstream MATLAB codebase. Perhaps it would make sense to also "fix" this in the MATLAB toolbox (@m-a-x-i-m-z)?

The challenge in the Matlab's code that the function mr.calcRfPower() belongs to the static object mr, which has no data properties and therefore has no access to system.rfRasterTime.

The only reasonable fix in this direction is to make sure that the function Sequence.calcRfPower() explicitly passes its system.rfRasterTime to each call off mr.calcRfPower().

Should we do it this way?

--M.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants