Skip to content
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

More usability improvements sw fitpowder (DRAFT IN PROG) #210

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
72 changes: 69 additions & 3 deletions +sw_tests/+unit_tests/unittest_sw_fitpowder.m
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,41 @@ function test_init_data_1d_indep_bg(testCase)
testCase.verify_results(out, expected_fitpow);
end

function test_set_background_strategy(testCase)
function test_set_background_strategy_to_planar(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
testCase.fit_func, testCase.j1, "independent");
% set some background parameters (correpsonding to planar bg
% with slope_en=slope_q=intercept =2
out.set_bg_parameters(1, 2); % en_slope = 2
out.set_bg_parameters(2, 10, 1); % intercept = 10 for cut 1
out.set_bg_parameters(2, 12, 2); % intercept = 12 for cut 2
out.set_background_strategy("planar");
expected_fitpow = testCase.default_fitpow;
expected_fitpow.params(2:end-1) = 2;
expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
testCase.verify_results(out, expected_fitpow);
testCase.verify_results(out, expected_fitpow, ...
testCase.default_fields, ...
'abs_tol', 1e-10);
end

function test_set_background_strategy_to_indep(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
testCase.fit_func, testCase.j1, "planar");
% set some background parameters (correpsonding to planar bg
% with slope_en=slope_q=intercept =2
out.set_bg_parameters(1:3, [2,2,2]); % en_slope = 2
out.set_background_strategy("independent");
expected_fitpow = testCase.default_fitpow;
% add extra background param
expected_fitpow.params = expected_fitpow.params([1:2,2:end],:);
expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
expected_fitpow.params(2:2:end-1) = 2; % en slope
expected_fitpow.params(3) = 10; % intercept = 10 for cut 1
expected_fitpow.params(5) = 12; % intercept = 12 for cut 2
expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
testCase.verify_results(out, expected_fitpow, ...
testCase.default_fields, ...
'abs_tol', 1e-10);
end

function test_replace_2D_data_with_1D_cuts(testCase)
Expand Down Expand Up @@ -342,6 +370,21 @@ function test_fit_background(testCase, fit_params)
'abs_tol', 1e-4);
end

function test_fit_background_and_scale(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
testCase.fit_func, testCase.j1);
out.fix_bg_parameters(1:2); % fix slopes of background to 0
out.set_bg_parameters(3, 1.5); % initial guess
out.fit_background_and_scale();
expected_fitpow = testCase.default_fitpow;
expected_fitpow.params(end-1) = 0.0029;
expected_fitpow.params(end) = 15.47;
expected_fitpow.bounds(2:3,:) = 0; % fixed bg slopes
testCase.verify_results(out, expected_fitpow, ...
testCase.default_fields, ...
'abs_tol', 1e-3);
end

function test_calc_cost_func_of_background_indep(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
testCase.fit_func, testCase.j1, "independent", 2);
Expand Down Expand Up @@ -406,11 +449,12 @@ function test_calc_uncertainty(testCase)
function test_estimate_scale_factor(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
testCase.fit_func, testCase.j1);
out.set_bg_parameters(3, 0.05); % set constant bg
out.powspec_args.dE = 0.1; % constant energy resolution
out.powspec_args.hermit = true;
out.estimate_scale_factor()
expected_fitpow = testCase.default_fitpow;
expected_fitpow.params(end) = 17.6;
expected_fitpow.params(end) = 17.36;
testCase.verify_results(out, expected_fitpow, ...
testCase.default_fields, 'abs_tol', 1e-1);
end
Expand Down Expand Up @@ -450,6 +494,28 @@ function test_add_1Dcuts_after_2D_data(testCase)
expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d; % integrtate over nQ pts
testCase.verify_results(out, expected_fitpow);
end

function test_set_bg_region_data_2d(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
testCase.fit_func, testCase.j1);
out.set_bg_region(0,1.5); % for all Q
out.set_bg_region(2.5,3.5,4.5,inf); % for highest Q
expected_fitpow = testCase.default_fitpow;
expected_fitpow.ibg = [1;4;6];
testCase.verify_results(out, expected_fitpow);
end

function test_set_bg_region_data_1d(testCase)
out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
testCase.fit_func, testCase.j1);
expected_fitpow = testCase.default_fitpow;
expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
out.set_bg_region(0,1.5); % for all cuts
out.set_bg_region(2.5,3.5,2); % for last cut
expected_fitpow.ibg = [1;4;6];
testCase.verify_results(out, expected_fitpow);
end

