Repository: aris-mav/NMRInversions.jl
Pull request #54: 'Remove Section' GUI implementation
Author: login: louob20
Created: 2025-03-10T18:00:37Z | Merged: 2025-03-19T10:35:27Z

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

<img width="700" alt="Screenshot 2025-03-10 at 17 53 29" src="https://github.com/user-attachments/assets/17d60ef3-f03b-4d35-a962-fc6e80abee86" />
<img width="697" alt="Screenshot 2025-03-10 at 17 53 48" src="https://github.com/user-attachments/assets/378919bb-dccf-4fbd-ba55-6c8bd0d4c02f" />
<img width="693" alt="Screenshot 2025-03-10 at 17 55 04" src="https://github.com/user-attachments/assets/c48cb906-91e0-4be3-9b65-581da22a33f1" />



Diff:
diff --git a/example_data/csv_IR/Chesire_sandstone_IR.csv b/example_data/csv_files/Chesire_sandstone_IR.csv
similarity index 100%
rename from example_data/csv_IR/Chesire_sandstone_IR.csv
rename to example_data/csv_files/Chesire_sandstone_IR.csv
diff --git a/example_data/csv_files/graphene_CPMG.csv b/example_data/csv_files/graphene_CPMG.csv
new file mode 100644
index 0000000..72c287c
--- /dev/null
+++ b/example_data/csv_files/graphene_CPMG.csv
@@ -0,0 +1,32 @@
+0.0007,0.346025
+0.0014,0.220525
+0.0021,0.147435
+0.0028,0.111799
+0.0035,0.0880444
+0.0042,0.0739969
+0.0049,0.0549486
+0.0056,0.0584712
+0.0063,0.0473388
+0.007,0.0434845
+0.0077,0.0281661
+0.0084,0.0363114
+0.0091,0.0402291
+0.0098,0.0319523
+0.0105,0.0415405
+0.0112,0.0320954
+0.0119,0.0252967
+0.0126,0.0283395
+0.0133,0.0177866
+0.014,0.0195451
+0.0147,0.0153591
+0.0154,0.0215488
+0.0161,0.0234149
+0.0168,0.0254711
+0.0175,0.0252673
+0.0182,0.0198021
+0.0189,0.0196933
+0.0196,0.0243988
+0.0203,0.0293147
+0.021,0.0200053
+0.0217,0.0167435
+0.0224,0.0238447
diff --git a/example_data/csv_IR/info.md b/example_data/csv_files/info.md
similarity index 63%
rename from example_data/csv_IR/info.md
rename to example_data/csv_files/info.md
index a4400ce..d787419 100644
--- a/example_data/csv_IR/info.md
+++ b/example_data/csv_files/info.md
@@ -1,4 +1,4 @@
-These are some IR (inversion recovery) data points corresponding to Cheshire sandstone, for testing the invert function.
+These are some IR (inversion recovery) data points corresponding to Cheshire sandstone, and some PFG (pulsed field gradient) data corresponding to water, for testing the invert function.
 
 You can use them as
 
