-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix make_extended_trapezoid area #171
Conversation
Improve timing/area calculation by ignoring raster_time at first and taking care of it in final calculation. This avoids issues by the discontinuities causing negative flat_times. Co-authored-by: Felix Zimmermann <fzimmermann89@gmail.com>
Looks good in principle, works better than the original function in all cases I've tested. In the code as-is, there are edge-cases in the form of In some cases they give an assertion error on (very small) negative In other cases they result in a non-time-optimal solution, due to the rounding to raster times: Another set of edge cases is in the form of An even more rare set of edge cases exist where the taking Besides these edge-cases, an issue I see is that with these changes that remove the discontinuity from the optimization problems, all optimization problems now have closed-form solutions, making all the iterative optimization unnecessary, solutions below: The first set of optimization:
Based on https://www.wolframalpha.com/input?i=%28s-x%29%2Fr*+%28s%2Bx%29%2F2+%2B+%28z-x%29%2Fr*+%28x%2Bz%29%2F2+%3D+v+solve+for+x and https://www.wolframalpha.com/input?i=%28x-s%29%2Fr*+%28s%2Bx%29%2F2+%2B+%28x-z%29%2Fr*+%28x%2Bz%29%2F2+%3D+v+solve+for+x The second optimization:
And the final optimization:
These can be easily replaced into the code, obviously with some renaming of the variables I used to make wolfram alpha happy :). These may also solve some of the edge-cases, or make them easier to detect. |
Yeah, the simple ramp-up or ramp-down cases were and are not handled separately atm. We only differentiate between with or without flat time. Probably this is something we should do directly in the beginning. It should fix all cases mentioned above, no ?!
Rounding down the ramp-times should not be possible, should it? Maybe a recalculation of the flat_time due to larger ramping areas makes sense, but I don't think this will ever lead to deviations of more than one raster_time from the optimum.
Yeah, we were thinking about the closed-form solution as well, but catching all edge cases, especially when enforcing the raster, is also not super simple and clean. Thus, we decided to provide a simple fix for the current version and maybe implement an alternative version later on. Would be nice to consider nulling the 1st moment as well as for example in https://onlinelibrary.wiley.com/doi/10.1002/mrm.20489
For the moment, I would prefer to stick with the old style using iterative optimization and just change the other points you mentioned. I will adjust the code and push the changes soon. |
The calculated time point is the earliest possible time to achieve the area while satisfying the slope constraint. |
I don't think the solutions I provided introduce any new edge cases. In fact, I think it makes it easier to detect the edge cases (i.e. avoid divisions by 0).
All that needs to happen on the analytical solutions is a little cleanup as I just hacked them together :). But they do work fine! |
Little update on the analytical solutions: First optimization, simplified and added a check for the case where the gradient is triangular (still needs to be preceded with a check that the solution is not just a triangular gradient with a ramp time that fits raster_time):
Flat time optimization needed a second case:
In addition, I wrote a little pytest script for the function, with some of the things I tried, I suggest you add it to pypulseq/tests, and add test cases for things I have not covered, or if new things come up: |
Bigger update... Because I was curious about the impact of rounding up or down, I added a test function based on recreating an existing extended gradient. It finds many failure cases, some 1e-5 off on the timing, but some a few that are a multiple of the duration off. I'm not sure what exactly these cases represent, but that the come up in a test with randomly generated values is a bit worrying. |
@schuenke Just as a heads-up, I don't know how it will perform with the recent changes to Another issue was the rewinders were significantly longer than they need to be. At the end of the day, I started using GrOpt, and I am quite happy with the result. |
The goal of the rewrite was also to reduce this error.. Once the different time points are determined, the last step should find the optimal gradient amplitude given the (rounded to raster time) time points to achieve exactly the requested amplitude. It should also always find the shortest possible gradient up to (worst case) 3 gradient raster steps. |
@fzimmermann89 Correct me if I'm wrong, but it is the shortest trapezoid, not the shortest gradient, that nulls the M0. |
Maybe I am wrong, but I'd say it should be the shortest gradient that has the requested area. The only difference can be of the order of few (~3) gradient_raster steps .. Or where is my error? How would a shorter gradient look like that has the same area as a max-slew trapezoid? |
@fzimmermann89 You might be right, and it might be a problem with my implementation back then that I was not able to find the shortest gradient. I can not think of any case that there is a shorter gradient. |
Thanks for the feedback. I'll keep it in mind, but I would definitely prefer an elegant and efficient solution in pypulseq. At least for the "simple" 0-th moment nulling. Pretty optimistic we can get this done 💪 |
change order of grad_end and grad_start arg define default system as None define single _calc_ramp_time function catch different cases for flat_time > 0
Hmm, not sure about this.
IMO we only want to use a single triangular gradient (just ONE ramp) if Just to make clear we all have the same things in mind... we expect the following cases:
|
But I just realize there is still a problem with that case, because that does not necessarily mean Just to be clear, this is not a problem with just the analytical solution, the same thing happens when using optimization.
On case 1, I assume you mean (integer) N * raster_time. And if you check for case 1, then a situation with equal slopes in case 2 will never happen. And it shouldn't, because the optimization problem is ill-defined in that case, every solution And in case 4 and 5, I noticed issues with start or end set to To be clear, I'm not expecting every edge case to be solved per se, but at the very least we can mark where there are issues. Right now I'm most worried about my second set of tests (recreating existing gradients), giving more errors than with the original code. |
Ah, sorry I somehow thought there was an abs() around the Just to clarify: I really like the idea of having a closed-form solution and I am motivated to implement it. I just want to think of potential edge-cases before the implementation. Regarding the errors in the test: you also test for existing gradient without max_slew, don't you? There might be cases where using max_slew is not optimal. E.g. if area is slightly larger than the area of the simple single ramp, but using a slightly lower slew rate might do the job?! Or Is there always a solution with max_slew that should result in the same total time? I will think about this again and try to define some example cases 😄
|
I have also found myself strangely intrigued by this seemingly simple geometric problem ;). I am currently looking into the cases where the total duration is multiple times off the optimum. And no, I have not explicitly considered that not using |
In the closed-form solution of the third problem, this fixes any slew rate violations:
Even when only rounding up, |
…ch for solutions - Added tests for `make_extended_trapezoid_area`
@schuenke I pushed the fixed version. I went back to using a grid search to solve for the timing. It's not as efficient as it can be, but for normal system parameters it is fine. For low slew rate (relative to maximum gradient strength), it can be slow to find a solution (but it will). |
Nice work 👍 While reviewing the code, I added some type hints, documentation and introduced some temp vars to improve the readability. Feel free to revert changes you don't like. |
Looks good. I tightened the bound for the linear search a bit by only considering a ramp down/up to gradient strength 0. After this, the desired area can always be reached by extending the duration (so binary search works there). Also did some minor cleaning. I had a look at the dead space issue. Without considering the raster time it is possible to calculate the range where the area is unreachable with some ugly quadratic formula. But the bounds it find are not entirely accurate once the raster time is considered. I do think, that if this can be solved accurately, it gives only one or two possible solutions for the entire problem (i.e. find the points where the minimum or maximum area for a given duration is equal to the desired area). But for now I'll leave it as is, we can revisit it if the performance of the function turns out to be too poor for some situations. |
…ry reasonable timings (primarily those using maximum slew rate)
Made one final change that substantially narrows the timings that are checked by |
Improve timing/area calculation by ignoring raster_time at first and taking care of it in final calculation. This avoids issues by the discontinuities causing negative flat_times.
The issue accured for example when trying to use
make_extended_trapezoid_area
to calculate the rewinder gradients for spiral MRI sequences.