pypulseq icon indicating copy to clipboard operation
pypulseq copied to clipboard

Implemented rotate3D. Fix for rotate.

Open BenWilhelmUniklinikFreiburg opened this issue 9 months ago • 3 comments

Implemented rotate3D. Changed rotate to have the correct rotation direction when rotating around the y-axis. The direction for rotating around the x- and z-axis is not changed.

Coverage

Coverage Report
FileStmtsMissCoverMissing
/home/runner/.local/lib/python3.12/site-packages/pypulseq
   add_gradients.py1232480%44, 52, 61, 85, 92, 120–123, 130–131, 150, 157, 175–206, 211, 239
   add_ramps.py36360%1–89
   align.py35489%41, 45, 69, 73
   calc_duration.py25196%37
   calc_ramp.py2182142%45–353
   calc_rf_bandwidth.py272026%37–59, 63–67
   check_timing.py872769%72, 76, 101, 194, 201, 211–255
   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.py921386%63, 72–76, 79, 128, 131, 135, 141, 145, 184, 186, 188, 196
   make_adiabatic_pulse.py1293970%196–200, 217–221, 229–230, 253, 259, 328–347, 451–460, 498–506
   make_arbitrary_grad.py37781%68, 71, 74, 77, 81, 83, 103
   make_arbitrary_rf.py665517%83–160
   make_block_pulse.py46393%112–116, 119
   make_delay.py9189%27
   make_digital_output_pulse.py16288%39, 47
   make_extended_trapezoid.py561279%67, 70, 76, 82, 85, 88, 91, 94, 116, 134, 136, 139
   make_extended_trapezoid_area.py93397%52, 227, 230
   make_gauss_pulse.py692071%127–131, 134–158, 165, 168
   make_label.py19384%36, 41, 43
   make_sigpy_pulse.py1163173%12–13, 112, 115, 119, 154, 157–161, 165, 168–169, 172–173, 188, 195, 200, 212, 215, 240–250, 264, 267, 297–307
   make_sinc_pulse.py681085%94, 100, 127–131, 135, 138–139, 142–143, 165
   make_trapezoid.py111794%177, 190, 196, 214, 232, 237, 255
   make_trigger.py16288%44, 52
   opts.py66986%78, 83, 102, 142, 166–170
   points_to_waveform.py9189%27
   rotate3D.py54689%44, 54, 57, 59, 93–94
   rotate.py76593%59, 83, 126, 133–134
   scale_grad.py14193%33
   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.py1139813%33–40, 55–62, 89–108, 129–132, 168–212, 242–246, 264–306
/home/runner/.local/lib/python3.12/site-packages/pypulseq/Sequence
   block.py3683291%59, 62, 70, 76, 91, 99, 105, 116, 119, 122, 130, 135, 144, 196, 245, 249, 265, 305–308, 337–338, 404, 410, 430, 499, 535, 541, 568, 606, 640
   calc_grad_spectrum.py81766%68–190
   calc_pns.py403122%45–96
   ext_test_report.py1401192%23, 58, 61, 135, 227–233
   install.py754244%31, 52, 69, 71, 112–131, 148, 181–184, 200–212, 254–278
   parula.py4250%19–86
   read_seq.py3126878%42–43, 90, 93, 105, 110, 116, 123, 132, 141, 146, 149, 157–159, 192, 197, 205–254, 284–287, 302–303, 332–349, 412, 415, 450, 458, 532, 574–578
   sequence.py73722470%11–14, 101–111, 132–145, 183, 248–251, 298, 325, 342, 390, 418, 445–450, 487, 503, 594, 616, 657–660, 714, 752, 763–764, 770, 781, 787, 789, 797, 830–838, 859–881, 924, 926, 929, 955–956, 959–962, 998–1008, 1017–1019, 1063–1075, 1090–1091, 1127–1128, 1154, 1160, 1163, 1200, 1321–1334, 1357, 1385, 1407–1409, 1430, 1461, 1472–1485, 1497–1508, 1554–1555, 1564–1582, 1606, 1636–1644, 1676–1786, 1815, 1829–1839, 1851
   write_seq.py172895%41, 65, 68–75