end

end
161 changes: 130 additions & 31 deletions swfiles/sw_fitpowder.m
Original file line number Diff line number Diff line change
Expand Up @@ -215,22 +215,44 @@ function initialise_background_parameters_and_bounds(obj)
end

function set_background_strategy(obj, strategy)
if (strategy == "independent" && obj.ndim==1) || strategy == "planar"
obj.background_strategy = strategy;
else
error('sw_fitpowder:set_background_strategy', ...
'Parameter indices supplied must be within number of model parameters');
end
if obj.background_strategy == "planar"
if strategy == "planar"
obj.fbg = obj.fbg_planar;
else
elseif strategy == "independent"
if obj.ndim == 2
error('sw_fitpowder:invalidinput', ...
['Can only have independent background strategy' ...
' for 1D datasets.'])
end
obj.fbg = obj.fbg_indep;
end
% store previous bg parameters before reset size of param array
ibg_par = (obj.nparams_model+1):(numel(obj.params)-1);
bg_pars = obj.params(ibg_par);
% reset param array d bounds etc.
obj.background_strategy = strategy;
obj.nparams_bg = obj.get_nparams_in_background_func();
if ~isempty(obj.params)
warning('sw_fitpowder:set_background_strategy', ...
'Overwriting background parmaweters with 0.');
obj.initialise_background_parameters_and_bounds();
if any(abs(bg_pars) > obj.zero_abs_tol) && obj.ndim == 1
% set good guess for background pars
modQs = obj.get_modQ_cens_of_cuts();
if obj.background_strategy == "independent"
% keep same energy slope (scaled by nQ)
obj.set_bg_parameters(1, bg_pars(1));
% set intercept (zero energy) for reach cut separately
bg_pars = num2cell(bg_pars);
intercepts = obj.fbg_planar(0, modQs, bg_pars{:});
for icut = 1:obj.ncuts
obj.set_bg_parameters(2, intercepts(icut), icut);
end
elseif obj.background_strategy == "planar"
% average the energy slope
slope_en = mean(bg_pars(1:2:end));
% fit intercepts to linear Q
pval = polyfit(modQs, bg_pars(2:2:end), 1);
obj.set_bg_parameters(1:3, [slope_en, pval]);
end
end
end
end

Expand Down Expand Up @@ -356,9 +378,6 @@ function replace_2D_data_with_1D_cuts(obj, qmins, qmaxs, background_strategy)
obj.add_data(cuts);
if nargin > 3 && background_strategy ~= obj.background_strategy
obj.set_background_strategy(background_strategy)
warning('spinw:sw_fitpowder:replace_2D_data_with_1D_cuts', ...
['Background strategy changed - background ' ...
'parameters and bounds will be cleared.']);
end
end

Expand Down Expand Up @@ -411,8 +430,37 @@ function set_caching(obj, do_cache)
function clear_cache(obj)
obj.ycalc_cached = [];
obj.model_params_cached = [];
obj.clear_background_region();
end

function clear_background_region(obj)
obj.ibg = [];
obj.reset_errors_of_bg_bins()
obj.reset_errors_of_bg_bins();
end

