Skip to content

Fix:missing-t_ktraj-return-from-calculate_kspace - #282

Open
Nikbert wants to merge 13 commits into
pulseq:masterfrom
Nikbert:fix-missing-t_ktraj-return-from-calculate_kspace
Open

Fix:missing-t_ktraj-return-from-calculate_kspace#282
Nikbert wants to merge 13 commits into
pulseq:masterfrom
Nikbert:fix-missing-t_ktraj-return-from-calculate_kspace

Conversation

@Nikbert

@Nikbert Nikbert commented May 14, 2025

Copy link
Copy Markdown

This adds t_ktraj to the return values of calculate_kspace, to make it consistent with the Matlab Pulseq Version.
t_ktraj is very handy when comparing measured and nominal trajectories.

All the Best

@github-actions

github-actions Bot commented May 30, 2025

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
/home/runner/.local/lib/python3.12/site-packages/pypulseq
   add_gradients.py1376056%44, 52, 58, 61, 75–86, 92, 125–128, 135–136, 155, 162, 167–263
   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.py962970%78, 82, 107, 180, 199, 232, 239, 249–293
   compress_shape.py30197%28
   convert.py40880%42, 48, 66, 72–73, 82, 88–89
   event_lib.py961485%6–9, 48–51, 70–71, 205–210
   make_adc.py981486%77, 80, 90–94, 97, 146, 149, 153, 159, 163, 202, 204, 206, 214
   make_adiabatic_pulse.py1323970%208–212, 232–236, 244–245, 268, 274, 343–362, 466–475, 513–521
   make_arbitrary_grad.py531572%71, 74, 77, 80, 96–98, 107, 109, 117–121, 130
   make_arbitrary_rf.py756316%95–179
   make_block_pulse.py48394%121–125, 128
   make_delay.py9189%27
   make_digital_output_pulse.py16288%42, 50
   make_extended_trapezoid.py561279%67, 70, 76, 82, 85, 88, 91, 94, 116, 134, 136, 139
   make_extended_trapezoid_area.py104496%68, 71, 261, 264
   make_gauss_pulse.py732073%142–146, 149–173, 180, 183
   make_label.py22482%64, 66, 68, 75
   make_sigpy_pulse.py1193075%12–13, 122, 125, 129, 166–170, 174, 177–178, 181–182, 197, 204, 209, 221, 224, 249–259, 273, 276, 306–316
   make_sinc_pulse.py701086%104, 110, 138–142, 146, 149–150, 153–154, 176
   make_soft_delay.py26292%107, 125
   make_trapezoid.py111794%177, 190, 196, 214, 232, 237, 255
   make_trigger.py16288%47, 55
   opts.py66986%78, 83, 102, 142, 166–170
   points_to_waveform.py9189%27
   rotate.py691480%15, 55, 66–69, 85–90, 112, 119–120
   scale_grad.py30197%65
   sigpy_pulse_opts.py26773%34–41
   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.py4697983%63, 66, 74, 80, 95, 103, 109, 120, 123, 126, 134, 139, 148, 159, 167, 207, 209, 213, 225, 274, 278, 294, 319–345, 382–385, 421–429, 436, 466–470, 512, 518, 551, 587–594, 611, 621, 647, 685, 703, 706, 724, 738, 765, 844, 881, 905
   calc_grad_spectrum.py81766%68–190
   calc_pns.py403122%45–96
   calculate_kspace.py89397%55, 136, 201
   ext_test_report.py1691193%81, 158, 169–170, 339–345
   install.py754244%31, 52, 69, 71, 112–131, 148, 181–184, 200–212, 254–278
   parula.py4250%19–86
   read_seq.py4003990%44–45, 110, 117, 130, 133–134, 138, 184, 228, 401, 422–439, 502, 505, 590, 614, 654, 685, 701–705, 712, 823, 834
   sequence.py56212378%10–13, 106–116, 137–150, 204, 269–272, 385, 413–416, 507, 535–544, 556, 578, 619–622, 676, 714, 725–726, 732, 743, 749, 751, 759, 792–800, 932, 1030, 1036, 1039, 1042, 1079, 1204–1217, 1275, 1297–1299, 1320, 1383, 1391, 1489, 1500–1513, 1582–1583, 1594–1612, 1636, 1666–1674, 1710, 1724–1734
   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, 318–324, 328–336, 359, 361, 363, 390–412, 457–458, 461–464, 487–496, 508, 525–535, 544–546, 565–567, 569–570, 572–573, 657–658, 674–691, 739–743
   tracing.py16662%33–34, 42, 54–55, 75
/home/runner/.local/lib/python3.12/site-packages/pypulseq/utils/siemens
   asc_to_hw.py58539%21–28, 48–106
   readasc.py59788%103–104, 110–111, 122–124
TOTAL5215156370% 

Tests Skipped Failures Errors Time
1291 24 💤 0 ❌ 0 🔥 5m 39s ⏱️