/home/runner/.local/lib/python3.12/site-packages/pypulseq/utils
   cumsum.py14193%17
   safe_pns_prediction.py12611310%50–87, 102–189, 197–214, 222, 244–250, 279–286, 310–336, 344–383, 396–411, 415
   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.py48456%25–100
TOTAL4434146367% 

Tests Skipped Failures Errors Time
1324 18 :zzz: 6 :x: 0 :fire: 3m 6s :stopwatch:

github-actions[bot] avatar Feb 13 '25 06:02 github-actions[bot]

@BenWilhelmUniklinikFreiburg All source code needs to be in src/pypulseq. So you have to move the rotate3D.py file in order for the code to work and the tests to run.

schuenke avatar Feb 13 '25 06:02 schuenke

@schuenke sorry, not sure how that happened. The files should be in the correct place now.

ghost avatar Feb 13 '25 09:02 ghost

@BenWilhelmUniklinikFreiburg I had a look at the tests and realized that you were only checking for val_a - val_b > tol without using abs() first, which means that the condition was never true for val_a beeing smaller than val_b. When fixing it, I realized that the tests failed.

For easier debugging, I introduced pytest.mark.parametrize and splitted the one test function with 3 assert statements into 3 test functions with 1 assert statement, each. Now it is obvious, that only the case where you provide the doubled rotation matrix fails.

Maybe you can check yourself what causes the problem. At least in one case the grad.last attribute was the problem.

schuenke avatar Jun 02 '25 10:06 schuenke

Ah, and it's NOT failing for all cases. All multiples of pi/2 work fine 😄

schuenke avatar Jun 02 '25 10:06 schuenke

Just a note: the Approx class in test_sequence.py should be able to handle checking the equality of arbitrary objects, as an extension of pytest.approx. So it can be used for checking the equality of gradient objects with tolerances. No need to write a custom compare_gradients function for each test script. Previously the Approx class was in a separate utils file in the tests folder, where I think it should be.

E.g. checking the equality of blocks works like this: https://github.com/imr-framework/pypulseq/blob/1c623fe0ff7ad4908ccb6c634ae9c9ae33e0630d/tests/test_sequence.py#L319

FrankZijlstra avatar Jun 05 '25 07:06 FrankZijlstra

It seems to me that tests fails for pypulseq.make_extended_trapezoid('y', [0, 5, 1, 3], convert_to_arbitrary=True, times=[1, 3, 4, 7]) when angles are 0.1, 1.0, 60.0, 400.1, -0.1 and -1.0 and rotation axis is x. It seems that rotation via rotate3D by the composite rotation_matrix @ rotation_matrix generate waveforms along y and z with the same area as the original y waveform. Maybe there is some rounding issues of the scaling factors given by the composite matrix which do not arise when applying two separate rotations - I am trying to investigate and will open a PR to @BenWilhelmUniklinikFreiburg's fork if I manage to fix this.

mcencini avatar Jun 24 '25 14:06 mcencini

Ok, scaling the area together with the other parameters in scale_grad fixes the inconsistency:

scaled_grad = copy(grad)
if scaled_grad.type == 'trap':
    scaled_grad.amplitude = scaled_grad.amplitude * scale
    scaled_grad.flat_area = scaled_grad.flat_area * scale
else:
    scaled_grad.waveform = scaled_grad.waveform * scale
    scaled_grad.first = scaled_grad.first * scale
    scaled_grad.last = scaled_grad.last * scale
scaled_grad.area = scaled_grad.area * scale

I think this is correct, isn't it? After all, it seems that area is a parameter for arbitrary grad events - in other functions such as add_gradients, area is updated as well.

mcencini avatar Jun 24 '25 15:06 mcencini

I opened a PR to @BenWilhelmUniklinikFreiburg fork with my changes - if merged, it should update this PR as well.

It also refactors tests so that it uses existing Approx, now moved to conftest.py to be re-used in different tests, as suggested by @FrankZijlstra.

As Approx was quite slow for test_rotation3D_vs_rotation, I refactored to speed it up. I later noticed that maybe test_rotation3D_vs_rotation generates unecessary long arbitrary waveforms (60000 points) - perhaps that is the reason for slow test runtime? If you like the older Approx version better, we can try reduce waveforms length and see if this is enough to achieve reasonable test runtime.

mcencini avatar Jun 24 '25 15:06 mcencini