diff --git a/example_data/csv_files/water_PFG.csv b/example_data/csv_files/water_PFG.csv
new file mode 100644
index 0000000..2539622
--- /dev/null
+++ b/example_data/csv_files/water_PFG.csv
@@ -0,0 +1,16 @@
+0.0168598,8.93818
+0.101159,6.9632
+0.185458,5.40806
+0.269757,4.23223
+0.354056,3.30936
+0.438355,2.59608
+0.522654,2.02982
+0.606953,1.59765
+0.691252,1.25206
+0.775551,0.986033
+0.85985,0.777756
+0.944149,0.610387
+1.02845,0.4785
+1.11275,0.37976
+1.19705,0.299643
+1.28135,0.235117
diff --git a/ext/gui_ext.jl b/ext/gui_ext.jl
index 352bbc7..3844a52 100644
--- a/ext/gui_ext.jl
+++ b/ext/gui_ext.jl
@@ -102,20 +102,20 @@ function Makie.plot(res_mat::AbstractVecOrMat{NMRInversions.inv_out_1D}; selecti
     if res.seq in [NMRInversions.IR]
         ax1 = Axis(fig[1:5, 1:5], xlabel="time (s)", ylabel="Signal (a.u.)")
         ax2 = Axis(fig[1:5, 6:10], xlabel="T₁ (s)", xscale=log10)
-        ax3 = Axis(fig[6:10,1:5], xlabel= "index", ylabel="Residuals (a.u.)")
+        ax3 = Axis(fig[6:10,1:5], xlabel= "time (s)", ylabel="Residuals (a.u.)")
 
     elseif res.seq in [NMRInversions.CPMG]
         ax1 = Axis(fig[1:5, 1:5], xlabel="time (s)", ylabel="Signal (a.u.)")
         ax2 = Axis(fig[1:5, 6:10], xlabel="T₂ (s)", xscale=log10)
-        ax3 = Axis(fig[6:10,1:5], xlabel= "index", ylabel="Residuals (a.u.)")
+        ax3 = Axis(fig[6:10,1:5], xlabel= "time (s)", ylabel="Residuals (a.u.)")
 
     elseif res.seq in [NMRInversions.PFG]
         ax1 = Axis(fig[1:5, 1:5], xlabel="b factor (s/m² e-9)", ylabel="Signal (a.u.)")
         ax2 = Axis(fig[1:5, 6:10], xlabel="D (m²/s)", xscale=log10)
-        ax3 = Axis(fig[6:10,1:5], xlabel= "index", ylabel="Residuals (a.u.)")
+        ax3 = Axis(fig[6:10,1:5], xlabel= "b factor (s/m² e-9)", ylabel="Residuals (a.u.)")
     end
 
-    #=linkxaxes!(ax1, ax3)=#
+    linkxaxes!(ax1, ax3)
 
     for r in res_mat
         draw_on_axes(ax1, ax2, ax3, r, selections = selections)
@@ -157,9 +157,10 @@ function Makie.plot(res::NMRInversions.inv_out_1D)
     vlines!(fig.content[2], int_low, color=:red)
     vlines!(fig.content[2], int_high, color=:red)
 
-    button_label = Button(fig[8,6:10], label = "Save selection")
-    button_reset = Button(fig[9,6:10], label = "Reset selections")
-    button_save = Button(fig[10,6:10], label = "Save and exit")
+    button_label = Button(fig[8,6:10], label = "Label current selection")
+    button_filter = Button(fig[9,6:10], label = "Filter-out current selection")
+    button_reset = Button(fig[10,6:8], label = "Reset selections")
+    button_save = Button(fig[10,8:10], label = "Save and exit")
 
     on(button_label.clicks) do _
         push!(res.selections, slider.interval[])
@@ -171,8 +172,19 @@ function Makie.plot(res::NMRInversions.inv_out_1D)
         vlines!(fig.content[2], int_high, color=:red)
     end
 
+    on(button_filter.clicks) do _
+        empty!(fig.content[1])
+        empty!(fig.content[2])
+        empty!(fig.content[3])
+        filter_range!(res, slider.interval[])
+        draw_on_axes(fig.content[1], fig.content[2], fig.content[3], res, selections = true)
+        vlines!(fig.content[2], int_low, color=:red)
+        vlines!(fig.content[2], int_high, color=:red)
+    end
+
     on(button_reset.clicks) do _
         empty!(res.selections)
+        res.filter = ones(length(res.X))
         empty!(fig.content[1])
         empty!(fig.content[2])
         empty!(fig.content[3])
@@ -187,18 +199,26 @@ function Makie.plot(res::NMRInversions.inv_out_1D)
         save(savedir, f)
     end
 
+
     return fig
 end
 
 
 function draw_on_axes(ax1, ax2, ax3, res::NMRInversions.inv_out_1D; selections = false)
 
-    c = length(ax2.scene.plots)
+    # apply filter to f and update fitted curve and residuals
+    f_prime = res.filter .* res.f
+    yfit_prime = create_kernel(
+        res.seq, res.xfit, (res.seq == PFG ? res.X .* 1e9 : res.X)) * f_prime
+    r_prime = create_kernel(
+        res.seq, res.x, (res.seq == PFG ? res.X .* 1e9 : res.X)) * f_prime - res.y
 
+    c = length(ax2.scene.plots)
     scatter!(ax1, res.x, real.(res.y), colormap=:tab10, colorrange=(1, 10), color=c)
-    lines!(ax1, res.xfit, res.yfit, colormap=:tab10, colorrange=(1, 10), color=c)
-    lines!(ax2, res.X, res.f, colormap=:tab10, colorrange=(1, 10), color=c)
-    lines!(ax3, res.r, colormap=:tab10, colorrange=(1, 10), color=c)
+    lines!(ax1, res.xfit, yfit_prime, colormap=:tab10, colorrange=(1, 10), color=c)
+    lines!(ax2, res.X, f_prime, colormap=:tab10, colorrange=(1, 10), color=c)
+    lines!(ax3, res.x, r_prime, colormap=:tab10, colorrange=(1, 10), color=c)
+    scatter!(ax3, res.x, r_prime, colormap=:tab10, colorrange=(1, 10), color=c)
 
 
     if selections
@@ -230,11 +250,11 @@ function draw_on_axes(ax1, ax2, ax3, res::NMRInversions.inv_out_1D; selections =
                 ax2, 
                 res.X[s[1]:s[2]],
                 zeros(length(res.f[s[1]:s[2]])),
-                res.f[s[1]:s[2]],
+                (res.f .* res.filter)[s[1]:s[2]],
                 color = clr, alpha = 0.5
             )
 
-            height = (1 - (i-1) * 0.1) * maximum(res.f) 
+            height = (1 - (i-1) * 0.1) * maximum(res.f .* res.filter) 
             text!(
                 ax2, res.X[2], height, 
                 text = Xlabel[1]*"$(round(wa[i], sigdigits = 2))"*Xlabel[2], 
@@ -622,8 +642,11 @@ function Makie.plot(res::NMRInversions.inv_out_2D)
         end
 
     end ## BUTTON CLICKS
-
+    
+    
     gui
+    
+
 end
 
 end
diff --git a/src/inversion_functions.jl b/src/inversion_functions.jl
index e3fce1c..47b4cb3 100644
--- a/src/inversion_functions.jl
+++ b/src/inversion_functions.jl
@@ -59,11 +59,11 @@ function invert(seq::Type{<:pulse_sequence1D}, x::AbstractArray, y::Vector;
     if isa(alpha, Real)
 
         α = alpha
-        f, r = solve_regularization(ker_struct.K, ker_struct.g, α, solver)
+        f, _ = solve_regularization(ker_struct.K, ker_struct.g, α, solver)
 
     else
 
-        f, r, α = find_alpha(ker_struct, solver, alpha)
+        f, _, α = find_alpha(ker_struct, solver, alpha)
 
     end
 
@@ -71,13 +71,14 @@ function invert(seq::Type{<:pulse_sequence1D}, x::AbstractArray, y::Vector;
     y_fit = create_kernel(seq, x_fit, X) * f
 
     isreal(y) ? SNR = NaN : SNR = calc_snr(y)
+    
+    r = create_kernel(seq, x, X) * f - y
 
     if seq == PFG
         X .= X ./ 1e9
     end
 
-
-    return inv_out_1D(seq, x, y, x_fit, y_fit, X, f, r, SNR, α, [],"")
+    return inv_out_1D(seq, x, y, x_fit, y_fit, X, f, r, SNR, α, [], ones(length(X)),"")
 
 end
 
diff --git a/src/misc.jl b/src/misc.jl
index 16f8a30..c8ea4a6 100644
--- a/src/misc.jl
+++ b/src/misc.jl
@@ -247,3 +247,19 @@ function weighted_averages(r::inv_out_2D)
 
     return wa_T1, wa_T2
 end
+
+
+export filter_range!
+"""
+    filter_range!(res::inv_res_1D , range)
+Apply selected range to the filter, scaling it to keep the integral of `f` constant.
+"""
+function filter_range!(res::inv_out_1D, range)
+
+    integral = sum(res.f)
+    res.filter[range[1]:range[2]] .= 0
+    new_integral = sum(res.f .* res.filter)
+    scale = new_integral != 0 ? integral / new_integral : 0
+    res.filter .= res.filter .* scale
+
+end
diff --git a/src/types.jl b/src/types.jl
index 8c0f2e5..ec45cd2 100644
--- a/src/types.jl
+++ b/src/types.jl
@@ -341,7 +341,7 @@ end
 
 export inv_out_1D
 """
-    inv_out_1D(seq, x, y, xfit, yfit, X, f, r, SNR, α, selections, title)
+    inv_out_1D(seq, x, y, xfit, yfit, X, f, r, SNR, α, selections, filter, title)
 
 Output of the invert function for 1D pulse sequences.
 A structure containing the following fields:
@@ -355,7 +355,8 @@ A structure containing the following fields:
 - `r`, the residuals.
 - `SNR`, the signal-to-noise ratio.
 - `α`, the regularization parameter.
-- `selections`, a vector of tuples whose elements are indicate the first and last index of the selected peaks. 
+- `selections`, a vector of tuples whose elements indicate the first and last index of the selected peaks.
+- `filter`, a vector representing the mask used to filter and scale the data.
 - `title`, a title describing the data.
 
 """
@@ -371,6 +372,7 @@ mutable struct inv_out_1D
     SNR::Real
     alpha::Real
     selections::Vector{Tuple}
+    filter::Vector
     title::String
 end
 


Reviews:
user: login: aris-mav
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-10T22:25:55Z
user: login: louob20
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:52:37Z
user: login: aris-mav
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-19T10:34:39Z

Review comments:
diff_hunk: @@ -247,3 +247,24 @@ function weighted_averages(r::inv_out_2D)
 
     return wa_T1, wa_T2
 end
+
+
+"""
+    delete_range!(results::inv_res_1D , range)
+Set selected range of f to 0, and update fitted curve and residuals.
+"""
+function delete_range!(results::inv_out_1D, range)
+
+    results.f[range[1]:range[2]] .= 0 
+
+    K = create_kernel(results.seq, results.xfit, results.X)
+    g = K * results.f
+    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]
path: src/misc.jl
user: login: aris-mav
body: 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.
created_at: 2025-03-10T22:25:55Z
author_association: OWNER
diff_hunk: @@ -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)
 
path: ext/gui_ext.jl
user: login: aris-mav
body: 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.
created_at: 2025-03-11T09:36:43Z
author_association: OWNER
diff_hunk: @@ -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)
 
path: ext/gui_ext.jl
user: login: aris-mav
body: Basically the delete button would only modify the `filter` field, and the reset button would turn it back to 1's.
created_at: 2025-03-11T10:26:27Z
author_association: OWNER
diff_hunk: @@ -247,3 +247,24 @@ function weighted_averages(r::inv_out_2D)
 
     return wa_T1, wa_T2
 end
+
+
+"""
+    delete_range!(results::inv_res_1D , range)
+Set selected range of f to 0, and update fitted curve and residuals.
+"""
+function delete_range!(results::inv_out_1D, range)
+
+    results.f[range[1]:range[2]] .= 0 
+
+    K = create_kernel(results.seq, results.xfit, results.X)
+    g = K * results.f
+    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]
path: src/misc.jl
user: login: louob20
body: 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.