@schuenke

Copy link
Copy Markdown
Collaborator

As far as I can see, the Matlab example sequences usually use the calculateKspacePP function, which returns even more parameters like t_adc and slicepos: https://github.com/pulseq/pulseq/blob/5ccd39c4170e48eec8542162ce7b4ec0839d91c1/matlab/%2Bmr/%40Sequence/Sequence.m#L2013

There is also (an old?) calculateKspace function, which, similar to the Python method, returns just 5 values:
https://github.com/pulseq/pulseq/blob/5ccd39c4170e48eec8542162ce7b4ec0839d91c1/matlab/%2Bmr/%40Sequence/Sequence.m#L1344

So in my opinion we should keep some consistency and either leave the Python function as it is or adjust it to match the Matlab calculateKspacePP function

@FrankZijlstra

Copy link
Copy Markdown
Collaborator

I ran into this when I reimplemented calculate_kspace at the time. I agree that it is a useful return value, but any external script that uses calculate_kspace will break as a result.

One non-breaking option is to add a return_t_ktraj parameter, default to False, and give a deprecation warning when it is said to False (i.e. in some next release the old-style return values will be removed). When it is True, return the new return values as in the PR.

@Nikbert

Nikbert commented Jun 16, 2025

Copy link
Copy Markdown
Author

I think your suggestions makes sense.
So, are we going to adapt the strange naming "calculateKspacePP" for a new function that has all the return values, mirroring the way it is done in pulseq?
(I talked to some colleagues here in Freiburg, everyone is using calculateKspacePP.)
Or should we do it as Franks suggested? I think in pulseq calculateKspacePP is planed to replace calculateKspace in the future as well. I somewhat lean to implementing "calculateKspacePP" as it makes it easier for someone who uses both implementations. However, it is an ugly solution as it leaves use with two very similar functions...
Any comments?

@FrankZijlstra

Copy link
Copy Markdown
Collaborator

calculateKspacePP refers to the piecewise-polynomial implementation for k-space calculation. We already moved this to calculate_kspace, because it is the only implementation that should be used. calculate_kspacePP exists in pypulseq merely for backwards compatibility (it will give a deprecation warning when used), but should be removed at some point. So no, this function should not be touched.

@Nikbert

Nikbert commented Jun 17, 2025

Copy link
Copy Markdown
Author

OK, then we can not mirror Pulseq calculateKspacePP. I think Franks suggestion sounds like a good solution. I think everyone that wants to match k-space data from a Skope fieldcam to the nominal trajectory would highly appreciate this change. I do not know how to alter this pull request accordingly. Frank can you help me?

@btasdelen

Copy link
Copy Markdown
Collaborator

If we are going to introduce a breaking change with an option to roll back via a return_XYZ parameter, would it make sense to return a dict instead? This function is returning 5 values (6 with the new addition), not really clean.

@FrankZijlstra

Copy link
Copy Markdown
Collaborator

I don't think it makes sense to have the rollback option not set as a default, because then it would still break old code unless the new parameter is set. With it set to default we can give a deprecation warning.

If we break things, I agree that returning a dict is nicer. Though a SimpleNamespace or result class (e.g. as sometimes used in scipy and numpy) is probably ever nicer. @schuenke probably can tell what is preferred if we do go that way.

To be clear, I'm not opposed to a breaking change, because even when we deprecate the current result values, at some point the breaking change needs to happen anyway. But this is something that would need to be documented. And a user should not get a "too many values to unpack" error without any indication why.

