Skip to content

Commit

Permalink
Test optmagk (#111)
Browse files Browse the repository at this point in the history
* Add initial fm/afm tests

* Small changes:

- Use TestMethodSetup so a new spinw object is used in each test
- Put default 'F' in properties

* Add kbase test

* Use multiple kbase vectors

* Add symbolic warn test

* Error if kbase is incorrect shape

The code required for this was already in genmagstr, this has been
refactored so it can also be called from optmagk

* Fix tests on CI

- Increase tolerance in independent kbase test
- Add symbolic tags to required tests

* Update optmagk docstring

* Ensure optmagk args get passed to ndbase.pso

Have put varargin last in ndbase.pso call, otherwise user provided
varargin is overwritten by the defaults

* Update publish-unit-test-result-action to v2

Using v1 gives the following warning:

The `set-output` command is deprecated and will be disabled soon. Please
upgrade to using Environment Files. For more information see:
https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
  • Loading branch information
rebeccafair authored Nov 15, 2022
1 parent f2fcb71 commit 91cc80a
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 30 deletions.
112 changes: 112 additions & 0 deletions +sw_tests/+unit_tests/unittest_spinw_optmagk.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
classdef unittest_spinw_optmagk < sw_tests.unit_tests.unittest_super
% Runs through unit test for @spinw/spinwave.m

properties
swobj = [];
% output from optmagk
default_mag_str = struct('nExt', int32([1 1 1]), ...
'F', [sqrt(1/3) + 1i*sqrt(1/2); ...
sqrt(1/3); ...
sqrt(1/3) - 1i*sqrt(1/2)], ...
'k', [1; 0; 0]);
end
properties (TestParameter)
kbase_opts = {[1; 1; 0], [1 0; 0 1; 0 0]};
end
methods (TestMethodSetup)
function setup_chain_model(testCase)
testCase.swobj = spinw();
testCase.swobj.genlattice('lat_const', [3 8 8])
testCase.swobj.addatom('r',[0; 0; 0],'S',1)
testCase.swobj.gencoupling();
end
end
methods (Test, TestTags = {'Symbolic'})
function test_symbolic_warns_returns_nothing(testCase)
testCase.swobj.addmatrix('label', 'J1', 'value', 1);
testCase.swobj.addcoupling('mat', 'J1', 'bond', 1);
testCase.swobj.symbolic(true)
testCase.verifyWarning(...
@() testCase.swobj.optmagk, ...
'spinw:optmagk:NoSymbolic');
testCase.verifyEmpty(testCase.swobj.mag_str.k);
testCase.verifyEmpty(testCase.swobj.mag_str.F);
testCase.verify_val(testCase.swobj.mag_str.nExt, ...
int32([1 1 1]));
end
end
methods (Test)
function test_wrong_shape_kbase_raises_error(testCase)
testCase.verifyError(...
@() testCase.swobj.optmagk('kbase', [1 1 0]), ...
'spinw:optmagk:WrongInput');
end
function test_fm_chain_optk(testCase)
testCase.swobj.addmatrix('label', 'J1', 'value', -1);
testCase.swobj.addcoupling('mat', 'J1', 'bond', 1);
out = testCase.swobj.optmagk;
out.stat = rmfield(out.stat, 'nFunEvals');

expected_mag_str = testCase.default_mag_str;
expected_out = struct('k', expected_mag_str.k, ...
'E', -1, ....
'F', expected_mag_str.F, ...
'stat', struct('S', 0, ...
'exitflag', -1));
% Test struct output by optmagk
testCase.verify_val(out, expected_out, 'abs_tol', 1e-4);
% Also test spinw attributes have been set
expected_mag_str = testCase.default_mag_str;
testCase.verify_val(testCase.swobj.mag_str, expected_mag_str, ...
'abs_tol', 1e-4);
end
function test_afm_chain_optk(testCase)
testCase.swobj.addmatrix('label', 'J1', 'value', 1);
testCase.swobj.addcoupling('mat', 'J1', 'bond', 1);
testCase.swobj.optmagk;
expected_mag_str = testCase.default_mag_str;
expected_mag_str.k = [0.5; 0; 0];
testCase.verify_val(testCase.swobj.mag_str, expected_mag_str, ...
'abs_tol', 1e-4);
end
function test_kbase(testCase, kbase_opts)
% See https://doi.org/10.1103/PhysRevB.59.14367
swobj = spinw();
swobj.genlattice('lat_const', [3 3 8])
swobj.addatom('r',[0; 0; 0],'S',1)
swobj.gencoupling();
J1 = 1.2;
J2 = 1.0;
swobj.addmatrix('label', 'J1', 'value', J1);
swobj.addmatrix('label', 'J2', 'value', J2);
swobj.addcoupling('mat', 'J1', 'bond', 2, 'subidx', 2);
swobj.addcoupling('mat', 'J2', 'bond', 1);
% Use rng seed for reproducible results
swobj.optmagk('kbase', kbase_opts, 'seed', 1);

expected_k = acos(-J2/(2*J1))/(2*pi);
rel_tol = 1e-5;
if abs(expected_k - swobj.mag_str.k(1)) > rel_tol*expected_k
% If this k doesn't match, try 1-k
expected_k = 1 - expected_k; % k and 1-k are degenerate
end
expected_mag_str = testCase.default_mag_str;
expected_mag_str.k = [expected_k; expected_k; 0];
testCase.verify_val(swobj.mag_str, expected_mag_str, ...
'rel_tol', 1e-5);
end
function test_afm_chain_ndbase_pso_varargin_passed(testCase)
testCase.swobj.addmatrix('label', 'J1', 'value', 1);
testCase.swobj.addcoupling('mat', 'J1', 'bond', 1);
% Verify in default case there is no warning
testCase.verifyWarningFree(...
@() testCase.swobj.optmagk, ...
'pso:convergence');
% Test that MaxIter gets passed through to ndbase.pso, triggers
% convergence warning
testCase.verifyWarning(...
@() testCase.swobj.optmagk('MaxIter', 1), ...
'pso:convergence');
end
end
end
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ jobs:
with:
path: artifacts
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v1
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: artifacts/**/junit_report*.xml
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ Bug Fixes
Previously this would've caused a crash.
- Fix error when plotting progress of ``optmagsteep`` without existing figure
- Correctly report magnetic moments in each iteration of ``optmagsteep``.
- Raise error if invalid shape ``kbase`` is provided to ``optmagk``,
previously it would be silently set to empty
- Ensure varargin is correctly passed through to ``ndbase.pso`` from
``optmagk``. Previously user provided ``TolFun``, ``TolX`` and
``MaxIter`` would be overwritten by the defaults.
23 changes: 2 additions & 21 deletions swfiles/@spinw/genmagstr.m
Original file line number Diff line number Diff line change
Expand Up @@ -226,26 +226,7 @@ function genmagstr(obj, varargin)
inpForm.soft = [inpForm.soft {true true false false false }];

param = sw_readparam(inpForm, varargin{:});

% Error if S or k is provided but is empty. This means it has failed the
% input validation, but hasn't caused an error because soft=True
err_str = [];
if isstruct(varargin{1})
varargin_struct = varargin{1};
else
varargin_struct = struct(varargin{:});
end
varargin_names = fields(varargin_struct);
if any(strcmpi(varargin_names, 'S')) && isempty(param.S)
err_str = ["S"];
end
if any(strcmpi(varargin_names, 'k')) && isempty(param.k)
err_str = [err_str "k"];
end
if length(err_str) > 0
error('spinw:genmagstr:WrongInput', 'Incorrect input size for ' + ...
join(err_str, ', '));
end
softparamcheck(["S", "k"], 'genmagstr', param, varargin{:});

if strcmpi(param.mode, 'rotate') && isempty(obj.mag_str.F)
error('spinw:genmagstr:WrongInput', ['rotate mode requires a magnetic ' ...
Expand Down Expand Up @@ -292,7 +273,7 @@ function genmagstr(obj, varargin)
if ~any(strcmp(fields(mode_args), param.mode))
error('spinw:genmagstr:WrongMode','Wrong param.mode value!');
else
unused_args = {varargin_names{:}};
unused_args = vararginnames(varargin{:});
unused_args(ismember(lower(unused_args), ["mode" "norm"])) = [];
unused_args(ismember(lower(unused_args), lower(mode_args.(lower(param.mode))))) = [];
if ~isempty(unused_args)
Expand Down
16 changes: 8 additions & 8 deletions swfiles/@spinw/optmagk.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
% `res = optmagk(obj,Name,Value)` determines the optimal propagation vector
% using the Luttinger-Tisza method. It calculates the Fourier transform of
% the Hamiltonian as a function of wave vector and finds the wave vector
% that corresponds to the smalles global eigenvalue of the Hamiltonian. It
% also returns the normal vector that corresponds to the rotating
% coordinate system. The global optimization is achieved using
% Particle-Swarm optimizer.
% that corresponds to the smalles global eigenvalue of the Hamiltonian.
% The global optimization is achieved using Particle-Swarm optimizer. This
% function sets k and F in spinw.mag_str, and also returns them.
%
% ### Input Arguments
%
Expand Down Expand Up @@ -41,8 +40,8 @@
% : Structure with the following fields:
% * `k` Value of the optimal k-vector, with values between 0
% and 1/2.
% * `n` Normal vector, defines the rotation axis of the
% rotating coordinate system.
% * `F` Fourier components for every spin in the magnetic
% cell.
% * `E` The most negative eigenvalue at the given propagation
% vector.
% * `stat` Full output of the [ndbase.pso] optimizer.
Expand All @@ -59,6 +58,7 @@

warning('off','sw_readparam:UnreadInput')
param = sw_readparam(inpForm, varargin{:});
softparamcheck(["kbase"], 'optmagk', param, varargin{:});
warning('on','sw_readparam:UnreadInput')


Expand Down Expand Up @@ -126,8 +126,8 @@

warning('off','sw_readparam:UnreadInput')
% optimise the energy using particle swarm
[pOpt0, ~, ~] = ndbase.pso([],@optfun,1/4*kones,'lb',0*kones,'ub',kones,varargin{:},...
'TolFun',1e-5,'TolX',1e-5,'MaxIter',1e3);
[pOpt0, ~, ~] = ndbase.pso([],@optfun,1/4*kones,'lb',0*kones,'ub',kones,...
'TolFun',1e-5,'TolX',1e-5,'MaxIter',1e3, varargin{:});
warning('on','sw_readparam:UnreadInput')

% generate an R-value
Expand Down
34 changes: 34 additions & 0 deletions swfiles/@spinw/private/softparamcheck.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
function softparamcheck(params_to_check, func_name, param, varargin)
% Checks if any parameters have been provided in varargin, but set to
% empty, and raises an error if so. This function is needed because by
% default 'soft' params will silently be set to empty if their shape is
% incorrect, causing unexpected behaviour for users.
%
% Input:
%
% params_to_check A list of strings of the parameters to check e.g.
% ["S", "k"]
% func_name The name of the function this is being called from
% to be used in the error identifier text
% param The output of sw_readparam. If an argument exists in
% varargin, but has been set to empty in param, we know
% it has been silently ignored so raise an error
% varargin Varargin that was used as input to sw_readparam to
% create param, this should be name-value pairs or a
% struct
%
if isempty(varargin)
return
end
names = vararginnames(varargin{:});
err_str = [];
for i = 1:length(params_to_check)
if any(strcmpi(names, params_to_check(i))) ...
&& isempty(param.(params_to_check(i)))
err_str = [err_str params_to_check(i)];
end
end
if ~isempty(err_str) > 0
error(['spinw:' char(func_name) ':WrongInput'], ...
'Incorrect input size for ' + join(err_str, ', '));
end
16 changes: 16 additions & 0 deletions swfiles/@spinw/private/vararginnames.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function names = vararginnames(varargin)
% Given varargin, returns a list of the given
% argument names (so we know which arguments a user has passed to a function).
% Note that inputs to SpinW functions can either be name-value pairs or a
% struct
%
% Input:
%
% varargin Variable-length argument list (name value pairs) or struct
if isstruct(varargin{1})
varargin_struct = varargin{1};
else
varargin_struct = struct(varargin{:});
end
names = fields(varargin_struct);
end

0 comments on commit 91cc80a

Please sign in to comment.