function set_bg_region(obj, en_lo, en_hi, varargin)
% 2D data: obj.set_bg_region(en_lo, en_hi, q_lo, q_hi)
% 1D data: obj.set_bg_region(en_lo, en_hi, icuts)
% both:
% obj.set_bg_region(en_lo, en_hi) % apply to all cuts/Q
if isempty(varargin)
iq = 1:size(obj.y, 2);
elseif obj.ndim == 1 && numel(varargin)==1
iq = varargin{1};
elseif obj.ndim==2 && numel(varargin)==2
[q_lo, q_hi] = varargin{:};
iq = obj.modQ_cens < q_hi & obj.modQ_cens > q_lo;
else
error('spinw:sw_fitpowder:set_bg_region', ...
['Wrong number of additional aruguments: 2 required' ...
' for 2D problem (qlo and qhi)' ...
'and 1 required for 1D problem (icut indices)']);
end
ien = obj.ebin_cens < en_hi & obj.ebin_cens > en_lo;
mask = false(size(obj.y));
mask(ien, iq) = true;
mask = mask & isfinite(obj.y);
obj.ibg = [obj.ibg; find(mask(:))];
end

function [ycalc, bg] = calc_spinwave_spec(obj, params)
Expand Down Expand Up @@ -504,6 +552,19 @@ function clear_cache(obj)
varargin{:});
end

function varargout = fit_background_and_scale(obj, varargin)
% fix all model parameters
initial_bounds = obj.bounds; % store so bounds can be reset
obj.fix_model_parameters(1:obj.nparams_model);
varargout = cell(1,nargout(obj.optimizer));
[varargout{:}] = obj.fit(varargin{:});
% reset bounds
obj.bounds = initial_bounds;
% overwrite non model parameters
obj.params = varargout{1}(:); % assume first output (as for ndbase)
end


function [param_errors, varargout] = calc_uncertainty(obj, params, varargin)
% Function to estimate parameter errors by evaluating hessian.
% By default will only evaluate derivatives for parameters
Expand Down Expand Up @@ -536,7 +597,7 @@ function clear_cache(obj)
varargout = {cov};
end

function fit_background(obj, varargin)
function varargout = fit_background(obj, varargin)
if isempty(obj.ibg)
obj.find_indices_and_mean_of_bg_bins();
end
Expand All @@ -554,26 +615,25 @@ function fit_background(obj, varargin)
else
fobj = @obj.calc_cost_func_of_background; % minimise scalar
end

[bg_params, ~, stat] = ndbase.simplex([], fobj, ...
obj.params(ibg_par)', ...
'lb', obj.bounds(ibg_par,1)', ...
'ub', obj.bounds(ibg_par,2)', ...
varargin{:});
if stat.exitFlag == 1
obj.params(ibg_par) = bg_params;
else
warning('spinw:sw_fitpowder:fit_background', ...
'Fit failed - parameters not updated.');
end
varargout = cell(1,nargout(obj.optimizer));
[varargout{:}] = obj.optimizer([], fobj, ...
obj.params(ibg_par)', ...
'lb', obj.bounds(ibg_par,1)', ...
'ub', obj.bounds(ibg_par,2)', ...
varargin{:});
obj.params(ibg_par) = varargout{1}(:);
end

function estimate_scale_factor(obj)
% set scale factor to 1
params = obj.params;
params(end) = 1;
[ycalc, bg] = calc_spinwave_spec(obj, params);
scale = max(obj.y - mean(bg(:)), [], "all")/max(ycalc, [], "all");
if obj.ndim == 1 && obj.background_strategy=="planar"
% integrate nQ |Q| points for each cut
bg = obj.rebin_powspec_to_1D_cuts(bg);
end
scale = max(obj.y - bg, [], "all")/max(ycalc - bg, [], "all");
obj.params(end) = scale;
end

Expand Down Expand Up @@ -611,7 +671,7 @@ function plot_result(obj, params, varargin)
end