<img width="677" alt="Screenshot 2025-03-11 at 20 20 20" src="https://github.com/user-attachments/assets/ed059174-98d7-4d40-85fe-3e8f02452392" />

<img width="681" alt="Screenshot 2025-03-11 at 20 20 57" src="https://github.com/user-attachments/assets/3855ec8a-9255-4429-b807-8b7d22f7dfbe" />

(even more seemingly incorrect than the last)


The following 2 are the plots of Kf, using the old and new K.

<img width="327" alt="Screenshot 2025-03-11 at 19 44 48" src="https://github.com/user-attachments/assets/2d6ee1c0-3efd-4084-81bf-67d45fc4cac0" />

<img width="332" alt="Screenshot 2025-03-11 at 19 45 47" src="https://github.com/user-attachments/assets/e66babc1-81be-49bf-a671-4e4cf0e3e2b8" />

created_at: 2025-03-12T16:52:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -247,3 +247,24 @@ function weighted_averages(r::inv_out_2D)
 
     return wa_T1, wa_T2
 end
+
+
+"""
+    delete_range!(results::inv_res_1D , range)
+Set selected range of f to 0, and update fitted curve and residuals.
+"""
+function delete_range!(results::inv_out_1D, range)
+
+    results.f[range[1]:range[2]] .= 0 
+
+    K = create_kernel(results.seq, results.xfit, results.X)
+    g = K * results.f
+    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]
path: src/misc.jl
user: login: aris-mav
body: 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?
created_at: 2025-03-12T20:47:01Z
author_association: OWNER

PR comments:
user: login: aris-mav
created_at: 2025-03-11T09:09:34Z
body: 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.
author_association: OWNER
user: login: louob20
created_at: 2025-03-12T16:56:08Z
body: 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? 
author_association: CONTRIBUTOR
user: login: aris-mav
created_at: 2025-03-12T17:06:06Z
body: > 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.
author_association: OWNER
user: login: louob20
created_at: 2025-03-12T17:13:24Z
body: Got it, sounds good. Thanks!
author_association: CONTRIBUTOR
user: login: louob20
created_at: 2025-03-18T13:20:36Z
body: 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,
<img width="402" alt="Screenshot 2025-03-18 at 12 42 36" src="https://github.com/user-attachments/assets/f03de518-5a13-4caa-87e8-9c1a4cf8ce5f" />
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,
<img width="393" alt="Screenshot 2025-03-18 at 12 43 46" src="https://github.com/user-attachments/assets/52b45ff1-7fba-4474-a628-0123a2b15b17" />
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`
<img width="345" alt="Screenshot 2025-03-18 at 13 13 42" src="https://github.com/user-attachments/assets/117db8d1-c2ae-4a74-9f3b-fda83d8d0cc6" />

Updated, `create_kernel(res.seq, res.x, res.X) * res.f`
<img width="330" alt="Screenshot 2025-03-18 at 13 14 40" src="https://github.com/user-attachments/assets/a243c3bb-ca6c-42cb-bd14-f055cd5dc62c" /> 


author_association: CONTRIBUTOR
user: login: aris-mav
created_at: 2025-03-18T14:36:00Z
body: 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).
author_association: OWNER
user: login: aris-mav
created_at: 2025-03-18T14:58:43Z
body: Would you please enable “Allow edits from maintainers"? 
This way I'll be able to push some changes here as well.
author_association: OWNER
user: login: louob20
created_at: 2025-03-18T15:18:52Z
body: > 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?
author_association: CONTRIBUTOR
user: login: aris-mav
created_at: 2025-03-18T15:23:32Z
body: > 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.
author_association: OWNER
user: login: louob20
created_at: 2025-03-18T18:44:28Z
body: @aris-mav I believe that last commit was all, if you would like to run the code before merging
author_association: CONTRIBUTOR
user: login: aris-mav
created_at: 2025-03-18T19:58:37Z
body: It seems that the filter selection function still updates the residuals and the yfit directly on the structure. 
Am I missing a commit perhaps?
author_association: OWNER
user: login: louob20
created_at: 2025-03-18T22:18:02Z
body: I have changed it, should be good now.
author_association: CONTRIBUTOR
user: login: aris-mav
created_at: 2025-03-19T10:35:48Z
body: Good job @louob20 !
author_association: OWNER