Skip to content

'Remove Section' GUI implementation#54

Merged
aris-mav merged 29 commits intoaris-mav:masterfrom
louob20:master
Mar 19, 2025
Merged

'Remove Section' GUI implementation#54
aris-mav merged 29 commits intoaris-mav:masterfrom
louob20:master

Conversation

@louob20
Copy link
Contributor

@louob20 louob20 commented Mar 10, 2025

Appears to work, but please do check that this gives the desired results

I'm also unsure how to format the buttons in a nicer manner

Screenshot 2025-03-10 at 17 53 29 Screenshot 2025-03-10 at 17 53 48 Screenshot 2025-03-10 at 17 55 04

src/misc.jl Outdated
results.yfit = g

# pick indices from xfit that are closest to the measured x values
closest_indices = [findmin(abs.(results.xfit .- x))[2] for x in results.x]
Copy link
Owner

@aris-mav aris-mav Mar 10, 2025

Choose a reason for hiding this comment

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

This is a nice solution, but the results would not be 100% accurate, since the different x values might not match. We could instead use the original K matrix (as opposed to the one made right above) and compute Kf-g again for the residuals? That seems a bit more elegant and would give the most accurate residuals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added the original ker_struct from the 1D invert method into the results. Calculating the residuals (as in the finding_alpha method, using g-Kf with g and K from the kernel struct) with the unchanged f leaves the residuals unchanged. However, changing f does give seemingly incorrect results. It's likely I'm just misunderstanding how ker_struct is used.

Screenshot 2025-03-11 at 20 20 20 Screenshot 2025-03-11 at 20 20 57

(even more seemingly incorrect than the last)

The following 2 are the plots of Kf, using the old and new K.

Screenshot 2025-03-11 at 19 44 48 Screenshot 2025-03-11 at 19 45 47

Copy link
Owner

Choose a reason for hiding this comment

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

The values in the kernel structure sometimes are compressed by singular value decomposition to make the inversion faster and remove a bit of noise. I imagine that may introduce some confusion, since if the data is compressed, g will have a different size compared to x or y.
This is also why the x axis in the residuals uses "index" instead of time.

That's a bit confusing without looking at your code. Could you push it please?

@aris-mav
Copy link
Owner

Just tried the code on my machine, it works fine, well done!
Happy to merge once you consider the points above.

Also, I was thinking one more thing;
It might be good if we scale the updated distribution after the deletion, so that its integral is the same as the one in the original distribution.
Haven't tested it, but I imagine the fit will be much better after a section is deleted if we do that. Let me know whether you'd be keen to test that.

@louob20
Copy link
Contributor Author

louob20 commented Mar 12, 2025

That sounds like a good idea, I will work on this.

In the current code there is no feature to find the integral of f is there?

@aris-mav
Copy link
Owner

That sounds like a good idea, I will work on this.

In the current code there is no feature to find the integral of f is there?

Great.
You're right, trapezoidal intergals would require external packages.
However, we don't necessarily care about the actual integral here.
Something like the sum all the values of the vector would be just as good as the integral in this case. As long as this measure is preserved, it would serve the same purpose as preserving the integral (since the x-axis would be the same before and after).

Perhaps we could also use the filter field to encode this information.
E.g., filter is all 1's at the beginning, but once some values are turned into 0's, the remaining 1's can get scaled up accordingly so that sum(f) == sum( filter .* f).
Hope that makes sense.

Of course, let me know if this, or anything else is unclear.

@louob20
Copy link
Contributor Author

louob20 commented Mar 12, 2025

Got it, sounds good. Thanks!

@louob20
Copy link
Contributor Author

louob20 commented Mar 18, 2025

Hi @aris-mav, I think I've figured out the issue. I have used create_kernel(res.seq, res.x, res.X) * res.f - res.y to find the residuals in the end.

Plotting create_kernel(res.seq, res.x, res.X) * res.f gives points that line up perfectly on the fitted curve and align with the res.y values. When removing an empty section, the residuals plot does change, however these must be the correct values.

Looking closely at the first index, having left f unchanged:

Original residuals plot,
Screenshot 2025-03-18 at 12 42 36
Notice how the distance from the point to the fitted curve is ~0.005, but looking at the residual plot it predicts ~0.0001.

Updated residuals plot,
Screenshot 2025-03-18 at 12 43 46
Now the distance from the point to the fitted curve has stayed the same at ~0.005, but the residual plot appears to be much more accurate, reading ~0.0058.

Using the updated method also gives sensible results when removing sections, unlike the original method, where residuals are very large initially and then quickly go to zero.
Additionally, if we plot K*f using the previous and updated method, the updated method gives a scatter plot of points arranged exponentially on the fitted curve, whereas the previous method gives odd results:

Original, res.ker_struct.K * res.f
Screenshot 2025-03-18 at 13 13 42

Updated, create_kernel(res.seq, res.x, res.X) * res.f
Screenshot 2025-03-18 at 13 14 40

@aris-mav
Copy link
Owner

That looks good, @louob20!
I'll also update how the original residuals are calculated, for consistency.

Just one thing, as mentioned in a previous comment, it would be good if the delete_selection! function only updates the filter field of the result structure. Also, filter_range might be a more appropriate name for it in this case.
This way, we can skip copying the original structure.
All the information will be there, and the new residuals/ yfit can be computed just before each plot (for both interactive and non-interactive plots).

@aris-mav
Copy link
Owner

Would you please enable “Allow edits from maintainers"?
This way I'll be able to push some changes here as well.

@louob20
Copy link
Contributor Author

louob20 commented Mar 18, 2025

That looks good, @louob20! I'll also update how the original residuals are calculated, for consistency.

Just one thing, as mentioned in a previous comment, it would be good if the delete_selection! function only updates the filter field of the result structure. Also, filter_range might be a more appropriate name for it in this case. This way, we can skip copying the original structure. All the information will be there, and the new residuals/ yfit can be computed just before each plot (for both interactive and non-interactive plots).

So you have to apply the filter to the function only when you want to plot it?

@aris-mav
Copy link
Owner

So you have to apply the filter to the function only when you want to plot it?

That's what I was thinking, that would be consistent with the 2D case and it would preserve the original information.

@louob20
Copy link
Contributor Author

louob20 commented Mar 18, 2025

@aris-mav I believe that last commit was all, if you would like to run the code before merging

@aris-mav
Copy link
Owner

It seems that the filter selection function still updates the residuals and the yfit directly on the structure.
Am I missing a commit perhaps?

@louob20
Copy link
Contributor Author

louob20 commented Mar 18, 2025

I have changed it, should be good now.

@@ -132,6 +132,7 @@ Run the GUI to plot the 1D inversion results and select peaks you want to label.
"""
function Makie.plot(res::NMRInversions.inv_out_1D)

Copy link
Owner

Choose a reason for hiding this comment

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

Maybe the most elegant and consistent way to handle this would be following the same approach as the 2D case:
Add a filter field to the results struct, which can be a vector with 0's in the deleted region and 1's everywhere else.
Then, each time before plotting, we can plot filter .* f instead of f, K *( filter .* f ) .- yfit instead of r etc.

If so, the delete_range! function can be modified to delete_range(r::NMRInversions.inv_out_1D) (or another suitable name), and it can return the values above which need to be plotted, instead of modifying the original structure. This way, the original information is preserved, and the new information is computed in-place using the 'filter' field.

Can't think of a better way, but keen to hear your opinion as well.

Copy link
Owner

Choose a reason for hiding this comment

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

Basically the delete button would only modify the filter field, and the reset button would turn it back to 1's.

src/misc.jl Outdated
results.yfit = g

# pick indices from xfit that are closest to the measured x values
closest_indices = [findmin(abs.(results.xfit .- x))[2] for x in results.x]
Copy link
Owner

Choose a reason for hiding this comment

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

The values in the kernel structure sometimes are compressed by singular value decomposition to make the inversion faster and remove a bit of noise. I imagine that may introduce some confusion, since if the data is compressed, g will have a different size compared to x or y.
This is also why the x axis in the residuals uses "index" instead of time.

That's a bit confusing without looking at your code. Could you push it please?

@aris-mav aris-mav merged commit dbe46bb into aris-mav:master Mar 19, 2025
3 checks passed
@aris-mav
Copy link
Owner

Good job @louob20 !

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.

2 participants