function plot_1d_cuts_on_data(obj, ycalc, varargin)
modQs = mean(reshape(obj.modQ_cens, [], obj.ncuts), 1);
modQs = obj.get_modQ_cens_of_cuts();
for icut = 1:obj.ncuts
ax = subplot(1, obj.ncuts, icut);
hold on; box on;
Expand All @@ -637,7 +697,7 @@ function plot_2d_contour_on_data(obj, ycalc, varargin)
ax = subplot(1,1,1);
box on; hold on;
h = imagesc(ax, obj.modQ_cens, obj.ebin_cens, y);
h.AlphaData = obj.y > 0; % make empty bins transparent
h.AlphaData = double(obj.y > 0); % make empty bins transparent
cbar = colorbar(ax);
cbar.Label.String = "Intensity";
xlabel(ax, "$\left|Q\right| (\AA^{-1})$", 'interpreter','latex');
Expand All @@ -646,6 +706,16 @@ function plot_2d_contour_on_data(obj, ycalc, varargin)
xlim(ax, [obj.modQ_cens(1), obj.modQ_cens(end)]);
end

function plot_background_region(obj)
if ~isempty(obj.ibg)
if obj.ndim == 1
obj.plot_background_region_1d()
else
obj.plot_background_region_2d()
end
end
end

function plot_1d_cuts_of_2d_data(obj, qmins, qmaxs, params)
% optionally plot ycalc provided, otherwise will plot
% fitpow.ycalc if not empty
Expand Down Expand Up @@ -692,14 +762,20 @@ function plot_1d_cuts_of_2d_data(obj, qmins, qmaxs, params)
resid = resid(isfinite(resid) & e > obj.zero_abs_tol);
end


function modQ_cens = get_modQ_cens_of_cuts(obj)
modQ_cens = mean(reshape(obj.modQ_cens, [], obj.ncuts), 1);
end

function cut = cut_2d_data(obj, qmin, qmax)
assert(obj.ndim ==2, ...
'sw_fitpowder:invalidinput', ...
'This function is only valid for 2D data');
cut = struct('x', obj.ebin_cens, 'qmin', qmin, 'qmax', qmax);
ikeep = obj.modQ_cens > qmin & obj.modQ_cens <= qmax;
ifinite = isfinite(obj.y(:, ikeep));
cut.y = mean(obj.y(:, ikeep), 2, 'omitnan');
cut.e = sqrt(sum(obj.e(:, ikeep).^2, 2))/sum(ikeep);
cut.e = sqrt(sum(obj.e(:, ikeep).^2, 2))./sum(ifinite, 2);
end
function ycalc = rebin_powspec_to_1D_cuts(obj, ycalc)
% sum up successive nQ points along |Q| axis (dim=2)
Expand Down Expand Up @@ -797,6 +873,29 @@ function fix_parameters(obj, iparams)
obj.ibg = iseed(isort(ipt:end));
end
end

function plot_background_region_2d(obj)
im = findobj(gca, 'type', 'Image');
is_valid = ~isempty(im) && all(size(im.CData) == size(obj.y));
assert(is_valid, 'sw_fitpowder:invalidinput', ...
['This function requires active axes to have an' ...
' image/colorfill plot of the data.']);
im.AlphaData(obj.ibg) = 0.5;
end

function plot_background_region_1d(obj)
fig = gcf();
axs = flip(fig.Children);
assert(obj.ncuts==numel(axs), 'sw_fitpowder:invalidinput', ...
['This function requires active figure to have same' ...
' number of axes as 1D cuts']);
[ien, icuts] = ind2sub(size(obj.y), obj.ibg);
for icut = 1:obj.ncuts
ibg_cut = ien(icuts == icut);
plot(axs(icut), obj.ebin_cens(ibg_cut), ...
obj.y(ibg_cut,icut), 'xb')
end
end
end
methods (Static=true, Hidden=true, Access = private)
function data_struct = convert_horace_to_struct(data)
Expand Down
Loading