lukeje added a commit to lukeje/pypulseq that referenced this pull request Apr 10, 2026
…t without breaking backwards compatibility (see pulseq#282).
@lukeje

lukeje commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

I also found I needed this parameter and so rolled up a quick solution in line with what Bilal suggested here which seems to pass the standard tests. Is it worth me making a new pull request or merging it with this one somehow?

@schuenke

Copy link
Copy Markdown
Collaborator

I will have a look at this again. Probably tuesday next week.

@mzaiss

mzaiss commented May 16, 2026

Copy link
Copy Markdown
Contributor

we see some other k-space inconsistencies, but it would be better if this PR is solved in 1.4 and 1.5 so we can rule some things out. so thumbs up for a solution to this, we are also happy to help here.

@schuenke

Copy link
Copy Markdown
Collaborator

okay, I agree it makes sense to keep the default behaviour as it is so that we dont break existing code. IMO, there are two options now:

  1. introduce a return_t_ktraj: bool = False flag and return 6 instead of 5 values if it's set to True
  2. introduce a return_all_values_as_dict: bool = False flag and return a dict with 6 (and potentially even more) entries.

What's your preference @FrankZijlstra @mzaiss @lukeje @btasdelen @Nikbert

Ofc the variable names are up for discussion as well

@lukeje

lukeje commented May 18, 2026

Copy link
Copy Markdown
Contributor
  1. introduce a return_all_values_as_dict: bool = False flag and return a dict with 6 (and potentially even more) entries.

I would prefer this option (as implemented in my branch here: https://github.com/lukeje/pypulseq/tree/calculate_kspace_dict but with output_dict as the variable).

@mzaiss

mzaiss commented May 20, 2026

Copy link
Copy Markdown
Contributor

2

@schuenke schuenke self-assigned this May 21, 2026
@schuenke schuenke added the enhancement New feature or request label May 21, 2026
@schuenke

Copy link
Copy Markdown
Collaborator

Okay, I implemented option 2 in c8cb776

As always when touching some files, I also did some additional refactoring. The most important things:

  • define some internal helper functions (b52c5cd)
  • change many variable names to be more descriptive (b52c5cd)
  • move the whole calculate_kspace code into a separate file (similar to test_report etc) (5f4ee89)
  • resolve merge conflics (a9cbc70)
  • add some tests (currently only for the new feature) (df6f783)

It should be easy to track the changes if you go through them step by step.

It would be great if @Nikbert and maybe one more maintainer could review the changes. I prefer to avoid approving my own implementations / changes.

@lukeje

lukeje commented May 25, 2026

Copy link
Copy Markdown
Contributor

This updated branch works for me. Though I noticed that calculate_kspace is also used in tests/test_sequence.py, e.g. in line 484. If the plan is to deprecate the old form, it might be good to update these calls too.

@schuenke

Copy link
Copy Markdown
Collaborator

Thanks for your feedback. I will have a look at the existing function calls and might update them accordingly.

@Nikbert

Nikbert commented Jun 4, 2026

Copy link
Copy Markdown
Author

Looks good to me! Thanks for following up on this issue.

@schuenke

Copy link
Copy Markdown
Collaborator

Since its been a while and I wasn't sure myself if everything was ported correctly, I let Claude do another review that might help whoever wants to review this PR:

Here you go:


PR #282 Review: calculate_kspace refactoring + t_ktraj return

I did a line-by-line comparison of the old inline method (master sequence.py lines 283–458) against the new standalone calculate_kspace.py. The code transfer is clean — no logic was lost. All 195 relevant tests pass.

Changes overview (5 files, +366 / −151)

File Change
Sequence/calculate_kspace.py New file — extracted code with helper functions, renamed variables, added output_as_dict
Sequence/sequence.py Thin wrapper delegating to ext_calculate_kspace
Sequence/ext_test_report.py Updated to use output_as_dict=True
tests/test_calculate_kspace.py New file — 3 tests for legacy, dict, and deprecated path
tests/test_sequence.py 3 call sites updated to use output_as_dict=True

Line-by-line comparison: nothing lost

All logic was preserved with only variable renames for clarity:

Old (master) New (PR)
gw_pp grad_waveforms_pp
gm_pp grad_moments_pp
ng n_grad_channels
tc time_candidates
ii ramp_idx / idx_in_support
dk k_offset
ii_next_excitation next_excitation_idx
ii_next_refocusing next_refocusing_idx
self.* seq.* (standalone function)

The three helper functions (_build_time_axis, _sample_gradient_moments, _apply_excitation_and_refocusing_resets) are clean extractions of the original inline code.

Improvements found during transfer

  1. Bugfix in warning message — The old code had error={t_ktraj(i_period) - t_excitation(ii_next_excitation)} using () (function call syntax) instead of [] (indexing). This would have crashed with a TypeError if the warning was ever triggered. The new code correctly uses [].

  2. Removed dead commented-out code# dk = -k_traj[:, i_period] was cleaned up.

  3. Robustified empty-list handling — Old if tc != []: replaced with if len(time_candidates) > 0: plus explicit else: time_candidates = np.zeros(0). Functionally equivalent but cleaner.

Things worth noting (not bugs, just observations)

  1. slice_pos is computed but never returned — This exists in both old and new code. The MATLAB calculateKspacePP does return slicepos. Since output_as_dict was introduced to make the return value extensible, it would be natural to add slice_pos to the dict in a follow-up. Not a regression — this was pre-existing.

  2. calculate_kspacePP correctly forwards output_as_dict — The deprecated wrapper properly passes through the new parameter. ✓

  3. ext_test_report.py migration is correct — Old unpacking k_traj_adc, _, t_excitation, _, t_adc = self.calculate_kspace() replaced with dict access. ✓

  4. test_sequence.py comparisons with Approx — Switched from tuple to dict comparison. Works because pytest.approx supports dicts natively. ✓

Verdict

Safe to merge. The refactoring faithfully preserves all original logic, the new output_as_dict feature is backwards-compatible (defaults to False), and the transfer actually fixed a latent bug in the warning message. The only optional follow-up would be including slice_pos in the dict output to eventually match MATLAB's calculateKspacePP.

@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.

Since I did a lot of refactoring myself, I would suggest that somebody else reviews this PR as well.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants