diff --git a/+sw_tests/+performance_tests/BiFeO3.m b/+sw_tests/+performance_tests/BiFeO3.m
new file mode 100644
index 000000000..7027a64cb
--- /dev/null
+++ b/+sw_tests/+performance_tests/BiFeO3.m
@@ -0,0 +1,66 @@
+function BiFeO3()
+ % setup
+ bfo = spinw;
+ bfo.genlattice('lat_const', [5.58 5.58 13.86], 'angled', [90 90 120], ...
+ 'sym', 'R 3 c');
+ bfo.addatom('r', [0 0 0.2212], 'S', 2.5, 'label', 'MFe3');
+ bfo.gencoupling('maxDistance', 20)
+ bfo.addmatrix('label', 'J1', 'value', 0.69029);
+ bfo.addmatrix('label', 'J2', 'value', 0.2)
+ bfo.addcoupling('mat', 'J1', 'bond', 1)
+ bfo.addcoupling('mat', 'J2', 'bond', 2)
+ bfo.addmatrix('label', 'D', 'value', [1 -1 0]*0.185)
+ bfo.addcoupling('mat', 'D', 'bond', 2)
+ bfo.optmagk('kbase', [1; 1; 0], 'seed', 1);
+ bfo.optmagsteep('random',false,'TolX', 1e-12);
+
+ % test parameters
+ % add omega tol for imag eigenvalues (ignored if hermit=0)
+ spinwave_args_common = {{[-1/2 0 0], [0 0 0], [1/2 1/2 0], 30}, ...
+ 'sortMode', false, 'hermit', 0, ...
+ 'omega_tol', 0.05};
+ egrid_args = {'component','Sperp','Evect',0:0.1:5, 'imagChk', 0};
+ inst_args = {'dE',0.1};
+
+ % do a spin wave calculation for incommensurate case ([nExt=[1,1,1])
+ % and commensurate nExt=0.01 - which corresponds to a supercell of
+ % [11 11 1]
+ for do_supercell = 0:1
+ if do_supercell
+ nExt = 0.01;
+ bfo.genmagstr('nExt', nExt)
+ mexs = 1; % requires ~100GB RAM with no mex
+ else
+ nExt = [1,1,1];
+ mexs = 0:1;
+ end
+ for mex = mexs
+ swpref.setpref('usemex', logical(mex))
+ if mex && ~do_supercell
+ hermits = 0:1;
+ else
+ % chol throws error as matrix is not positive-definite
+ hermits = 0;
+ end
+ for hermit = hermits
+ for optmem = 0:5:10
+ spinwave_args = [spinwave_args_common, ...
+ 'hermit', hermit, 'optmem', optmem];
+ test_name = [mfilename(), '_mex_', ...
+ num2str(swpref.getpref('usemex').val), ...
+ '_nExt_', regexprep(num2str(nExt), ' +', '_'), ...
+ '_hermit_', num2str(hermit), ...
+ '_optmem_', num2str(optmem)];
+ sw_tests.utilities.profile_spinwave(test_name, bfo, ...
+ spinwave_args,...
+ egrid_args, ...
+ inst_args, ...
+ 0:1);
+ end
+ end
+ end
+ end
+
+
+
+end
\ No newline at end of file
diff --git a/+sw_tests/+performance_tests/FMchain.m b/+sw_tests/+performance_tests/FMchain.m
new file mode 100644
index 000000000..eeff57dda
--- /dev/null
+++ b/+sw_tests/+performance_tests/FMchain.m
@@ -0,0 +1,45 @@
+function FMchain()
+ % setup
+ FMchain = spinw;
+ FMchain.genlattice('lat_const',[3 8 8],'angled',[90 90 90])
+ FMchain.addatom('r', [0 0 0],'S', 1,'label','MCu1')
+ FMchain.gencoupling('maxDistance',7)
+ FMchain.addmatrix('value',-eye(3),'label','Ja')
+ FMchain.addcoupling('mat','Ja','bond',1);
+ FMchain.genmagstr('mode','direct', 'k',[0 0 0], ...
+ 'n',[1 0 0],'S',[0; 1; 0]);
+
+
+ % test parameters
+ if sw_tests.utilities.is_daaas()
+ do_profiles = 0; % for some reason profile takes > 12 hrs on IDAaaS
+ else
+ do_profiles = 0:1;
+ end
+ spinwave_args_common = {{[0 0 0], [1 0 0], 1e7}, ...
+ 'sortMode', false};
+ egrid_args = {'component','Sperp','Evect',0:0.1:5};
+ inst_args = {'dE',0.1};
+
+ for mex = 0:1
+ swpref.setpref('usemex', logical(mex))
+ for hermit = 0:1
+ for optmem = 0:5:10
+ spinwave_args = [spinwave_args_common, ...
+ 'hermit', hermit, 'optmem', optmem];
+ test_name = [mfilename() '_mex_', ...
+ num2str(swpref.getpref('usemex').val), ...
+ '_hermit_', num2str(hermit), ...
+ '_optmem_' num2str(optmem)];
+ sw_tests.utilities.profile_spinwave(test_name, FMchain, ...
+ spinwave_args, ...
+ egrid_args, ...
+ inst_args, ...
+ do_profiles);
+ end
+ end
+ end
+
+
+
+end
\ No newline at end of file
diff --git a/+sw_tests/+system_tests/systemtest_spinwave.m b/+sw_tests/+system_tests/systemtest_spinwave.m
new file mode 100644
index 000000000..289f4361f
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave.m
@@ -0,0 +1,233 @@
+classdef systemtest_spinwave < matlab.unittest.TestCase
+ % Base class for all systems test of spinwave.m based on tutorials
+
+ properties
+ generate_reference_data = false;
+ reference_data = [];
+ reference_data_dir = fullfile('.', 'test_data', 'system_tests');
+ relToll = 0.01;
+ absToll = 1e-6;
+ swobj = [];
+ cleanup_warnings = {};
+ end
+
+ methods (TestClassSetup)
+ function get_reference_data(testCase)
+ if isempty(testCase.reference_data_file)
+ return
+ end
+ fname = fullfile(testCase.reference_data_dir, testCase.reference_data_file);
+ if ~exist(testCase.reference_data_dir, 'dir')
+ mkdir(testCase.reference_data_dir);
+ end
+ if ~exist(fname, 'file') || testCase.generate_reference_data
+ testCase.generate_reference_data = true;
+ tmp = []; save(fname, 'tmp');
+ warning('Generating reference data');
+ else
+ testCase.reference_data = load(fname);
+ end
+ end
+ function disable_mex_setup(testCase)
+ swpref.setpref('usemex', false);
+ end
+ end
+
+ methods (TestClassTeardown)
+ function save_reference_data(testCase)
+ if testCase.generate_reference_data
+ fname = fullfile(testCase.reference_data_dir, testCase.reference_data_file);
+ ref_dat = load(fname);
+ ref_dat = rmfield(ref_dat, 'tmp');
+ save(fname, '-struct', 'ref_dat');
+ end
+ end
+ function disable_mex_teardown(testCase)
+ swpref.setpref('usemex', false);
+ end
+ end
+
+ methods (Static)
+ function out = get_hash(obj)
+ % Calculates a hash for an object or struct using undocumented built-ins
+ % Based on DataHash (https://uk.mathworks.com/matlabcentral/fileexchange/31272-datahash)
+ Engine = java.security.MessageDigest.getInstance('MD5');
+ Engine.update(getByteStreamFromArray(obj));
+ out = typecast(Engine.digest, 'uint8');
+ end
+ function out = sanitize_data(in_dat)
+ if isstruct(in_dat)
+ out = sanitize_struct(in_dat);
+ elseif iscell(in_dat)
+ out = sanitize_cell(in_dat);
+ elseif isnumeric(in_dat)
+ out = sanitize(in_dat);
+ else
+ out = in_dat;
+ end
+ end
+ end
+
+ methods
+ function out = approxMatrix(testCase, actual, expected, frac_not_match)
+ % Checks if two arrays are approximately the same with most entries equal but a fraction not
+ if iscell(actual)
+ out = actual;
+ for ii = 1:numel(actual)
+ out{ii} = testCase.approxMatrix(actual{ii}, expected{ii}, frac_not_match);
+ end
+ else
+ diff = abs(actual - expected);
+ rel_diff = diff ./ expected;
+ if (numel(find((diff > testCase.absToll) & (rel_diff > testCase.relToll))) / numel(actual)) < frac_not_match
+ out = expected;
+ else
+ out = actual;
+ end
+ end
+ end
+ function [actual, expected] = verify_eigval_sort(testCase, actual, expected, nested)
+ if nargin < 4
+ nested = 0;
+ end
+ if iscell(actual)
+ for ii = 1:numel(actual)
+ [actual{ii}, expected{ii}] = testCase.verify_eigval_sort(actual{ii}, expected{ii}, nested);
+ end
+ else
+ % Checks if actual and expected eigenvalues match, otherwise try a different sorting
+ import matlab.unittest.constraints.*
+ theseBounds = RelativeTolerance(testCase.relToll) | AbsoluteTolerance(testCase.absToll);
+ comparator = IsEqualTo(expected, 'Within', theseBounds);
+ if ~comparator.satisfiedBy(actual)
+ if nested > 1
+ actual = sort(abs(actual));
+ expected = sort(abs(expected));
+ else
+ actual = sort(actual, 'ComparisonMethod', 'real');
+ expected = sort(expected, 'ComparisonMethod', 'real');
+ end
+ if nested < 2
+ [actual, expected] = testCase.verify_eigval_sort(actual, expected, nested + 1);
+ end
+ end
+ end
+ end
+ function fieldname = get_fieldname(testCase, pars)
+ if isempty(pars)
+ fieldname = 'data';
+ elseif ischar(pars)
+ fieldname = pars;
+ else
+ fieldname = ['d' reshape(dec2hex(testCase.get_hash(pars)),1,[])];
+ end
+ end
+ function save_test_data(testCase, data, pars)
+ filename = fullfile(testCase.reference_data_dir, testCase.reference_data_file);
+ tmpstr.(testCase.get_fieldname(pars)) = data;
+ save(filename, '-append', '-struct', 'tmpstr');
+ end
+ function verify_test_data(testCase, test_data, ref_data)
+ import matlab.unittest.constraints.IsEqualTo
+ import matlab.unittest.constraints.RelativeTolerance
+ import matlab.unittest.constraints.AbsoluteTolerance
+ theseBounds = RelativeTolerance(testCase.relToll) | AbsoluteTolerance(testCase.absToll);
+ test_data = testCase.sanitize_data(test_data);
+ ref_data = testCase.sanitize_data(ref_data);
+ testCase.verifyThat(test_data, IsEqualTo(ref_data, 'Within', theseBounds));
+ end
+ function generate_or_verify(testCase, spec, pars, extrafields, approxSab, tolSab)
+ if nargin < 5
+ approxSab = false;
+ elseif nargin == 5
+ tolSab = 0.05;
+ end
+ if testCase.generate_reference_data
+ data.input = struct(testCase.swobj);
+ data.spec = {spec.omega spec.Sab};
+ if isfield(spec, 'swConv'); data.spec = [data.spec {spec.swConv}]; end
+ if isfield(spec, 'swInt'); data.spec = [data.spec {spec.swInt}]; end
+ if nargin > 3
+ extras = fieldnames(extrafields);
+ for ii = 1:numel(extras)
+ data.(extras{ii}) = extrafields.(extras{ii});
+ end
+ end
+ testCase.save_test_data(data, pars);
+ else
+ ref_data = testCase.reference_data.(testCase.get_fieldname(pars));
+ test_data.input = struct(testCase.swobj);
+ [spec.omega, ref_data.spec{1}] = testCase.verify_eigval_sort(spec.omega, ref_data.spec{1});
+ test_data.spec = {spec.omega spec.Sab};
+ if isfield(spec, 'swConv'); test_data.spec = [test_data.spec {spec.swConv}]; end
+ if isfield(spec, 'swInt'); test_data.spec = [test_data.spec {spec.swInt}]; end
+ if nargin > 3
+ extras = fieldnames(extrafields);
+ for ii = 1:numel(extras)
+ test_data.(extras{ii}) = extrafields.(extras{ii});
+ end
+ end
+ if any(approxSab)
+ % For the Sab or Sabp tensor, just check that a fraction of entries match
+ test_data.spec{2} = testCase.approxMatrix(spec.Sab, ref_data.spec{2}, tolSab);
+ if numel(test_data.spec) == 4
+ test_data.spec{4} = testCase.approxMatrix(spec.swInt, ref_data.spec{4}, tolSab);
+ end
+ if isfield(test_data, 'Sabp')
+ test_data.Sabp = testCase.approxMatrix(test_data.Sabp, ref_data.Sabp, tolSab);
+ end
+ if isfield(test_data, 'V')
+ test_data.V = testCase.approxMatrix(test_data.V, ref_data.V, tolSab);
+ end
+ end
+ testCase.verify_test_data(test_data, ref_data);
+ end
+ end
+ function generate_or_verify_generic(testCase, data, fieldname)
+ if testCase.generate_reference_data
+ testCase.save_test_data(data, fieldname);
+ else
+ testCase.verify_test_data(data, testCase.reference_data.(fieldname));
+ end
+ end
+ function disable_warnings(testCase, varargin)
+ testCase.cleanup_warnings = [testCase.cleanup_warnings, ...
+ {onCleanup(@(c) cellfun(@(c) warning('on', c), varargin))}];
+ cellfun(@(c) warning('off', c), varargin);
+ end
+ end
+
+end
+
+function out = sanitize(array)
+ out = array;
+ out(abs(out) > 1e8) = 0;
+end
+
+function sanitized = sanitize_struct(in_dat)
+ fnam = fieldnames(in_dat);
+ for ii = 1:numel(fnam)
+ if isnumeric(in_dat.(fnam{ii}))
+ sanitized.(fnam{ii}) = sanitize(in_dat.(fnam{ii}));
+ elseif isstruct(in_dat.(fnam{ii}))
+ sanitized.(fnam{ii}) = sanitize_struct(in_dat.(fnam{ii}));
+ elseif iscell(in_dat.(fnam{ii}))
+ sanitized.(fnam{ii}) = sanitize_cell(in_dat.(fnam{ii}));
+ else
+ sanitized.(fnam{ii}) = in_dat.(fnam{ii});
+ end
+ end
+end
+
+function sanitized = sanitize_cell(in_dat)
+ sanitized = in_dat;
+ for ii = 1:numel(in_dat)
+ if isnumeric(in_dat{ii})
+ sanitized{ii} = sanitize(in_dat{ii});
+ elseif isstruct(in_dat{ii})
+ sanitized{ii} = sanitize_struct(in_dat{ii});
+ elseif iscell(in_dat{ii})
+ sanitized{ii} = sanitize_cell(in_dat{ii});
+ end
+ end
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_KCu3As2O7.m b/+sw_tests/+system_tests/systemtest_spinwave_KCu3As2O7.m
new file mode 100644
index 000000000..52230d31a
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_KCu3As2O7.m
@@ -0,0 +1,55 @@
+classdef systemtest_spinwave_KCu3As2O7 < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = 'systemstest_spinwave_KCu3As2O7.mat';
+ end
+
+ methods (TestMethodSetup)
+ function prepareForRun(testCase)
+ % From Tutorial 18, a distorted kagome lattice from PRB 89, 140412 (2014)
+ % To test incommensurate spin wave calculations and also the structure optimisation routine
+ J = -2; Jp = -1; Jab = 0.75; Ja = -J/.66 - Jab; Jip = 0.01;
+ hK = spinw;
+ hK.genlattice('lat_const',[10.2 5.94 7.81],'angled',[90 117.7 90],'sym','C 2/m');
+ hK.addatom('r',[0 0 0],'S',1/2,'label','MCu2','color','b');
+ hK.addatom('r',[1/4 1/4 0],'S',1/2,'label','MCu2','color','k');
+ hK.gencoupling();
+ hK.addmatrix('label','J-', 'color','r', 'value',J);
+ hK.addmatrix('label','J''','color','g', 'value',Jp);
+ hK.addmatrix('label','Ja', 'color','b', 'value',Ja);
+ hK.addmatrix('label','Jab','color','cyan','value',Jab);
+ hK.addmatrix('label','Jip','color','gray','value',Jip);
+ hK.addcoupling('mat','J-','bond',1);
+ hK.addcoupling('mat','J''','bond',2);
+ hK.addcoupling('mat','Ja','bond',3);
+ hK.addcoupling('mat','Jab','bond',5);
+ hK.addcoupling('mat','Jip','bond',10);
+ testCase.swobj = hK;
+ end
+ end
+
+ methods (Test)
+ function test_KCu3As2O7(testCase)
+ hK = testCase.swobj;
+ hK.genmagstr('mode','helical','n',[0 0 1],'S',[1 0 0]','k',[0.77 0 0.115],'next',[1 1 1]);
+ optpar.func = @gm_planar;
+ optpar.nRun = 10;
+ optpar.xmin = [ zeros(1,6), 0.5 0 0.0, 0 0];
+ optpar.xmax = [2*pi*ones(1,6), 1.0 0 0.5, 0 0];
+ magoptOut = hK.optmagstr(optpar);
+ opt_energy = hK.energy;
+ % Optmised structure with optmagstr not constant enough for check (will vary within a phase factor)
+ % Use optmagsteep structure instead, but check its ground state energy.
+ hK.genmagstr('mode','helical','n',[0 0 1],'S',[1 0 0]','k',[0.77 0 0.115],'next',[1 1 1]);
+ magoptOut = hK.optmagsteep('nRun', 100);
+ hkSpec = hK.spinwave({[0 0 0] [1 0 0] 100},'hermit',false);
+ hkSpec = sw_neutron(hkSpec);
+ hkSpec = sw_egrid(hkSpec,'Evect',linspace(0,5,100),'imagChk',false);
+ % Remove problematic indices
+ hkSpec.Sab(:,:,7,[97 99]) = 0;
+ hkSpec.swInt(7,[97 99]) = 0;
+ testCase.generate_or_verify(hkSpec, {}, struct('opt_energy', opt_energy, 'energy', hK.energy), 'approxSab', 0.5);
+ end
+ end
+
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_af33kagome.m b/+sw_tests/+system_tests/systemtest_spinwave_af33kagome.m
new file mode 100644
index 000000000..f4e7f90e3
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_af33kagome.m
@@ -0,0 +1,37 @@
+classdef systemtest_spinwave_af33kagome < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = 'systemstest_spinwave_af33kagome.mat';
+ end
+
+ methods (TestMethodSetup)
+ function prepareForRun(testCase)
+ % From Tutorial 8, a sqrt(3) x sqrt(3) kagome AFM to test incommensurate calculations
+ AF33kagome = spinw;
+ AF33kagome.genlattice('lat_const',[6 6 40],'angled',[90 90 120],'sym','P -3');
+ AF33kagome.addatom('r',[1/2 0 0],'S', 1,'label','MCu1','color','r');
+ AF33kagome.gencoupling('maxDistance',7);
+ AF33kagome.addmatrix('label','J1','value',1.00,'color','g');
+ AF33kagome.addcoupling('mat','J1','bond',1);
+ testCase.swobj = AF33kagome;
+ testCase.relToll = 0.027;
+ testCase.absToll = 2e-5;
+ end
+ end
+
+ methods (Test)
+ function test_af33kagome(testCase)
+ AF33kagome = testCase.swobj;
+ S0 = [0 0 -1; 1 1 -1; 0 0 0];
+ AF33kagome.genmagstr('mode','helical','k',[-1/3 -1/3 0],'n',[0 0 1],'unit','lu','S',S0,'nExt',[1 1 1]);
+ kag33Spec = AF33kagome.spinwave({[-1/2 0 0] [0 0 0] [1/2 1/2 0] 100},'hermit',false,'saveSabp',true);
+ kag33Spec = sw_egrid(kag33Spec,'component','Sxx+Syy','imagChk',false, 'Evect', linspace(0, 3, 100));
+ % Reduce values of S(q,w) so it falls within tolerance (rather than change tolerance for all values)
+ kag33Spec.swConv = kag33Spec.swConv / 2e5;
+ % Ignores swInt in this case
+ kag33Spec.swInt = 0;
+ testCase.generate_or_verify(kag33Spec, {}, struct('energy', AF33kagome.energy, 'Sabp', kag33Spec.Sabp), 'approxSab', 0.5);
+ end
+ end
+
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_biquadratic.m b/+sw_tests/+system_tests/systemtest_spinwave_biquadratic.m
new file mode 100644
index 000000000..66b4575ee
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_biquadratic.m
@@ -0,0 +1,45 @@
+classdef systemtest_spinwave_biquadratic < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = 'systemstest_spinwave_biquadratic.mat';
+ end
+
+ properties (TestParameter)
+ mex = {0, 1};
+ end
+
+ methods (TestMethodSetup)
+ function prepareForRun(testCase)
+ % From Tutorial 28, to test the biquadratic interactions functionality, theory from PHYSICAL REVIEW B 85, 054409 (2012)
+ S = 5/2; J2 = 5.5; J1 = 5.0; Q = 0.01*J2;
+ fcc = spinw;
+ fcc.genlattice('lat_const',[8 8 8]);
+ fcc.addatom('r',[0 0 0],'S',S);
+ fcc.addatom('r',[1/2 1/2 0],'S',S);
+ fcc.addatom('r',[1/2 0 1/2],'S',S);
+ fcc.addatom('r',[0 1/2 1/2],'S',S);
+ fcc.gencoupling();
+ fcc.addmatrix('label','J1','value',J1,'color','b');
+ fcc.addmatrix('label','J2','value',J2,'color','g');
+ % there is a factor 2 difference between SpinW and paper
+ fcc.addmatrix('label','B','value',-0.5*Q/S^3*2,'color','r');
+ fcc.addcoupling('mat','J1','bond',1);
+ fcc.addcoupling('mat','J2','bond',2);
+ fcc.addcoupling('mat','B','bond',1,'type','biquadratic');
+ fcc.genmagstr('mode','helical','S',[1 -1 -1 -1;1 -1 -1 -1;zeros(1,4)],'k',[1/2 1/2 1/2],'next',[2 2 2],'n',[0 0 1]);
+ testCase.swobj = fcc;
+ end
+ end
+
+ methods (Test)
+ function test_biquadratic(testCase, mex)
+ swpref.setpref('usemex', mex);
+ fcc = testCase.swobj;
+ spec = fcc.spinwave({[1 0 0] [0 0 0] [1/2 1/2 0] [1/2 1/2 1/2] [0 0 0] 50});
+ spec = sw_egrid(spec);
+ spec = sw_omegasum(spec,'zeroint',1e-5,'emptyval',0,'tol',1e-4);
+ testCase.generate_or_verify(spec, {}, struct(), 'approxSab', 0.02);
+ end
+ end
+
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_incommensurate_and_supercell_consistency.m b/+sw_tests/+system_tests/systemtest_spinwave_incommensurate_and_supercell_consistency.m
new file mode 100644
index 000000000..7eed5dce7
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_incommensurate_and_supercell_consistency.m
@@ -0,0 +1,147 @@
+classdef systemtest_spinwave_incommensurate_and_supercell_consistency < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = [];
+ tol = 1e-5;
+ end
+ methods (Static)
+ function om = remove_ghosts(spec, tol)
+ om = spec.omega(find(abs(spec.Sperp) > tol));
+ om = sort(unique(round(om / tol) * tol));
+ end
+ end
+ methods
+ function assert_super_and_incom_consistency(testCase, swobj, ...
+ spec_super, spec_incom, ...
+ ghost_tol)
+ if nargin < 5
+ ghost_tol = testCase.tol;
+ end
+ % test cross-section in q,En bins
+ testCase.verify_test_data(spec_incom.swConv, ...
+ spec_super.swConv)
+ % check correct number of modes (2*nmag)
+ nExt = swobj.mag_str.nExt;
+ n_matom = numel(swobj.matom().S);
+ testCase.assertEqual(size(spec_incom.Sperp, 1), ...
+ 6*n_matom);
+ testCase.assertEqual(size(spec_super.Sperp, 1), ...
+ 2*prod(nExt)*n_matom);
+ testCase.assertEqual(testCase.remove_ghosts(spec_super, ghost_tol), ...
+ testCase.remove_ghosts(spec_incom, ghost_tol));
+ end
+ end
+
+ methods (Test)
+ function test_AFM_kagome(testCase)
+ % setup structure (taken from tutorial 8)
+ AF33kagome = spinw;
+ AF33kagome.genlattice('lat_const',[6 6 40], ...
+ 'angled',[90 90 120], 'sym','P -3')
+ AF33kagome.addatom('r',[1/2 0 0],'S', 1, 'label','MCu1')
+ AF33kagome.gencoupling('maxDistance',7);
+ AF33kagome.addmatrix('label','J1','value',1)
+ AF33kagome.addcoupling('mat','J1','bond',1);
+ % sqrt3 x sqrt(3) magnetic structure
+ k = [-1/3 -1/3 0];
+ n = [0, 0, 1];
+ S = [0 0 -1; 1 1 -1; 0 0 0];
+ % binning for spinwave spectrum
+ qarg = {[-1/2 0 0] [0 0 0] [1/2 1/2 0] 50};
+ evec = 0:0.1:1.5;
+
+ % use structural unit cell with incommensurate k
+ AF33kagome.genmagstr('mode','helical','unit','lu', 'k', k,...
+ 'n',n, 'S', S, 'nExt',[1 1 1]);
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ spec_incom = AF33kagome.spinwave(qarg, 'hermit', true);
+ spec_incom = sw_egrid(spec_incom, 'component','Sperp', 'Evect', evec, ...
+ 'zeroEnergyTol', 1e-2);
+ % use supercell k=0 structure
+ AF33kagome.genmagstr('mode','helical','unit','lu', 'k', k,...
+ 'n',n, 'S', S, 'nExt', [3,3,1]);
+
+ spec_super = AF33kagome.spinwave(qarg, 'hermit', true);
+ spec_super = sw_egrid(spec_super, 'component','Sperp', 'Evect', evec);
+
+ testCase.assert_super_and_incom_consistency(AF33kagome, ...
+ spec_super, ...
+ spec_incom, 5e-2);
+ end
+
+ function test_two_matom_per_unit_cell(testCase)
+ % setup structure (taken from tutorial 19)
+ FeCuChain = spinw;
+ FeCuChain.genlattice('lat_const',[3 8 4],'sym','P 1')
+ FeCuChain.addatom('label','MCu2','r',[0 0 0])
+ FeCuChain.addatom('label','MFe2','r',[0 1/2 0])
+
+ FeCuChain.gencoupling
+ FeCuChain.addmatrix('label','J_{Cu-Cu}','value',1)
+ FeCuChain.addmatrix('label','J_{Fe-Fe}','value',1)
+ FeCuChain.addmatrix('label','J_{Cu-Fe}','value',-0.1)
+
+ FeCuChain.addcoupling('mat','J_{Cu-Cu}','bond',1)
+ FeCuChain.addcoupling('mat','J_{Fe-Fe}','bond',2)
+ FeCuChain.addcoupling('mat','J_{Cu-Fe}','bond',[4 5])
+ % AFM structure
+ k = [1/2, 0, 0];
+ S = [0 0;1 1;0 0];
+ % binning for spinwave spectrum
+ qarg = {[0 0 0] [0, 0.5, 0] 5};
+ evec = 0:0.5:5;
+
+ % use structural unit cell with incommensurate k
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian', ...
+ 'spinw:magstr:NotExact', ...
+ 'spinw:spinwave:Twokm');
+ FeCuChain.genmagstr('mode','helical','k', k,...
+ 'S', S, 'nExt',[1 1 1]);
+ spec_incom = FeCuChain.spinwave(qarg, 'hermit', true);
+ spec_incom = sw_egrid(spec_incom, 'component','Sperp', 'Evect',evec);
+ % use supercell k=0 structure
+ FeCuChain.genmagstr('mode','helical','k', k,...
+ 'S', S, 'nExt', [2,1,1]);
+ spec_super = FeCuChain.spinwave(qarg, 'hermit', true);
+ spec_super = sw_egrid(spec_super, 'component','Sperp', 'Evect',evec);
+
+ testCase.assert_super_and_incom_consistency(FeCuChain, ...
+ spec_super, ...
+ spec_incom);
+ end
+
+ function test_two_sym_equiv_matoms_per_unit_cell(testCase)
+ sw = spinw;
+ sw.genlattice('lat_const',[4,5,12],'sym','I m m m')
+ sw.addatom('S', 1, 'r',[0 0 0]);
+ sw.addmatrix('label', 'J', 'value', 1);
+ sw.addmatrix('label', 'A', 'value', diag([0 0 -0.1]))
+ sw.gencoupling;
+ sw.addcoupling('mat','J','bond', 1);
+ sw.addaniso('A');
+ % AFM structure
+ S = [0; 0; 1];
+ k = [0.5, 0 0];
+ % binning for spinwave spectrum
+ qarg = {[0 0 0] [1/2 0 0] 5};
+ evec = 0:0.5:1.5;
+
+ % use structural unit cell with incommensurate k
+ testCase.disable_warnings('spinw:genmagstr:SnParallel');
+ sw.genmagstr('mode','helical','k', k,...
+ 'S', S, 'nExt',[1 1 1]);
+ spec_incom = sw.spinwave(qarg, 'hermit', true);
+ spec_incom = sw_egrid(spec_incom, 'component','Sperp', 'Evect', evec);
+ % use supercell k=0 structure
+ sw.genmagstr('mode','helical','k', k,...
+ 'S', S, 'nExt', [2 1 1]);
+ spec_super = sw.spinwave(qarg, 'hermit', true);
+ spec_super = sw_egrid(spec_super, 'component','Sperp', 'Evect', evec);
+
+ testCase.assert_super_and_incom_consistency(sw, ...
+ spec_super, ...
+ spec_incom);
+ end
+ end
+
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_pcsmo.m b/+sw_tests/+system_tests/systemtest_spinwave_pcsmo.m
new file mode 100644
index 000000000..6d92c9c5b
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_pcsmo.m
@@ -0,0 +1,103 @@
+classdef systemtest_spinwave_pcsmo < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = 'systemstest_spinwave_pcsmo.mat';
+ end
+
+ properties (TestParameter)
+ usehorace = {false true};
+ end
+
+ methods (TestMethodSetup)
+ function prepareForRun(testCase)
+ % Runs the Goodenough model for (Pr,Ca)SrMn2O7, based on PRL, 109, 237202 (2012)
+ JF1 = -11.39; JA = 1.5; JF2 = -1.35; JF3 = 1.5; Jperp = 0.88; D = 0.074;
+ lat = [5.408 5.4599 19.266]; alf = [90 90 90];
+ SM4 = 7/4; % Spin length for Mn4+
+ SM3 = 7/4; % Spin length for Mn3+
+ pcsmo = spinw;
+ pcsmo.genlattice('lat_const', lat.*[2 2 1], 'angled', alf, 'sym', 'x,y+1/2,-z');
+ [~,ffn3] = sw_mff('MMn3');
+ [~,ffn4] = sw_mff('MMn4');
+ myaddatom3 = @(x,y,z) pcsmo.addatom('label', x, 'r', y, 'S', SM3, 'color', z, ...
+ 'formfactn', ffn3, 'formfactx', 'MMn3', 'Z', 25, 'b', sw_nb('MMn3'));
+ myaddatom4 = @(x,y,z) pcsmo.addatom('label', x, 'r', y, 'S', SM4, 'color', z, ...
+ 'formfactn', ffn4, 'formfactx', 'MMn4', 'Z', 25, 'b', sw_nb('MMn4'));
+ myaddatom4('Mn4-up', [0 0 0.1], 'gold');
+ myaddatom4('Mn4-up', [0.5 0.5 0.1], 'gold');
+ myaddatom4('Mn4-dn', [0 0.5 0.1], 'gold');
+ myaddatom4('Mn4-dn', [0.5 0 0.1], 'gold');
+ myaddatom3('Mn3-up', [0.25 0.75 0.1], 'black');
+ myaddatom3('Mn3-up', [0.75 0.75 0.1], 'black');
+ myaddatom3('Mn3-dn', [0.25 0.25 0.1], 'black');
+ myaddatom3('Mn3-dn', [0.75 0.25 0.1], 'black');
+ % Generate the CE magnetic structure
+ S0 = [0; 1; 0];
+ spin_up = find(~cellfun(@isempty, strfind(pcsmo.table('matom').matom, 'up')));
+ spin_dn = find(~cellfun(@isempty, strfind(pcsmo.table('matom').matom, 'dn')));
+ SS = zeros(3, 16);
+ SS(:, spin_up) = repmat(S0, 1, numel(spin_up));
+ SS(:, spin_dn) = repmat(-S0, 1, numel(spin_dn));
+ pcsmo.genmagstr('mode', 'direct', 'S', SS)
+ % Generate the exchange interactions
+ pcsmo.gencoupling('forceNoSym', true)
+ pcsmo.addmatrix('label', 'JF1', 'value', JF1, 'color', 'green');
+ pcsmo.addmatrix('label', 'JA', 'value', JA, 'color', 'yellow');
+ pcsmo.addmatrix('label', 'JF2', 'value', JF2, 'color', 'white');
+ pcsmo.addmatrix('label', 'JF3', 'value', JF3, 'color', 'red');
+ pcsmo.addmatrix('label', 'Jperp', 'value', Jperp, 'color', 'blue');
+ pcsmo.addmatrix('label', 'D', 'value', diag([0 0 D]), 'color', 'white');
+ % The zig-zag chains couple Mn3-Mn4 with same spin.
+ pcsmo.addcoupling('mat', 'JF1', 'bond', 1, 'atom', {'Mn3-up', 'Mn4-up'})
+ pcsmo.addcoupling('mat', 'JF1', 'bond', 1, 'atom', {'Mn3-dn', 'Mn4-dn'})
+ % And vice-versa for the inter-chain interaction
+ pcsmo.addcoupling('mat', 'JA', 'bond', 1, 'atom', {'Mn3-up', 'Mn4-dn'})
+ pcsmo.addcoupling('mat', 'JA', 'bond', 1, 'atom', {'Mn3-dn', 'Mn4-up'})
+ pcsmo.addcoupling('mat', 'Jperp', 'bond', 2)
+ % JF3 couples Mn3 within the same zig-zag (same spin)
+ pcsmo.addcoupling('mat', 'JF3', 'bond', 3, 'atom', 'Mn3-up')
+ pcsmo.addcoupling('mat', 'JF3', 'bond', 3, 'atom', 'Mn3-dn')
+ % Find indexes of the Mn4+ atoms which have a=0.5:
+ idmid = find((~cellfun(@isempty, strfind(pcsmo.table('matom').matom, 'Mn4'))) ...
+ .* (pcsmo.table('matom').pos(:,1)==0.5));
+ bond8 = pcsmo.table('bond', 8);
+ % Finds the bonds which start on one of these atoms and goes along +b
+ idstart = find(ismember(bond8.idx1, idmid) .* (bond8.dr(:,2)>0));
+ % Finds the bonds which ends on one of these atoms and goes along -b
+ idend = find(ismember(bond8.idx2, idmid) .* (bond8.dr(:,2)<0));
+ pcsmo.addcoupling('mat', 'JF2', 'bond', 8, 'subIdx', [idstart; idend]')
+ pcsmo.addaniso('D')
+ % Define twins
+ pcsmo.addtwin('rotC', [0 1 0; 1 0 0; 0 0 1]);
+ pcsmo.twin.vol = [0.5 0.5];
+ pcsmo.unit.qmat = diag([2 2 1]);
+ % Assign to property
+ testCase.swobj = pcsmo;
+ testCase.absToll = 2e-6;
+ end
+ end
+
+ methods (Test)
+ function test_pcsmo(testCase, usehorace)
+ % (002) is problematic - Goldstone mode gives indexing error in different Matlab versions
+ qln = {[0 0 0] [1.98 0 0] 50};
+ if usehorace
+ hkl = sw_qscan(qln);
+ % spinwavefast doesn't work for twins yet
+ testCase.swobj.notwin();
+ [w0, s0] = testCase.swobj.horace(hkl(:,1), hkl(:,2), hkl(:,3), 'dE', 2, 'useFast', false);
+ [w1, s1] = testCase.swobj.horace(hkl(:,1), hkl(:,2), hkl(:,3), 'dE', 2, 'useFast', true);
+ testCase.verify_test_data({w0(1:numel(w1)) s0(1:numel(w1))}, {w1 s1});
+ testCase.generate_or_verify_generic({w0 s0}, 'data_horace');
+ else
+ testCase.swobj.twin.rotc(3,3,2) = 0; % Changed in code #85, but not in test .mat file
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ spec = testCase.swobj.spinwave(qln, 'formfact', true, 'saveV', true, 'saveH', true, 'optmem', 2);
+ spec = sw_egrid(spec, 'Evect', linspace(0, 100, 200));
+ spec = sw_neutron(spec);
+ testCase.generate_or_verify(spec, {}, struct('V', spec.V, 'H', spec.H), 'approxSab', 0.11);
+ end
+ end
+ end
+
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_symbolic_nips.m b/+sw_tests/+system_tests/systemtest_spinwave_symbolic_nips.m
new file mode 100644
index 000000000..a84d8c163
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_symbolic_nips.m
@@ -0,0 +1,116 @@
+classdef (TestTags = {'Symbolic'}) systemtest_spinwave_symbolic_nips < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = '';
+ swobj_nn = [];
+ symspec = [];
+ end
+
+ properties (TestParameter)
+ test_spectra_function_name = {'sw_neutron', 'sw_egrid', 'sw_instrument', ...
+ 'sw_omegasum', 'sw_plotspec', 'sw_tofres'};
+ end
+
+ methods (TestClassSetup)
+ function prepareForRun(testCase)
+ % Symbolic calculation, based on "Magnetic dynamics of NiPS3", A.R.Wildes et al., Phys. Rev. B in press
+ nips = spinw();
+ nips.genlattice('lat_const', [5.812, 10.222, 6.658], 'angled', [90, 107.16, 90], 'sym', 12);
+ nips.addatom('r', [0 0.333 0], 'S', 1, 'label', 'MNi2');
+ nips.gencoupling();
+ nips.addmatrix('label', 'J1', 'mat', 1);
+ nips.addcoupling('mat', 'J1', 'bond', 1);
+ nips.addcoupling('mat', 'J1', 'bond', 2);
+ nips.genmagstr('mode', 'direct', 'k', [0 1 0], 'S', [1 0 0; -1 0 0; -1 0 0; 1 0 0]');
+ % The full system is too complicated to determine the general dispersion
+ % It will run for several hours and then run out of memory.
+ % Instead for some tests we simplify it by including only nearest neighbour interactions
+ % to be able to run through the full calculation.
+ testCase.swobj_nn = nips.copy();
+ testCase.swobj_nn.symbolic(true);
+ testCase.symspec = testCase.swobj_nn.spinwavesym();
+ nips.addmatrix('label', 'J2', 'mat', 1);
+ nips.addcoupling('mat', 'J2', 'bond', 3);
+ nips.addcoupling('mat', 'J2', 'bond', 4);
+ nips.addmatrix('label', 'J3', 'mat', 1);
+ nips.addcoupling('mat', 'J3', 'bond', 7);
+ nips.addcoupling('mat', 'J3', 'bond', 8);
+ nips.addmatrix('label', 'Jp', 'mat', 1);
+ nips.addcoupling('mat', 'Jp', 'bond', 5);
+ nips.addcoupling('mat', 'Jp', 'bond', 6);
+ nips.addmatrix('mat', diag([0 0 1]), 'label','D');
+ nips.addaniso('D');
+ nips.symbolic(true);
+ testCase.swobj = nips;
+ end
+ end
+
+ methods (Test)
+ function test_symbolic_hamiltonian(testCase)
+ % Calculates the symbolic Hamiltonian and check it is Hermitian
+ nips = testCase.swobj;
+ % Specify 'eig', false to force not calculate dispersion (see general_hkl test below)
+ symSpec = nips.spinwavesym('eig', false);
+ ham = symSpec.ham;
+ % Checks hamiltonian is hermitian
+ import matlab.unittest.constraints.IsEqualTo
+ testCase.verifyThat(simplify(ham - conj(transpose(ham))), IsEqualTo(sym(zeros(8))));
+ end
+ function test_symbolic_hamiltonian_white(testCase)
+ % Calculates the symbolic Hamiltonian and check it obeys definition
+ % of White et al., PR 139 A450 (1965)
+ nips = testCase.swobj;
+ symSpec = nips.spinwavesym('hkl', [0; 0; 0]);
+ ham = symSpec.ham;
+ g = sym(diag([ones(1, 4) -ones(1,4)]));
+ Echeck = simplify(eig(g * symSpec.ham));
+ import matlab.unittest.constraints.IsEqualTo
+ testCase.verifyThat(simplify(symSpec.omega), IsEqualTo(Echeck));
+ end
+ function test_symbolic_hamiltonian_squared(testCase)
+ % Calculates the symbolic Hamiltonian and its squared eigenvalues
+ % agree with the block determinant identity
+ nips = testCase.swobj;
+ symSpec = nips.spinwavesym('hkl', [0.5; 0.5; 0.5]);
+ g = sym(diag([ones(1, 4) -ones(1,4)]));
+ hamsq = (g * symSpec.ham)^2;
+ % Split squared hamiltonian into blocks - hamsq = [A B; C D]
+ A = hamsq(1:4, 1:4);
+ B = hamsq(1:4, 5:8);
+ C = hamsq(5:8, 1:4);
+ D = hamsq(5:8, 5:8);
+ % Now the normal hamiltonian has the form: ham = [U V; V' U]
+ % so when squared we get hamsq = [U^2+V^2 2*U*V; 2*U*V U^2+V^2] - e.g. [A B; B A]
+ % This would satisfy the block determinant identity
+ % det([A B; B A]) = det(A - B) * det(A + B)
+ % So the eigenvalues of the full matrix [A B; B A] are those of [A-B] and [A+B]
+ % (From wikipedia: https://en.wikipedia.org/wiki/Block_matrix#Block_matrix_determinant)
+ % But actually for spin wave Hamiltonians, a stricter criteria applies, with B = -B'
+ % so the eigenvalues of (A+B) is the same as (A-B)
+ % These eigenvalues are the squared magnon frequencies here, and the positive (negative)
+ % roots represents magnon creation (anihilation) modes.
+ import matlab.unittest.constraints.IsEqualTo
+ testCase.verifyThat(simplify(A), IsEqualTo(simplify(D)));
+ testCase.verifyThat(simplify(B), IsEqualTo(simplify(C)));
+ E1 = sort(simplify(eig(A + B)));
+ E2 = sort(simplify(eig(A - B)));
+ testCase.verifyThat(E1, IsEqualTo(E2));
+ omega = sort(simplify(symSpec.omega.^2));
+ testCase.verifyThat(unique(omega), IsEqualTo(unique(E1)));
+ end
+ function test_symbolic_general_hkl(testCase)
+ % Test we can run the full spin wave calc outputing the spin-spin correlation matrix Sab
+ variables = [sym('J1'), sym('h'), sym('k')]; % No 'l' because no out-of-plane coupling
+ import matlab.unittest.constraints.IsEqualTo
+ testCase.verifyThat(sort(symvar(testCase.symspec.omega)), IsEqualTo(variables));
+ end
+ function test_symbolic_spectra(testCase, test_spectra_function_name)
+ % Tests that running standard functions with symbolic spectra gives error
+ test_fun = eval(['@' test_spectra_function_name]);
+ testCase.verifyError( ...
+ @() feval(test_spectra_function_name, testCase.symspec), ...
+ [test_spectra_function_name ':SymbolicInput']);
+ end
+ end
+
+end
diff --git a/+sw_tests/+system_tests/systemtest_spinwave_yb2ti2o7.m b/+sw_tests/+system_tests/systemtest_spinwave_yb2ti2o7.m
new file mode 100644
index 000000000..9bdae8274
--- /dev/null
+++ b/+sw_tests/+system_tests/systemtest_spinwave_yb2ti2o7.m
@@ -0,0 +1,64 @@
+classdef systemtest_spinwave_yb2ti2o7 < sw_tests.system_tests.systemtest_spinwave
+
+ properties
+ reference_data_file = 'systemstest_spinwave_yb2ti2o7.mat';
+ end
+
+ properties (TestParameter)
+ B = {2 5};
+ Q = {{[-0.5 -0.5 -0.5] [2 2 2]} {[1 1 -2] [1 1 1.5]} {[2 2 -2] [2 2 1.5]} {[-0.5 -0.5 0] [2.5 2.5 0]} {[0 0 1] [2.3 2.3 1]}};
+ end
+
+ methods (TestMethodSetup)
+ function prepareForRun(testCase)
+ % From Tutorial 20, Yb2Ti2O7, based on PRX 1, 021002 (2011)
+ % First set up the crystal structure
+ symStr = '-z, y+3/4, x+3/4; z+3/4, -y, x+3/4; z+3/4, y+3/4, -x; y+3/4, x+3/4, -z; x+3/4, -z, y+3/4; -z, x+3/4, y+3/4';
+ yto = spinw;
+ a = 10.0307;
+ yto.genlattice('lat_const',[a a a],'angled',[90 90 90],'sym',symStr,'label','F d -3 m Z')
+ yto.addatom('label','Yb3+','r',[1/2 1/2 1/2],'S',1/2)
+ % We generate the list of bonds.
+ yto.gencoupling
+ % We create two 3x3 matrix, one for the first neighbor anisotropic exchange
+ % and one for the anisotropic g-tensor. And assign them appropriately.
+ yto.addmatrix('label', 'J1', 'color', [255 0 0], 'value', 1)
+ yto.addmatrix('label', 'g0', 'color', [0 0 255], 'value', -0.84*ones(3)+4.32*eye(3));
+ yto.addcoupling('mat', 'J1', 'bond', 1)
+ yto.addg('g0')
+ % Sets the correct values for the matrix elements of J1
+ J1 = -0.09; J2 = -0.22; J3 = -0.29; J4 = 0.01;
+ yto.setmatrix('mat','J1','pref',[J1 J3 J2 -J4]);
+ testCase.swobj = yto;
+ end
+ end
+
+ methods (Test)
+ function test_yto(testCase, B, Q)
+ n = [1 -1 0];
+ % set magnetic field
+ testCase.swobj.field(n/norm(n)*B);
+ % create fully polarised magnetic structure along the field direction
+ testCase.swobj.genmagstr('S',n','mode','helical');
+ % find best structure using steepest descendend
+ testCase.swobj.optmagsteep;
+ ytoSpec = testCase.swobj.spinwave([Q {50}],'gtensor',true);
+ ytoSpec = sw_neutron(ytoSpec);
+ % bin the spectrum in energy
+ ytoSpec = sw_egrid(ytoSpec,'Evect',linspace(0,2,100),'component','Sperp');
+ %figure; sw_plotspec(ytoSpec,'axLim',[0 0.5],'mode',3,'dE',0.09,'colorbar',false,'legend',false); title(''); caxis([0 60]); colormap(jet);
+ testCase.generate_or_verify(ytoSpec, {B Q}, struct(), 'approxSab', 0.5);
+ end
+ function test_yto_twin(testCase)
+ % Adds a twin and runs test with single field/Q
+ testCase.swobj.addtwin('axis', [1 -1 0], 'phid', 90);
+ testCase.test_yto(4, {[-0.5 -0.5 -0.5] [2 2 2]});
+ end
+ function test_yto_mex(testCase, B)
+ % Tests mex with just first Q setting
+ swpref.setpref('usemex', 1)
+ testCase.test_yto(B, {[-0.5 -0.5 -0.5] [2 2 2]});
+ end
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_mmat.m b/+sw_tests/+unit_tests/unittest_mmat.m
new file mode 100644
index 000000000..2cccde801
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_mmat.m
@@ -0,0 +1,26 @@
+classdef unittest_mmat < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for sw_neutron.m
+
+ properties
+ swobj = [];
+ end
+
+ methods (Test)
+ function test_mmat_branch(testCase)
+ matA = rand(15,10);
+ matB = rand(10,5,100);
+ % Run it normally
+ mat0 = mmat(matA, matB);
+ % Force sw_freemem to return only 100 bytes available
+ mock_freemem = sw_tests.utilities.mock_function('sw_freemem', 100);
+ % Checks that with low memory the routine actually does not call bsxfun
+ mock_bsxfun = sw_tests.utilities.mock_function('bsxfunsym');
+ mat1 = mmat(matA, matB);
+ testCase.verify_val(mat0, mat1, 'rel_tol', 0.01, 'abs_tol', 1e-6);
+ testCase.assertEqual(mock_freemem.n_calls, 1);
+ testCase.assertEqual(mock_bsxfun.n_calls, 0);
+ end
+ end
+
+end
+
diff --git a/+sw_tests/+unit_tests/unittest_ndbase_cost_function_wrapper.m b/+sw_tests/+unit_tests/unittest_ndbase_cost_function_wrapper.m
new file mode 100644
index 000000000..56322cdb0
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_ndbase_cost_function_wrapper.m
@@ -0,0 +1,166 @@
+classdef unittest_ndbase_cost_function_wrapper < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for ndbase optimisers, atm only simplex passes
+ % these tests
+
+ properties
+ fcost = @(p) (p(1)-1)^2 + (p(2)-2)^2
+ params = [2,4]
+ end
+
+ properties (TestParameter)
+ bound_param_name = {'lb', 'ub'}
+ no_lower_bound = {[], [-inf, -inf], [NaN, -inf]};
+ no_upper_bound = {[], [inf, inf], [inf, NaN]};
+ errors = {ones(1,3), [], zeros(1,3), 'NoField'}
+ end
+
+ methods
+ function [pfree, pbound, cost_val] = get_pars_and_cost_val(testCase, cost_func_wrap)
+ pfree = cost_func_wrap.get_free_parameters(testCase.params);
+ pbound = cost_func_wrap.get_bound_parameters(pfree);
+ cost_val = cost_func_wrap.eval_cost_function(pfree);
+ end
+ end
+
+ methods (Test)
+
+ function test_init_with_fcost_no_bounds(testCase)
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, testCase.params);
+ testCase.verify_val(pbound, testCase.params);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ end
+
+ function test_init_with_fcost_no_bounds_name_value_passed(testCase, no_lower_bound, no_upper_bound)
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', no_lower_bound, 'ub', no_upper_bound);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, testCase.params);
+ testCase.verify_val(pbound, testCase.params);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ end
+
+ function test_init_with_fcost_lower_bound_only(testCase)
+ % note first param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', [3, 1]);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, [1.1180, 3.8730], 'abs_tol', 1e-4);
+ testCase.verify_val(pbound, [3.5, 4], 'abs_tol', 1e-4);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ end
+
+ function test_init_with_fcost_upper_bound_only(testCase)
+ % note second param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'ub', [3, 1]);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, [1.7320, 1.1180], 'abs_tol', 1e-4);
+ testCase.verify_val(pbound, [2, 0.5], 'abs_tol', 1e-4);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ end
+
+ function test_init_with_fcost_both_bounds(testCase)
+ % note second param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', [1, 2], 'ub', [3, 2.5]);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, [0, 0], 'abs_tol', 1e-4);
+ testCase.verify_val(pbound, [2, 2.25], 'abs_tol', 1e-4);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ end
+
+ function test_init_with_fcost_both_bounds_with_fixed_param(testCase)
+ % note second param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', [1, 2.5], 'ub', [3, 2.5]);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, 0, 'abs_tol', 1e-4); % only first param free
+ testCase.verify_val(pbound, [2, 2.5], 'abs_tol', 1e-4);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ testCase.verify_val(cost_func_wrap.ifixed, 2);
+ testCase.verify_val(cost_func_wrap.ifree, 1);
+ testCase.verify_val(cost_func_wrap.pars_fixed, 2.5);
+ end
+
+
+ function test_init_with_fcost_both_bounds_fixed_invalid_param_using_ifix(testCase)
+ % note second param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', [1, 2], 'ub', [3, 2.5], 'ifix', [2]);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, 0, 'abs_tol', 1e-4); % only first param free
+ testCase.verify_val(pbound, [2, 2.25], 'abs_tol', 1e-4);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ testCase.verify_val(cost_func_wrap.ifixed, 2);
+ testCase.verify_val(cost_func_wrap.ifree, 1);
+ testCase.verify_val(cost_func_wrap.pars_fixed, 2.25);
+ end
+
+ function test_init_with_fcost_both_bounds_fixed_param_using_ifix(testCase)
+ % note second param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', [1, 2], 'ub', [3, 6], 'ifix', [2]);
+ [pfree, pbound, ~] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, 0, 'abs_tol', 1e-4); % only first param free
+ testCase.verify_val(pbound, testCase.params, 'abs_tol', 1e-4);
+ testCase.verify_val(cost_func_wrap.pars_fixed, testCase.params(2));
+ end
+
+ function test_init_with_fcost_no_bounds_with_fixed_param_using_ifix(testCase)
+ % note second param outside bounds
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'ifix', [2]);
+ [pfree, pbound, ~] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, testCase.params(1), 'abs_tol', 1e-4); % only first param free
+ testCase.verify_val(pbound, testCase.params, 'abs_tol', 1e-4);
+ testCase.verify_val(cost_func_wrap.ifixed, 2);
+ testCase.verify_val(cost_func_wrap.ifree, 1);
+ testCase.verify_val(cost_func_wrap.pars_fixed, testCase.params(2));
+ end
+
+ function test_init_with_data(testCase, errors)
+ % all errors passed lead to unweighted residuals (either as
+ % explicitly ones or the default weights if invalid errors)
+ if ischar(errors) && errors == "NoField"
+ dat = struct('x', 1:3);
+ else
+ dat = struct('x', 1:3, 'e', errors);
+ end
+ dat.y = polyval(testCase.params, dat.x);
+ cost_func_wrap = ndbase.cost_function_wrapper(@(x, p) polyval(p, x), testCase.params, 'data', dat);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(pfree, testCase.params, 'abs_tol', 1e-4);
+ testCase.verify_val(pbound, testCase.params, 'abs_tol', 1e-4);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 1e-4);
+ end
+
+ function test_wrong_size_bounds(testCase, bound_param_name)
+ testCase.verifyError(...
+ @() ndbase.cost_function_wrapper(testCase.fcost, testCase.params, bound_param_name, ones(3)), ...
+ 'ndbase:cost_function_wrapper:WrongInput');
+ end
+
+ function test_incompatible_bounds(testCase)
+ testCase.verifyError(...
+ @() ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'lb', [1,1,], 'ub', [0,0]), ...
+ 'ndbase:cost_function_wrapper:WrongInput');
+ end
+
+ function test_init_with_resid_handle(testCase)
+ x = 1:3;
+ y = polyval(testCase.params, x);
+ cost_func_wrap = ndbase.cost_function_wrapper(@(p) y - polyval(p, x), testCase.params, 'resid_handle', true);
+ [~, ~, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 1e-4);
+ end
+
+ function test_init_with_fcost_all_params_fixed(testCase)
+ % note second param outside bounds
+ ifixed = 1:2;
+ cost_func_wrap = ndbase.cost_function_wrapper(testCase.fcost, testCase.params, 'ifix', ifixed);
+ [pfree, pbound, cost_val] = testCase.get_pars_and_cost_val(cost_func_wrap);
+ testCase.verifyEmpty(pfree);
+ testCase.verify_val(pbound, testCase.params);
+ testCase.verify_val(cost_val, testCase.fcost(pbound), 'abs_tol', 1e-4);
+ testCase.verify_val(cost_func_wrap.ifixed, ifixed);
+ testCase.verifyEmpty(cost_func_wrap.ifree);
+ testCase.verify_val(cost_func_wrap.pars_fixed, testCase.params);
+ end
+
+
+ end
+end
\ No newline at end of file
diff --git a/+sw_tests/+unit_tests/unittest_ndbase_estimate_hessian.m b/+sw_tests/+unit_tests/unittest_ndbase_estimate_hessian.m
new file mode 100644
index 000000000..fbdb40884
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_ndbase_estimate_hessian.m
@@ -0,0 +1,73 @@
+classdef unittest_ndbase_estimate_hessian < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for @spinw/spinwave.m
+
+ properties
+ fcost = @(pars) (pars(1)+5*pars(2))^2 + (pars(1)*pars(2))^2;
+ expected_hessian = @(pars) [2+2*pars(2)^2, 4*pars(1)*pars(2)+10;
+ 4*pars(1)*pars(2)+10, 50+2*pars(1)^2];
+ minimum = [0 0];
+ end
+
+ methods (Test)
+ function test_wrong_number_absolute_steps(testCase)
+ testCase.verifyError(...
+ @() ndbase.estimate_hessian(testCase.fcost, testCase.minimum, 'step', [1,1,1]), ...
+ 'ndbase:estimate_hessian');
+ end
+ function test_automatic_step_size(testCase)
+ out = ndbase.estimate_hessian(testCase.fcost, testCase.minimum);
+ testCase.verify_val(out, testCase.expected_hessian(testCase.minimum), 'abs_tol', 1e-6);
+ end
+ function test_outputs_varargout_struct(testCase)
+ pars = [1,0];
+ [out, stats] = ndbase.estimate_hessian(testCase.fcost, pars);
+ testCase.verify_val(out, testCase.expected_hessian(pars), 'abs_tol', 1e-6);
+ testCase.verify_val(stats.cost_val, testCase.fcost(pars));
+ testCase.verify_val(stats.step_size, [1,1]*1.49e-8, 'abs_tol', 1e-10);
+ end
+ function test_absolute_step_size(testCase)
+ pars = [1,0];
+ out = ndbase.estimate_hessian(testCase.fcost, pars, 'step', pars*1e-4);
+ % automatic step size assumes
+ testCase.verify_val(out, testCase.expected_hessian(pars), 'abs_tol', 1e-4);
+ end
+ function test_relative_step_size(testCase)
+ pars = [1,0];
+ out = ndbase.estimate_hessian(testCase.fcost, pars, 'step', 1e-4);
+ testCase.verify_val(out, testCase.expected_hessian(pars), 'abs_tol', 1e-4);
+ end
+ function test_absolute_step_size_negative(testCase)
+ out = ndbase.estimate_hessian(testCase.fcost, testCase.minimum, 'step', [-1e-5, -1e-5]);
+ testCase.verify_val(out, testCase.expected_hessian(testCase.minimum), 'abs_tol', 1e-6);
+ end
+ function test_warning_if_automatic_step_size_fails(testCase)
+ % use function with shallow stationary point
+ testCase.verifyWarning(...
+ @() ndbase.estimate_hessian(@(pars) (pars(1)^3)*(pars(2)^3), ...
+ [0,0]), ...
+ 'ndbase:estimate_hessian');
+ end
+ function test_niter(testCase)
+ % set niter small so that appropriate step size not reached
+ testCase.verifyWarning(...
+ @() ndbase.estimate_hessian(testCase.fcost, testCase.minimum, ...
+ 'niter', 1), ...
+ 'ndbase:estimate_hessian');
+ end
+ function test_cost_tol(testCase)
+ % set cost_tol so high the appropriate step size not reached
+ testCase.verifyWarning(...
+ @() ndbase.estimate_hessian(testCase.fcost, testCase.minimum, ...
+ 'cost_tol', 1e3), ...
+ 'ndbase:estimate_hessian');
+ end
+ function test_set_ivary(testCase)
+ % use function with shallow stationary point
+ fcost = @(pars) pars(1)^2 + testCase.fcost(pars(2:end));
+ pars = [3, testCase.minimum];
+ out = ndbase.estimate_hessian(fcost, pars, 'ivary',2:3);
+ testCase.verify_val(out, testCase.expected_hessian(testCase.minimum), 'abs_tol', 1e-6);
+ end
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_ndbase_optimisers.m b/+sw_tests/+unit_tests/unittest_ndbase_optimisers.m
new file mode 100644
index 000000000..89974dfd9
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_ndbase_optimisers.m
@@ -0,0 +1,107 @@
+classdef unittest_ndbase_optimisers < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for ndbase optimisers using bounded parameter
+ % transformations.
+
+ properties
+ rosenbrock = @(x) (1-x(1)).^2 + 100*(x(2) - x(1).^2).^2;
+ rosenbrock_minimum = [1, 1];
+ end
+
+ properties (TestParameter)
+ optimiser = {@ndbase.simplex, @ndbase.lm4};
+ poly_func = {@(x, p) polyval(p, x), '@(x, p) polyval(p, x)'}
+ end
+
+ methods (Test)
+ function test_optimise_data_struct(testCase, optimiser, poly_func)
+ linear_pars = [2, 1];
+ dat = struct('x', 1:3, 'e', ones(1,3));
+ dat.y = polyval(linear_pars, dat.x);
+ [pars_fit, cost_val, ~] = optimiser(dat, poly_func, [-1,-1]);
+ testCase.verify_val(pars_fit, linear_pars, 'abs_tol', 2e-4);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 5e-7);
+ end
+
+ function test_optimise_residual_array_lm(testCase, optimiser)
+ linear_pars = [2, 1];
+ x = 1:3;
+ y = polyval(linear_pars, x);
+ [pars_fit, cost_val, ~] = optimiser([], @(p) y - polyval(p, x), [-1,-1], 'resid_handle', true);
+ testCase.verify_val(pars_fit, linear_pars, 'abs_tol', 2e-4);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 5e-7);
+ end
+
+ function test_optimise_rosen_free(testCase, optimiser)
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1]);
+ testCase.verify_val(pars_fit, testCase.rosenbrock_minimum, 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 2e-7);
+ end
+
+ function test_optimise_rosen_lower_bound_minimum_accessible(testCase, optimiser)
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'lb', [-2, -2]);
+ testCase.verify_val(pars_fit, testCase.rosenbrock_minimum, 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_lower_bound_minimum_not_accessible(testCase, optimiser)
+ % note intital guess is outside bounds
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'lb', [-inf, 2]);
+ testCase.verify_val(pars_fit, [-1.411, 2], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 5.821, 'abs_tol', 1e-3);
+ end
+
+ function test_optimise_rosen_upper_bound_minimum_accessible(testCase, optimiser)
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'ub', [2, 2]);
+ testCase.verify_val(pars_fit, testCase.rosenbrock_minimum, 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_upper_bound_minimum_not_accessible(testCase, optimiser)
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'ub', [0, inf]);
+ testCase.verify_val(pars_fit, [0, 0], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 1, 'abs_tol', 1e-4);
+ end
+
+ function test_optimise_rosen_both_bounds_minimum_accessible(testCase, optimiser)
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'lb', [-2, -2], 'ub', [2, 2]);
+ testCase.verify_val(pars_fit, testCase.rosenbrock_minimum, 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 0, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_both_bounds_minimum_not_accessible(testCase, optimiser)
+ % note intital guess is outside bounds
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'lb', [-2, -2], 'ub', [0, 0]);
+ testCase.verify_val(pars_fit, [0, 0], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 1, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_parameter_fixed_minimum_not_accessible(testCase, optimiser)
+ % note intital guess is outside bounds
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'lb', [0, -0.5], 'ub', [0, 0]);
+ testCase.verify_val(pars_fit, [0, 0], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 1, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_parameter_fixed_minimum_not_accessible_with_vary_arg(testCase, optimiser)
+ % note intital guess is outside bounds
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [0,-1], 'lb', [nan, -0.5], 'ub', [nan, 0], 'vary', [false, true]);
+ testCase.verify_val(pars_fit, [0, 0], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 1, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_parameter_all_fixed(testCase, optimiser)
+ % note intital guess is outside bounds
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [-1,-1], 'lb', [0, 0], 'ub', [0, 0]);
+ testCase.verify_val(pars_fit, [0, 0], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 1, 'abs_tol', 1e-6);
+ end
+
+ function test_optimise_rosen_parameter_all_fixed_with_vary_arg(testCase, optimiser)
+ % note intital guess is outside bounds
+ [pars_fit, cost_val, ~] = optimiser([], testCase.rosenbrock, [0, 0], 'vary', [false, false]);
+ testCase.verify_val(pars_fit, [0, 0], 'abs_tol', 1e-3);
+ testCase.verify_val(cost_val, 1, 'abs_tol', 1e-6);
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/+sw_tests/+unit_tests/unittest_spinw.m b/+sw_tests/+unit_tests/unittest_spinw.m
new file mode 100644
index 000000000..414b94306
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw.m
@@ -0,0 +1,84 @@
+classdef unittest_spinw < sw_tests.unit_tests.unittest_super
+ % Runs through unit tests for @spinw/spinw.m
+ properties (TestParameter)
+ spinw_struct_input = { ...
+ % {input struct, expected output file}
+ {struct('lattice', struct('angle', [pi, pi, (2*pi)/3], ...
+ 'lat_const', [2, 2, 4])), ...
+ 'spinw_from_struct_lat224.mat'}, ...
+ {struct('lattice', struct('angle', [pi; pi; (2*pi)/3], ...
+ 'lat_const', [2; 2; 4])), ...
+ 'spinw_from_struct_lat224.mat'}};
+ spinw_figure_input = { ...
+ % {input figure, expected output file}
+ {'default_structure.fig', 'spinw_default.mat'}, ...
+ {'afm_chain_spec.fig', 'spinw_afm_chain.mat'}}
+ spinw_file_input = { ...
+ % {input cif/fst file, expected output file}
+ {'YFeO3_mcphase.cif', 'spinw_from_cif_YFeO3_mcphase.mat'}, ...
+ {'LaFeO3_fullprof.cif', 'spinw_from_cif_LaFeO3_fullprof.mat'}, ...
+ {'BiMn2O5.fst', 'spinw_from_fst_BiMn2O5.mat'}};
+ end
+ methods (Test)
+ function test_spinw_no_input(testCase)
+ % Tests that if spinw is called with no input, a default spinw
+ % object is created
+ expected_spinw = testCase.load_spinw('spinw_default.mat');
+ actual_spinw = spinw;
+ testCase.verify_obj(actual_spinw, expected_spinw);
+ end
+ function test_spinw_from_struct_input(testCase, spinw_struct_input)
+ % Tests that if spinw is called with struct input, the relevant
+ % fields are set
+ expected_spinw = testCase.load_spinw(spinw_struct_input{2});
+ actual_spinw = spinw(spinw_struct_input{1});
+ testCase.verify_obj(actual_spinw, expected_spinw);
+ end
+ function test_spinw_from_spinw_obj(testCase)
+ expected_spinw = testCase.load_spinw('spinw_from_struct_lat224.mat');
+ actual_spinw = spinw(expected_spinw);
+ % Ensure creating from a spinw obj creates a copy
+ assert(actual_spinw ~= expected_spinw);
+ testCase.verify_obj(actual_spinw, expected_spinw);
+ end
+ function test_spinw_from_figure(testCase, spinw_figure_input)
+ expected_spinw = testCase.load_spinw(spinw_figure_input{2});
+ figure = testCase.load_figure(spinw_figure_input{1});
+ actual_spinw = spinw(figure);
+ % Ensure creating from a figure creates a copy
+ assert(actual_spinw ~= expected_spinw);
+ testCase.verify_obj(actual_spinw, expected_spinw);
+ close(figure);
+ end
+ function test_spinw_from_incorrect_figure(testCase)
+ fig = figure('visible', 'off');
+ testCase.verifyError(@() spinw(fig), 'spinw:spinw:WrongInput');
+ close(fig);
+ end
+ function test_spinw_from_file(testCase, spinw_file_input)
+ fname = fullfile(testCase.get_unit_test_dir(), 'cifs', spinw_file_input{1});
+ expected_spinw = testCase.load_spinw(spinw_file_input{2});
+ actual_spinw = spinw(fname);
+ testCase.verify_obj(actual_spinw, expected_spinw);
+ end
+ function test_spinw_from_file_wrong_sym(testCase)
+ % Test use of a symmetry not available in symmetry.dat gives
+ % an appropriate error
+ fname = fullfile(testCase.get_unit_test_dir(), 'cifs', 'BiMnO3.fst');
+ testCase.verifyError(@() spinw(fname), 'generator:WrongInput');
+ end
+ function test_spinw_from_wrong_input(testCase)
+ % Test creating spinw object from invalid input gives an
+ % appropriate error
+ testCase.verifyError(@() spinw(4), 'spinw:spinw:WrongInput');
+ end
+ function test_spinw_nmagext(testCase)
+ swobj = testCase.load_spinw('spinw_afm_chain.mat');
+ testCase.assertEqual(swobj.nmagext, 2);
+ end
+ function test_spinw_natom(testCase)
+ swobj = testCase.load_spinw('spinw_from_cif_YFeO3_mcphase.mat');
+ testCase.assertEqual(swobj.natom, 5);
+ end
+ end
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_addaniso.m b/+sw_tests/+unit_tests/unittest_spinw_addaniso.m
new file mode 100644
index 000000000..59e4f8724
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_addaniso.m
@@ -0,0 +1,89 @@
+classdef unittest_spinw_addaniso < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_single_ion = struct('aniso', int32(1), ...
+ 'g', zeros(1,0,'int32'), 'field', [0,0,0], 'T', 0)
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw();
+ testCase.swobj.addatom('r',[0,0,0], 'S',1)
+ testCase.swobj.addmatrix('label','A1','value',diag([-0.1 0 0]))
+ end
+ end
+
+ methods (Test)
+
+ function test_addaniso_requires_matrix(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addaniso(), ...
+ 'MATLAB:minrhs') % better if sw_readparam:MissingParameter
+ end
+
+ function test_addaniso_wrong_matrix_label(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addaniso('A2'), ...
+ 'spinw:addaniso:WrongCouplingTypeIdx')
+ end
+
+ function test_addaniso_with_wrong_atom_label(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addaniso('A1', 'atom_2'), ...
+ 'spinw:addaniso:WrongString')
+ end
+
+ function test_addaniso_with_no_magnetic_atom(testCase)
+ testCase.swobj.addatom('r',[0,0,0], 'S',0, ...
+ 'label', 'atom_1', 'update', true)
+ testCase.verifyError(...
+ @() testCase.swobj.addaniso('A1'), ...
+ 'spinw:addaniso:NoMagAtom')
+ end
+
+ function test_addaniso_all_symm_equiv_atoms(testCase)
+ testCase.swobj.genlattice('sym','I 4'); % body-centred
+ testCase.swobj.addaniso('A1')
+ expected_single_ion = testCase.default_single_ion;
+ expected_single_ion.aniso = int32([1, 1]);
+ testCase.verify_val(testCase.swobj.single_ion, ...
+ expected_single_ion)
+ end
+
+ function test_addaniso_specific_atoms_wrong_atomIdx(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addaniso('A1', 'atom_1', 3), ...
+ 'MATLAB:matrix:singleSubscriptNumelMismatch')
+ end
+
+ function test_addaniso_with_atomIdx_error_when_high_symm(testCase)
+ testCase.swobj.genlattice('sym','I 4'); % body-centred
+ testCase.verifyError(...
+ @() testCase.swobj.addaniso('A1', 'atom_1', 1), ...
+ 'spinw:addaniso:SymmetryProblem')
+ end
+
+ function test_addaniso_specific_atom_label(testCase)
+ % setup unit cell with body-centred atom as different species
+ testCase.swobj.addatom('r',[0.5; 0.5; 0.5], 'S',1)
+ testCase.swobj.addaniso('A1', 'atom_2')
+ expected_single_ion = testCase.default_single_ion;
+ expected_single_ion.aniso = int32([0, 1]);
+ testCase.verify_val(testCase.swobj.single_ion, ...
+ expected_single_ion)
+ end
+
+ function test_addaniso_overwrites_previous_aniso(testCase)
+ testCase.swobj.addmatrix('label','A2','value',diag([-0.5 0 0]))
+ testCase.swobj.addaniso('A1')
+ testCase.swobj.addaniso('A2')
+ expected_single_ion = testCase.default_single_ion;
+ expected_single_ion.aniso = int32(2);
+ testCase.verify_val(testCase.swobj.single_ion, ...
+ expected_single_ion)
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_addatom.m b/+sw_tests/+unit_tests/unittest_spinw_addatom.m
new file mode 100644
index 000000000..85bd2ee84
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_addatom.m
@@ -0,0 +1,220 @@
+classdef unittest_spinw_addatom < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_unit_cell = struct('r', [0; 0; 0], 'S', 0, ...
+ 'label', {{'atom_1'}}, 'color', int32([255; 0; 0]), 'ox', 0, ...
+ 'occ', 1, 'b', [1; 1], 'ff', [zeros(2,10) [1; 1]], ...
+ 'A', int32(-1), 'Z', int32(113), 'biso', 0)
+ ff = [0.4198, 14.2829, 0.6054, 5.4689, 0.9241, -0.0088, ...
+ 0, 0, 0, 0, -0.9498;
+ 6.9270, 0.3783, 2.0813, 0.0151, 11.1284, 5.3800, ...
+ 2.3751, 14.4296, -0.4193, 0.0049, -0.0937]; % Mn3+ form-fac
+ end
+ properties (TestParameter)
+ property_error = {{'Z', 'spinw:addatom:WrongInput'}, ...
+ {'A', 'spinw:addatom:WrongInput'}, ...
+ {'biso', 'spinw:addatom:WrongInput'}, ...
+ {'ox', 'spinw:addatom:WrongInput'}, ...
+ {'S', 'spinw:sw_valid:SizeMismatch'}};
+ oxidation_label = {{3, 'Fe3+_1'}, {-3, 'Fe3-_1'}};
+ pos_vector = {[0;0;0], [0 0 0]}
+ property_value = {{'S',1}, {'occ', 0.5}, {'biso', 0.5}};
+ b_name = {'b', 'bn'}
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw(); % default init
+ end
+ end
+
+ methods (Test)
+ function test_no_input_calls_help(testCase)
+ % Tests that if call addmatrix with no input, it calls the help
+ help_function = sw_tests.utilities.mock_function('swhelp');
+ testCase.swobj.addatom();
+ testCase.assertEqual(help_function.n_calls, 1);
+ testCase.assertEqual(help_function.arguments, ...
+ {{'spinw.addatom'}});
+ end
+
+ function test_add_multiple_atom_throws_mismatch_parameter_size(testCase, property_error)
+ [prop, error] = property_error{:};
+ testCase.verifyError(...
+ @() testCase.swobj.addatom('r', [0 0;0 0.5;0 0.5], prop, 2), ...
+ error)
+ end
+
+ function test_add_atom_fails_invalid_position_size(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addatom('r', [0, 0]), ...
+ 'MATLAB:NonIntegerInput') % need a better error for this
+ end
+
+ function test_add_atom_fails_without_position(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addatom('S', 1), ...
+ 'sw_readparam:MissingParameter')
+ end
+
+ function test_add_atom_fails_with_negative_spin(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addatom('r', [0; 0; 0], 'S', -1), ...
+ 'spinw:addatom:WrongInput')
+ end
+
+ function test_add_atom_warns_bx_provided(testCase)
+ testCase.verifyWarning(...
+ @() testCase.swobj.addatom('r', [0; 0; 0], 'bx', 2), ...
+ 'spinw:addatom:DeprecationWarning')
+ end
+
+ function test_add_atom_with_bn_throws_deprecation_warning(testCase)
+ testCase.verifyWarning(...
+ @() testCase.swobj.addatom('r', [0; 0; 0], 'bn', 2), ...
+ 'spinw:addatom:DeprecationWarning')
+ end
+
+ function test_add_atom_warns_b_and_bn_provided(testCase)
+ testCase.verifyWarning(...
+ @() testCase.swobj.addatom('r', [0; 0; 0], 'b', 2, 'bn', 3), ...
+ 'spinw:addatom:WrongInput')
+ % check value for scattering length is bn provided
+ testCase.assertEqual(testCase.swobj.unit_cell.b(1,1), 3)
+ end
+
+ function test_add_single_default_atom_with_only_position(testCase, pos_vector)
+ testCase.swobj.addatom('r', pos_vector)
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ testCase.default_unit_cell);
+ end
+
+ function test_add_single_atom_custom_parameters(testCase, property_value)
+ [prop, val] = property_value{:};
+ testCase.swobj.addatom('r', [0; 0; 0], prop, val)
+ expected_unit_cell = testCase.default_unit_cell;
+ expected_unit_cell.(prop) = val;
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ expected_unit_cell)
+
+ end
+
+ function test_add_atom_with_custom_scatt_length(testCase, b_name)
+ b = 2;
+ testCase.disable_warnings('spinw:addatom:DeprecationWarning');
+ testCase.swobj.addatom('r', [0;0;0], b_name, b)
+ expected_unit_cell = testCase.default_unit_cell;
+ expected_unit_cell.b = [b; 1];
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ expected_unit_cell)
+ end
+
+ function test_add_multiple_atom_with_single_call(testCase)
+ pos = [[0; 0; 0] [0; 0; 0.5]];
+ S = [0, 1];
+ testCase.swobj.addatom('r', pos, 'S', S)
+ unit_cell = testCase.swobj.unit_cell;
+ testCase.assertEqual(unit_cell.r, pos)
+ testCase.assertEqual(unit_cell.S, S) % default non-mag
+ testCase.assertEqual(unit_cell.label, {'atom_1', 'atom_2'})
+ end
+
+
+ function test_add_atom_with_update_true_different_spin(testCase)
+ pos = [0; 0; 0];
+ label = 'atom';
+ testCase.swobj.addatom('r', pos, 'label', label)
+ testCase.swobj.addatom('r', pos, 'S', 1, 'label', label)
+ expected_unit_cell = testCase.default_unit_cell;
+ expected_unit_cell.S = 1;
+ expected_unit_cell.label = {label};
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ expected_unit_cell)
+ end
+
+ function test_add_atom_update_false_different_spin(testCase)
+ pos = [0; 0; 0];
+ testCase.swobj.addatom('r', pos, 'label', 'atom1')
+ testCase.verifyWarning(...
+ @() testCase.swobj.addatom('r', pos, 'S', 1, ...
+ 'label', 'atom1', 'update', false), ...
+ 'spinw:addatom:WrongInput') % warns occ > 1
+ unit_cell = testCase.swobj.unit_cell;
+ testCase.assertEqual(unit_cell.r, [pos, pos]) % 2 atoms
+ testCase.assertEqual(unit_cell.S, [0, 1])
+ end
+
+ function test_add_atom_will_not_update_different_pos(testCase)
+ pos = [0; 0; 0];
+ testCase.swobj.addatom('r', pos, 'label', 'atom1')
+ testCase.swobj.addatom('r', pos + 0.5, 'S', 1, 'label', 'atom1')
+ unit_cell = testCase.swobj.unit_cell;
+ testCase.assertEqual(unit_cell.r, [pos, pos+0.5]) % 2 atom
+ testCase.assertEqual(unit_cell.S, [0, 1])
+ end
+
+ function test_add_atom_named_ion_lookup(testCase)
+ label = 'Mn3+';
+ testCase.swobj.addatom('r', [0; 0; 0], 'label', label)
+ expected_unit_cell = testCase.default_unit_cell;
+ expected_unit_cell.S = 2;
+ expected_unit_cell.ox = 3;
+ expected_unit_cell.Z = int32(25);
+ expected_unit_cell.b = [-3.73; 1];
+ expected_unit_cell.ff = testCase.ff;
+ expected_unit_cell.label = {label};
+ expected_unit_cell.color = int32([156; 122; 199]);
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ expected_unit_cell, 'abs_tol', 1e-4)
+ end
+
+ function test_add_atom_lookup_by_Z_with_custom_ox(testCase, oxidation_label)
+ [ox, label] = oxidation_label{:};
+ Z = int32(26);
+ testCase.swobj.addatom('r',[0;0;0], 'Z', Z, 'ox', ox)
+ expected_unit_cell = testCase.default_unit_cell;
+ expected_unit_cell.ox = ox;
+ expected_unit_cell.Z = Z;
+ expected_unit_cell.label = {label};
+ expected_unit_cell.color = int32([224; 102; 51]);
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ expected_unit_cell)
+ end
+
+ function test_add_atom_with_custom_form_factor(testCase)
+ label = 'Mn3+';
+ testCase.swobj.addatom('r', [0; 0; 0], 'label', label, ...
+ 'formfact', 1:9);
+ expected_unit_cell = testCase.default_unit_cell;
+ expected_unit_cell.ox = 3;
+ expected_unit_cell.Z = int32(25);
+ expected_unit_cell.b = [-3.73; 1];
+ expected_unit_cell.ff = [1:8 0 0 9; testCase.ff(2,:)];
+ expected_unit_cell.label = {label};
+ expected_unit_cell.color = int32([156; 122; 199]);
+ testCase.verify_val(testCase.swobj.unit_cell, ...
+ expected_unit_cell, 'abs_tol', 1e-4)
+ end
+
+ function test_add_atom_with_custom_form_factor_wrong_size(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addatom('r', [0; 0; 0], 'label', 'Mn3+', ...
+ 'formfact', 1:8), ...
+ 'MATLAB:catenate:dimensionMismatch');
+ end
+
+ end
+
+ methods (Test, TestTags = {'Symbolic'})
+ function test_add_atom_in_symbolic_mode_has_symbolic_S(testCase)
+ pos = [0 0.5; 0 0.5; 0 0.5];
+ testCase.swobj.symbolic(true)
+ testCase.swobj.addatom('r', pos, 'S', [0,1])
+ unit_cell = testCase.swobj.unit_cell;
+ testCase.assertEqual(unit_cell.r, pos)
+ testCase.assertEqual(unit_cell.S, [sym(0), sym('S_2')])
+ end
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_addcoupling.m b/+sw_tests/+unit_tests/unittest_spinw_addcoupling.m
new file mode 100644
index 000000000..5cccc4205
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_addcoupling.m
@@ -0,0 +1,235 @@
+classdef unittest_spinw_addcoupling < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_coupling = struct('dl', int32([1, 0, 0, 1, 1, 1, 0, 1, 0;
+ 0, 1, 0, 0,-1, 1,-1, 0, 1;
+ 0, 0, 1,-1, 0, 0, 1, 1, 1]), ...
+ 'atom1', ones(1,9, 'int32'), 'atom2', ones(1, 9, 'int32'), ...
+ 'mat_idx', zeros(3, 9, 'int32'), 'idx', int32([1 1 1 2 2 2 2 2 2]), ...
+ 'type', zeros(3, 9, 'int32'), 'sym', zeros(3, 9, 'int32'), ...
+ 'rdip', 0.0, 'nsym',int32(0))
+ end
+ properties (TestParameter)
+ bond_atoms = {{'atom_1', 'atom_1'}, 'atom_1', [1,1], 1};
+ invalid_bond_atoms = {{'atom_1', 'atom_2', 'atom_1'}, [1,2,1]}
+ mat_label = {'J1', 1}
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw(); % default init
+ testCase.swobj.addatom('r',[0 0 0],'S',1)
+ testCase.swobj.gencoupling('maxDistance',5) % generate bond list
+ testCase.swobj.addmatrix('label','J1','value', 1)
+ end
+ end
+
+ methods (Test)
+
+ function test_add_coupling_requires_mat_and_bond(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling(), ...
+ 'sw_readparam:MissingParameter')
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat','J1'), ...
+ 'sw_readparam:MissingParameter')
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('bond', 1), ...
+ 'sw_readparam:MissingParameter')
+ end
+
+ function test_add_coupling_requires_run_gencoupling(testCase)
+ sw = spinw(); % default init
+ sw.addatom('r',[0 0 0],'S',1)
+ sw.addmatrix('label','J1','value', 1)
+ testCase.verifyError(...
+ @() sw.addcoupling('mat','J1','bond',1), ...
+ 'spinw:addcoupling:CouplingError')
+ end
+
+ function test_add_coupling_requires_magnetic_atom(testCase)
+ sw = spinw(); % default init
+ sw.addatom('r',[0 0 0],'S',0)
+ testCase.verifyError(...
+ @() sw.addcoupling('mat','J1','bond',1), ...
+ 'spinw:addcoupling:NoMagAtom')
+ end
+
+ function test_add_coupling_with_invalid_matrix(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', 'J2', 'bond', 1), ...
+ 'spinw:addcoupling:WrongMatrixLabel')
+ end
+
+ function test_add_coupling_to_sym_equivalent_bonds(testCase, mat_label)
+ testCase.swobj.addcoupling('mat', mat_label, 'bond', 1)
+ % check matrix added to first three (symm equiv.) bonds
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1:3) = 1;
+ expected_coupling.sym(1,1:3) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_add_coupling_to_individual_bond(testCase)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, 'subIdx', 1)
+ % check matrix added to only first bond with subIdx = 1
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_add_ccoupling_lower_symm_with_subIdx(testCase)
+ testCase.swobj.genlattice('sym', 'I 4')
+ testCase.swobj.gencoupling('maxDistance',5) % generate bond list
+ testCase.verifyWarning(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, 'subIdx', 1), ...
+ 'spinw:addcoupling:SymetryLowered')
+ testCase.assertFalse(any(testCase.swobj.coupling.sym(1,:)))
+ end
+
+ function test_add_coupling_uses_subIdx_only_first_bond(testCase)
+ testCase.verifyWarning(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', [1, 2], 'subIdx', 1), ...
+ 'spinw:addcoupling:CouplingSize')
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_add_coupling_to_multiple_bonds(testCase)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', [1, 2])
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,:) = 1;
+ expected_coupling.sym(1,:) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_add_coupling_to_bonds_using_atom_label(testCase, bond_atoms)
+ % add other atom to the sw object made on setup
+ testCase.swobj.addatom('r',[0.5, 0.5, 0.5],...
+ 'S',1, 'label', {'atom_2'})
+ testCase.swobj.gencoupling('maxDistance',3) % generate bond list
+ % add bond only between atom_1 and atom_1 (bond 2 in
+ % obj.coupling.idx)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 2, ...
+ 'atom', bond_atoms)
+ % check matrix added to atom_1-atom_1 bonds not atom_1-atom_2
+ coupl = testCase.swobj.coupling;
+ ibond = find(coupl.mat_idx(1,:));
+ testCase.assertEqual(ibond, 9:11);
+ testCase.assertTrue(all(coupl.atom1(ibond)==int32(1)))
+ testCase.assertTrue(all(coupl.atom2(ibond)==int32(1)))
+ testCase.assertTrue(all(coupl.idx(ibond)==int32(2)))
+ end
+
+ function test_add_coupling_to_bond_between_atoms_different_to_label(testCase, bond_atoms)
+ % add other atom to the sw object made on setup
+ testCase.swobj.addatom('r',[0.5, 0.5, 0.5],...
+ 'S',1, 'label', {'atom_2'})
+ testCase.swobj.gencoupling('maxDistance',3) % generate bond list
+ % atom_1-atom_1 bonds have 'bond' index 2 (in sw.coupling.idx)
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, ...
+ 'atom', bond_atoms), ...
+ 'spinw:addcoupling:NoBond')
+ end
+
+ function test_add_coupling_to_bond_with_invalid_atom_label(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, ...
+ 'atom', {'atom_1', 'atom_3'}), ...
+ 'spinw:addcoupling:WrongInput')
+ end
+
+ function test_add_coupling_more_than_two_atom_labels(testCase, invalid_bond_atoms)
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, ...
+ 'atom', invalid_bond_atoms), ...
+ 'spinw:addcoupling:WrongInput')
+ end
+
+ function test_add_coupling_biquadratic_exchange(testCase)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, ...
+ 'type', 'biquadratic')
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1:3) = 1;
+ expected_coupling.sym(1,1:3) = 1;
+ expected_coupling.type(1,1:3) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_anisotropic_biquadratic_exchange_throws(testCase)
+ testCase.swobj.addmatrix('label','D','value',[0 -1 0]);
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', 'D', 'bond', 1, ...
+ 'type', 'biquadratic'), ...
+ 'spinw:addcoupling:WrongInput')
+ end
+
+ function test_add_coupling_invalid_exchange_type(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, ...
+ 'type', 'invalid_type'), ...
+ 'spinw:addcoupling:WrongInput')
+ end
+
+ function test_add_coupling_multiple_on_same_bond(testCase)
+ testCase.swobj.addmatrix('label','J2','value', 1)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1)
+ testCase.swobj.addcoupling('mat', 2, 'bond', 1)
+ % check matrix added to first three (symm equiv.) bonds
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1:3) = 1;
+ expected_coupling.mat_idx(2,1:3) = 2;
+ expected_coupling.sym(1:2,1:3) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_add_coupling_max_num_matrices_added(testCase)
+ % add 4 matrices to a bond
+ for imat = 1:4
+ mat_str = ['J', num2str(imat)];
+ testCase.swobj.addmatrix('label', mat_str,'value', imat)
+ if imat < 4
+ testCase.swobj.addcoupling('mat', mat_str, 'bond', 1)
+ else
+ % exceeded max. of 3 matrices on a bond
+ testCase.verifyError(...
+ @() testCase.swobj.addcoupling('mat', mat_str, 'bond', 1), ...
+ 'spinw:addcoupling:TooManyCoupling')
+
+ end
+ end
+ end
+
+ function test_add_coupling_with_sym_false(testCase)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, 'sym', false)
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1:3) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_add_coupling_duplicate_matrix_on_same_bond(testCase)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1)
+ testCase.verifyWarning(...
+ @() testCase.swobj.addcoupling('mat', 'J1', 'bond', 1), ...
+ 'spinw:addcoupling:CouplingIdxWarning')
+ % check matrix only added once
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.mat_idx(1,1:3) = 1;
+ expected_coupling.sym(1,1:3) = 1;
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_addg.m b/+sw_tests/+unit_tests/unittest_spinw_addg.m
new file mode 100644
index 000000000..bde9ef7d2
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_addg.m
@@ -0,0 +1,94 @@
+classdef unittest_spinw_addg < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_single_ion = struct('g', int32(1), ...
+ 'aniso', zeros(1,0,'int32'), 'field', [0,0,0], 'T', 0)
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw();
+ testCase.swobj.addatom('r',[0,0,0], 'S',1)
+ testCase.swobj.addmatrix('label','g1','value',diag([2 1 1]))
+ end
+ end
+
+ methods (Test)
+
+ function test_addg_requires_matrix(testCase)
+ testCase.verifyError(@() testCase.swobj.addg(), ...
+ 'MATLAB:minrhs') % better if sw_readparam:MissingParameter
+ end
+
+ function test_addg_wrong_matrix_label(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addg('g2'), ...
+ 'spinw:addg:WrongCouplingTypeIdx')
+ end
+
+ function test_addaniso_with_no_magnetic_atom(testCase)
+ testCase.swobj.addatom('r',[0,0,0], 'S',0, ...
+ 'label', 'atom_1', 'update', true)
+ testCase.verifyError(...
+ @() testCase.swobj.addg('g1'), ...
+ 'spinw:addg:NoMagAtom')
+ end
+
+ function test_addg_validates_gmatrix(testCase)
+ testCase.swobj.addmatrix('label', 'g', ...
+ 'value', ones(3));
+ testCase.verifyError(@() testCase.swobj.addg('g'), ...
+ 'spinw:addg:InvalidgTensor')
+ end
+
+ function test_addg_with_wrong_atom_label_not_write_g(testCase)
+ testCase.swobj.addg('g1', 'atom_2')
+ testCase.assertFalse(any(testCase.swobj.single_ion.g))
+ end
+
+ function test_addg_all_symm_equiv_atoms(testCase)
+ testCase.swobj.genlattice('sym','I 4'); % body-centred
+ testCase.swobj.addg('g1')
+ expected_single_ion = testCase.default_single_ion;
+ expected_single_ion.g = int32([1, 1]);
+ testCase.verify_val(testCase.swobj.single_ion, ...
+ expected_single_ion)
+ end
+
+ function test_addg_with_atomIdx_error_when_high_symm(testCase)
+ testCase.swobj.genlattice('sym','I 4'); % body-centred
+ testCase.verifyError(...
+ @() testCase.swobj.addg('g1', 'atom_1', 1), ...
+ 'spinw:addg:SymmetryProblem')
+ end
+
+ function test_addg_specific_atoms_wrong_atomIdx(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addg('g1', 'atom_1', 3), ...
+ 'MATLAB:matrix:singleSubscriptNumelMismatch')
+ end
+
+ function test_addg_specific_atom_label(testCase)
+ % setup unit cell with body-centred atom as different species
+ testCase.swobj.addatom('r',[0.5; 0.5; 0.5], 'S',1)
+ testCase.swobj.addg('g1', 'atom_2')
+ expected_single_ion = testCase.default_single_ion;
+ expected_single_ion.g = int32([0, 1]);
+ testCase.verify_val(testCase.swobj.single_ion, ...
+ expected_single_ion)
+ end
+
+ function test_addg_overwrites_previous_gtensor(testCase)
+ testCase.swobj.addmatrix('label','g2','value',diag([1, 1, 2]))
+ testCase.swobj.addg('g1')
+ testCase.swobj.addg('g2')
+ expected_single_ion = testCase.default_single_ion;
+ expected_single_ion.g = int32(2);
+ testCase.verify_val(testCase.swobj.single_ion, ...
+ expected_single_ion)
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_addmatrix.m b/+sw_tests/+unit_tests/unittest_spinw_addmatrix.m
new file mode 100644
index 000000000..f29d804f8
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_addmatrix.m
@@ -0,0 +1,141 @@
+classdef unittest_spinw_addmatrix < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_matrix = struct('mat', eye(3), 'color', int32([0;0;0]), ...
+ 'label', {{'mat1'}})
+ end
+ properties (TestParameter)
+ wrong_matrix_input = {'str', []};
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw;
+ end
+ end
+
+ methods (Test)
+ function test_no_input_calls_help(testCase)
+ % Tests that if call addmatrix with no input, it calls the help
+ help_function = sw_tests.utilities.mock_function('swhelp');
+ testCase.swobj.addmatrix();
+ testCase.assertEqual(help_function.n_calls, 1);
+ testCase.assertEqual(help_function.arguments, ...
+ {{'spinw.addmatrix'}});
+ end
+
+ function test_non_numeric_input_warns_and_not_add_matrix(testCase, wrong_matrix_input)
+ testCase.verifyError(...
+ @() testCase.swobj.addmatrix('value', wrong_matrix_input), ...
+ 'spinw:addmatrix:WrongInput');
+ testCase.assertTrue(isempty(testCase.swobj.matrix.mat));
+ end
+
+ function test_label_and_no_matrix_warns_and_adds_default(testCase)
+ testCase.verifyWarning(...
+ @() testCase.swobj.addmatrix('label', 'mat'), ...
+ 'spinw:addmatrix:NoValue');
+ testCase.assertEqual(testCase.swobj.matrix.mat, eye(3));
+ end
+
+ function test_single_value_adds_diag_matrix_no_label_color(testCase)
+ J = 1.0;
+ testCase.swobj.addmatrix('value', J);
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = J*eye(3);
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_matrix_value_added_no_modification(testCase)
+ mat = reshape(1:9, 3, 3);
+ testCase.swobj.addmatrix('value', mat);
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = mat;
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_vector_value_adds_DM_matrix(testCase)
+ [m1, m2, m3] = deal(1,2,3);
+ testCase.swobj.addmatrix('value', [m1, m2, m3]);
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = [0 m3 -m2; -m3 0 m1; m2 -m1 0];
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_add_multiple_matrices_separate_calls(testCase)
+ nmat = 2;
+ for imat = 1:nmat
+ testCase.swobj.addmatrix('value', imat);
+ end
+ % check matrix properties have correct dimensions
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = cat(3, diag([1,1,1]), diag([2,2,2]));
+ expected_matrix.label = {'mat1', 'mat2'};
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_add_multiple_matrices_single_call(testCase)
+ mat = cat(3, eye(3), 2*eye(3));
+ testCase.swobj.addmatrix('value', mat);
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = mat;
+ expected_matrix.label = {'mat1', 'mat2'};
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_add_matrix_with_same_name_overwritten(testCase)
+ testCase.swobj.addmatrix('value', 1.0);
+ testCase.swobj.addmatrix('value', 2.0, 'label', 'mat1');
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = 2*eye(3);
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_add_multiple_matrices_same_label(testCase)
+ % not possible to have more than two matrix.mat with same label
+ % if matrices are added with addmatrix. To test this need to
+ % modify spinw attribute directly
+ testCase.swobj.matrix.label = {'mat', 'mat'};
+ testCase.verifyError(...
+ @() testCase.swobj.addmatrix('value', 1, 'label', 'mat'), ...
+ 'spinw:addmatrix:LabelError');
+ end
+
+ function test_user_supplied_label_used(testCase)
+ label = 'custom';
+ testCase.swobj.addmatrix('value', 1.0, 'label', label);
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.label = {label};
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ expected_matrix)
+ end
+
+ function test_user_supplied_color_string(testCase)
+ testCase.swobj.addmatrix('value', 1.0, 'color', 'blue');
+ testCase.verify_spinw_matrix(testCase.swobj.matrix, ...
+ testCase.default_matrix)
+ % test color explicitly (only size and data type checked above)
+ testCase.assertEqual(testCase.swobj.matrix.color', ...
+ [0,0,int32(255)])
+ end
+
+ end
+
+ methods (Test, TestTags = {'Symbolic'})
+ function add_matrix_with_symbolic_value(testCase)
+ testCase.swobj.symbolic(true);
+ testCase.swobj.addmatrix('value', 1)
+ expected_matrix = testCase.default_matrix;
+ expected_matrix.mat = sym('mat1')*eye(3);
+ testCase.verify_spinw_matrix(expected_matrix, testCase.swobj.matrix)
+ end
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_addtwin.m b/+sw_tests/+unit_tests/unittest_spinw_addtwin.m
new file mode 100644
index 000000000..f0e0cb7e5
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_addtwin.m
@@ -0,0 +1,97 @@
+classdef unittest_spinw_addtwin < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw();
+ end
+ end
+
+ methods (Test)
+ function test_no_input_calls_help(testCase)
+ % Tests that if call addmatrix with no input, it calls the help
+ help_function = sw_tests.utilities.mock_function('swhelp');
+ testCase.swobj.addtwin();
+ testCase.assertEqual(help_function.n_calls, 1);
+ testCase.assertEqual(help_function.arguments, ...
+ {{'spinw.addtwin'}});
+ end
+
+ function test_addtwin_with_rotc_not_valid_rotation(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addtwin('rotC', ones(3)), ...
+ 'spinw:addtwin:WrongInput');
+ end
+
+ function test_addtwin_with_rotc_invalid_size(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.addtwin('rotC', ones(4) ), ...
+ 'sw_readparam:ParameterSizeMismatch');
+ end
+
+ function test_addtwin_with_only_axis_defaults_identity_rotc(testCase)
+ testCase.swobj.addtwin('axis', [0 0 1])
+ testCase.assertEqual(testCase.swobj.twin.vol, [1, 1])
+ testCase.assertEqual(testCase.swobj.twin.rotc, ...
+ cat(3, eye(3), eye(3)))
+ end
+
+ function test_addtwin_with_axis_custom_vol_ratio(testCase)
+ testCase.swobj.addtwin('axis', [0 0 1], 'vol', 0.5)
+ testCase.assertEqual(testCase.swobj.twin.vol, [1, 0.5])
+ testCase.assertEqual(testCase.swobj.twin.rotc, ...
+ cat(3, eye(3), eye(3)))
+ end
+
+ function test_addtwin_overwrite(testCase)
+ testCase.swobj.addtwin('axis', [0,0,1], 'phid', 90, ...
+ 'vol', 0.5, 'overwrite', true)
+ testCase.assertEqual(testCase.swobj.twin.vol, 0.5)
+ testCase.verify_val(testCase.swobj.twin.rotc, ...
+ [0 -1 0; 1 0 0; 0 0 1])
+ end
+
+ function test_addtwin_with_phid(testCase)
+ import matlab.unittest.constraints.IsEqualTo
+ testCase.swobj.addtwin('axis', [0 0 1], 'phid', [90])
+ testCase.assertEqual(testCase.swobj.twin.vol, [1, 1])
+ testCase.verify_val(testCase.swobj.twin.rotc(:,:,2), ...
+ [0 -1 0; 1 0 0; 0 0 1])
+ end
+
+ function test_addtwin_with_multiple_phid(testCase)
+ testCase.swobj.addtwin('axis', [0 0 1], 'phid', [90, 180])
+ testCase.assertEqual(testCase.swobj.twin.vol, [1, 1, 1])
+ testCase.verify_val(testCase.swobj.twin.rotc(:,:,2), ...
+ [0 -1 0; 1 0 0; 0 0 1])
+ testCase.verify_val(testCase.swobj.twin.rotc(:,:,3), ...
+ [-1 0 0; 0 -1 0; 0 0 1])
+
+ end
+
+ function test_addtwin_with_multiple_rotc(testCase)
+ rotc = cat(3, [0 -1 0; 1 0 0; 0 0 1], [-1 0 0; 0 -1 0; 0 0 1]);
+ testCase.swobj.addtwin('rotc', rotc)
+ testCase.assertEqual(testCase.swobj.twin.vol, [1, 1, 1])
+ testCase.assertEqual(testCase.swobj.twin.rotc(:,:,2:end), rotc)
+ end
+
+ function test_addtwin_support_inversion_rotc(testCase)
+ rotc = -eye(3);
+ testCase.swobj.addtwin('rotc', rotc)
+ testCase.assertEqual(testCase.swobj.twin.vol, [1, 1])
+ testCase.assertEqual(testCase.swobj.twin.rotc(:,:,2:end), rotc)
+ end
+
+ function test_addtwin_overwrite_vol_ratio(testCase)
+ testCase.swobj.addtwin('axis', [0,0,1], 'vol', 0.5, ...
+ 'overwrite', true)
+ testCase.assertEqual(testCase.swobj.twin.vol, 0.5)
+ testCase.assertEqual(testCase.swobj.twin.rotc, eye(3))
+ end
+
+ end
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_field.m b/+sw_tests/+unit_tests/unittest_spinw_field.m
new file mode 100644
index 000000000..943bafc3e
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_field.m
@@ -0,0 +1,63 @@
+classdef unittest_spinw_field < sw_tests.unit_tests.unittest_super
+ properties
+ swobj
+ end
+ properties (TestParameter)
+ incorrect_input = {0, [1; 1], ones(1, 4)};
+ end
+ methods(TestMethodSetup)
+ function create_sw_model(testCase)
+ testCase.swobj = spinw();
+ end
+ end
+ methods (Test)
+ function test_incorrect_shape_field_raises_error(testCase, ...
+ incorrect_input)
+ testCase.verifyError(...
+ @() field(testCase.swobj, incorrect_input), ...
+ 'spinw:magfield:ArraySize');
+ end
+ function test_default_field(testCase)
+ testCase.assertEqual(field(testCase.swobj), [0 0 0]);
+ end
+ function test_set_field(testCase)
+ field_val = [1 2 3];
+ field(testCase.swobj, field_val);
+ testCase.assertEqual(field(testCase.swobj), field_val);
+ end
+ function test_set_field_column_vector(testCase)
+ field_val = [1; 2; 3];
+ field(testCase.swobj, field_val);
+ testCase.assertEqual(field(testCase.swobj), field_val');
+ end
+ function test_set_field_multiple_times(testCase)
+ field(testCase.swobj, [1 2 3]);
+ new_field_val = [1.5 2.5 -3.5];
+ field(testCase.swobj, new_field_val);
+ testCase.assertEqual(field(testCase.swobj), new_field_val);
+ end
+ function test_returns_spinw_obj(testCase)
+ field_val = [1 2 3];
+ new_swobj = field(testCase.swobj, field_val);
+ % Test field is set
+ testCase.assertEqual(field(testCase.swobj), field_val)
+ % Also test handle to swobj is returned
+ assert (testCase.swobj == new_swobj);
+ end
+ end
+ methods (Test, TestTags = {'Symbolic'})
+ function test_set_field_sym_mode_sym_input(testCase)
+ testCase.swobj.symbolic(true);
+ field_val = sym([pi pi/2 pi/3]);
+ field(testCase.swobj, field_val);
+ testCase.assertEqual(field(testCase.swobj), field_val);
+ end
+ function test_set_field_sym_mode_non_sym_input(testCase)
+ testCase.swobj.symbolic(true);
+ field_val = [pi pi/2 pi/3];
+ field(testCase.swobj, field_val);
+ expected_field_val = field_val*sym('B');
+ testCase.assertEqual(field(testCase.swobj), expected_field_val);
+ end
+ end
+end
\ No newline at end of file
diff --git a/+sw_tests/+unit_tests/unittest_spinw_fitspec.m b/+sw_tests/+unit_tests/unittest_spinw_fitspec.m
new file mode 100644
index 000000000..d499e269e
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_fitspec.m
@@ -0,0 +1,57 @@
+classdef unittest_spinw_fitspec < sw_tests.unit_tests.unittest_super
+ % Tests for fitspec - not strictly a unit test, but make sure it runs
+
+ properties
+ datafile = '';
+ swobj = [];
+ fitpar = struct();
+ end
+
+ methods (TestClassSetup)
+ function setup_model_and_fitpars(testCase)
+ % Writes out the mode data
+ testCase.datafile = fullfile(tempdir, 'triAF_modes.txt');
+ fid = fopen(testCase.datafile, 'w');
+ fprintf(fid, ' QH QK QL Elim1 Elim2 I1 EN1 sig1 I2 EN2 sig2\n');
+ fprintf(fid, ' 1 0.2 1 0.5 5.0 22.6 2.94 0.056 0 0 0\n');
+ fprintf(fid, ' 1 0.4 1 0.5 5.0 65.2 2.48 0.053 13.8 3.23 0.061\n');
+ fprintf(fid, ' 1 0.6 1 0.5 5.0 69.5 2.52 0.058 16.3 3.15 0.054\n');
+ fprintf(fid, ' 1 0.8 1 0.5 5.0 22.6 2.83 0.057 0 0 0\n');
+ fclose(fid);
+ testCase.swobj = sw_model('triAF', 0.7);
+ testCase.fitpar = struct('datapath', testCase.datafile, ...
+ 'Evect', linspace(0, 5, 51), ...
+ 'func', @(obj,p)matparser(obj,'param',p,'mat',{'J_1'},'init',1),...
+ 'xmin', 0, 'xmax', 2, 'x0', 0.7, ...
+ 'plot', false, 'hermit', true, ...
+ 'optimizer', 'simplex', ...
+ 'maxiter', 1, 'maxfunevals', 1, 'nMax', 1, 'nrun', 1);
+ end
+ end
+
+ methods (TestClassTeardown)
+ function del_mode_file(testCase)
+ try
+ delete(testCase.datafile); % Ignores errors if file already deleted
+ end
+ end
+ end
+
+ methods (Test)
+ function test_fitspec(testCase)
+ fitout = testCase.swobj.fitspec(testCase.fitpar);
+ testCase.verify_val(fitout.x, 0.7, 'abs_tol', 0.25);
+ testCase.verify_val(fitout.redX2, 243, 'abs_tol', 10);
+ end
+ function test_fitspec_twin(testCase)
+ % Checks that twins are handled correctly
+ swobj = copy(testCase.swobj);
+ % Adds a twin with very small volume so it doesn't affect original fit
+ % If twins not handled correctly, the fit will be bad.
+ swobj.addtwin('axis', [1 1 1], 'phid', 54, 'vol', 0.01);
+ fitout = swobj.fitspec(testCase.fitpar);
+ testCase.verify_val(fitout.x, 0.7, 'abs_tol', 0.25);
+ testCase.verify_val(fitout.redX2, 243, 'abs_tol', 10);
+ end
+ end
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_gencoupling.m b/+sw_tests/+unit_tests/unittest_spinw_gencoupling.m
new file mode 100644
index 000000000..6e076a317
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_gencoupling.m
@@ -0,0 +1,200 @@
+classdef unittest_spinw_gencoupling < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_coupling = struct('dl', int32([1, 0; 0 1; 0 0]), ...
+ 'atom1', ones(1,2, 'int32'), 'atom2', ones(1, 2, 'int32'), ...
+ 'mat_idx', zeros(3, 2, 'int32'), 'idx', int32([1 1]), ...
+ 'type', zeros(3, 2, 'int32'), 'sym', zeros(3, 2, 'int32'), ...
+ 'rdip', 0.0, 'nsym',int32(0))
+ end
+ properties (TestParameter)
+ dist_params = {'maxDistance', 'tolMaxDist', 'tolDist', 'dMin', 'maxSym'}
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw(); % default init
+ testCase.swobj.genlattice('lat_const',[3 3 5])
+ testCase.swobj.addatom('r',[0 0 0],'S',1)
+ end
+ end
+
+ methods (Test)
+
+ function test_gencoupling_requires_magnetic_atom(testCase)
+ % change previously added atom to have S=0 (non magnetic)
+ testCase.swobj.addatom('r',[0 0 0],'S',0, ...
+ 'label', 'atom_1', 'update', true)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling(), ...
+ 'spinw:gencoupling:NoMagAtom')
+ end
+
+ function test_gencoupling_with_maxDistance_less_than_dMin(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling('maxDistance', 0.1, 'dMin', 0.5), ...
+ 'spinw:gencoupling:MaxDLessThanMinD')
+ end
+
+ function test_gencoupling_with_tolMaxDist_grtr_than_maxDist(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling('maxDistance', 5, 'tolMaxDist', 10), ...
+ 'spinw:gencoupling:TolMaxDist')
+ end
+
+ function test_gencoupling_with_tolDist_grtr_than_maxDist(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling('maxDistance', 5, 'tolDist', 10), ...
+ 'spinw:gencoupling:TolDist')
+ end
+
+ function test_gencoupling_with_atom_dist_less_than_dMin(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling('dMin', 5), ...
+ 'spinw:gencoupling:AtomPos')
+ end
+
+ function test_gencoupling_with_maxDistance_less_than_lattice_param(testCase)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling('maxDistance', ...
+ testCase.swobj.lattice.lat_const(1)/2), ...
+ 'spinw:gencoupling:maxDistance')
+ end
+
+ function test_gencoupling_with_negative_distances(testCase, dist_params)
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling(dist_params, -1), ...
+ 'spinw:gencoupling:NegativeDistances')
+ end
+
+ function test_gencoupling_with_tol_when_maxDistance_equal_lattice_param(testCase)
+ % check bonds are created for atoms separated by latt. param.
+ % when tol > delta
+ delta = 1e-2;
+ testCase.swobj.gencoupling('tolMaxDist', 10*delta, 'maxDistance', ...
+ testCase.swobj.lattice.lat_const(1) - delta)
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ end
+
+ function test_gencoupling_with_tolDist_for_symm_equiv_bonds(testCase)
+ % change lattice param to slightly break tetragonal sym.
+ delta = 1e-3;
+ testCase.swobj.genlattice('lat_const',[3 3+delta 5])
+ % check that when tolDist > delta the bonds are equiv.
+ testCase.swobj.gencoupling('maxDistance', 4, 'tolDist', 10*delta)
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ % check that when tolDist > delta the bonds are inequiv.
+ testCase.swobj.gencoupling('maxDistance', 4, 'tolDist', 0.1*delta)
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.idx = int32([1, 2]); % i.e. not sym equiv
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_gencoupling_with_non_P0_spacegroup(testCase)
+ % set ortho spacegroup even though lattice is tetragonal
+ testCase.swobj.genlattice('sym', 'P 2') % not overwrite abc
+ % test forceNoSym = true (just uses bond length for idx)
+ testCase.swobj.gencoupling('maxDistance', 4, 'forceNoSym', true)
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ % test forceNoSym = false (default) - checks spacegroup
+ testCase.swobj.gencoupling('maxDistance', 4)
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.idx = int32([1, 2]); % i.e. not sym equiv
+ expected_coupling.nsym = int32(2);
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_gencoupling_with_nonzero_fid(testCase)
+ mock_fprintf = sw_tests.utilities.mock_function('fprintf0');
+ fid = 3;
+ testCase.swobj.gencoupling('maxDistance', 4, 'fid', fid)
+ testCase.assertEqual(mock_fprintf.n_calls, 2);
+ % check fid used to write file
+ for irow = 1:mock_fprintf.n_calls
+ testCase.assertEqual(mock_fprintf.arguments{irow}{1}, fid)
+ end
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ end
+
+ function test_gencoupling_overwrites_previous_call(testCase)
+ testCase.swobj.gencoupling('maxDistance', 8)
+ testCase.swobj.gencoupling('maxDistance', 4)
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ end
+
+ function test_gencoupling_with_maxSym_multiple_bonds(testCase)
+ testCase.swobj.genlattice('sym', 'P 4') % not overwrite abc
+ % set maxSym distance to only include bond idx = 1
+ testCase.swobj.gencoupling('maxDistance', 4.5 , 'maxSym', 4)
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.dl = int32([1 0 1 1; 0 1 -1 1; 0 0 0 0]);
+ expected_coupling.atom1 = ones(1, 4, 'int32');
+ expected_coupling.atom2 = ones(1, 4, 'int32');
+ expected_coupling.mat_idx = zeros(3, 4, 'int32');
+ expected_coupling.idx = int32([1 1 2 2]);
+ expected_coupling.type = zeros(3, 4, 'int32');
+ expected_coupling.sym = zeros(3, 4, 'int32');
+ expected_coupling.nsym = int32(1);
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ % increase maxSym to include bond idx = 2 (length sqrt(2)*3)
+ testCase.swobj.gencoupling('maxDistance', 4.5 , 'maxSym', 4.25);
+ expected_coupling.nsym = int32(2);
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_gencoupling_maxSym_less_than_any_bond_length(testCase)
+ testCase.swobj.genlattice('sym', 'P 4') % not overwrite abc
+ testCase.verifyWarning(...
+ @() testCase.swobj.gencoupling('maxDistance', 4, ...
+ 'maxSym', 2), 'spinw:gencoupling:maxSym')
+ % check nsym not set (bonds reduced to P0)
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ end
+
+ function test_gencoupling_when_tol_violates_symmetry(testCase)
+ testCase.swobj.genlattice('lat_const',[3 3.5 5], 'sym', 'P 4')
+ % bond along a and b are sym. equiv. according to P 4
+ % but lengths differ by 0.5 Ang
+ testCase.verifyError(...
+ @() testCase.swobj.gencoupling('maxDistance', 3.25, ...
+ 'tolDist', 0.6), 'spinw:gencoupling:SymProblem')
+ end
+
+ function test_gencoupling_with_two_mag_atoms(testCase)
+ testCase.swobj.addatom('r',[0.25 0.25 0.25],'S',2)
+ testCase.swobj.gencoupling('maxDistance', 3);
+ expected_coupling = testCase.default_coupling;
+ expected_coupling.dl = int32([0, 1, 0, 1, 0, 1, 0;
+ 0, 0, 1, 0, 1, 0, 1;
+ 0, 0, 0, 0, 0, 0, 0]);
+ expected_coupling.atom1 = int32([1 2 2 1 1 2 2]);
+ expected_coupling.atom2 = int32([2 1 1 1 1 2 2]);
+ expected_coupling.mat_idx = zeros(3, 7, 'int32');
+ expected_coupling.idx = int32([1 2 2 3 3 3 3]);
+ expected_coupling.type = zeros(3, 7, 'int32');
+ expected_coupling.sym = zeros(3, 7, 'int32');
+ testCase.verify_val(testCase.swobj.coupling, ...
+ expected_coupling)
+ end
+
+ function test_gencoupling_ignores_non_mag_atoms(testCase)
+ testCase.swobj.addatom('r',[0.25 0.25 0.25],'S',0)
+ testCase.swobj.gencoupling('maxDistance', 4);
+ testCase.verify_val(testCase.swobj.coupling, ...
+ testCase.default_coupling)
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_genlattice.m b/+sw_tests/+unit_tests/unittest_spinw_genlattice.m
new file mode 100644
index 000000000..66b1c12a9
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_genlattice.m
@@ -0,0 +1,265 @@
+classdef unittest_spinw_genlattice < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_latt = struct('angle', repmat(pi/2, 1, 3), ...
+ 'lat_const', repmat(3, 1, 3), ...
+ 'sym', zeros(3, 4, 0), ...
+ 'origin', zeros(1, 3), 'label', 'P 0');
+ P2_sym = cat(3, [1 0 0 0; 0 1 0 0; 0 0 1 0], ...
+ [-1 0 0 0; 0 1 0 0; 0 0 -1 0])
+ end
+ properties (TestParameter)
+ param_name = {'angle', 'lat_const'};
+ spgr = {'P 2', 3}; % spacegroup and index in symmetry.dat
+ invalid_spgr = {'P2', 'P 7', '-x,y,-x', '-x,y', eye(2)};
+ sym_param_name = {'sym', 'spgr'};
+ basis_vecs = {[1/2 1/2 0; 0 1/2 1/2; 1/2 0 1/2], ... % RH
+ [1/2 1/2 0; 1/2 0 1/2; 0 1/2 1/2]}; % LH
+ nelem = {1,3}; % length of cell input for spgr
+ invalid_perm = {[1,4,2],[0,1,2], [1,1,1], 'bad', 'zzz', 'aaa', ...
+ {1,2,3}}
+ invalid_origin = {[-0.5,0,0], [0,2,0]};
+ invalid_label = {1, {'label'}}
+ % test user provided label always used for all types of sym input
+ spgr_type = {'P 2', 3, '-x,y,-z', [eye(3), zeros(3,1)]};
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw(); % default init
+ end
+ end
+
+ methods (Test)
+
+ function test_params_alter_lattice_fields_same_name(testCase, param_name)
+ value = [0.5, 0.5, 0.5];
+ testCase.swobj.genlattice(param_name, value);
+ expected_latt = testCase.default_latt;
+ expected_latt.(param_name) = value;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_angled_degree_conversion(testCase)
+ testCase.swobj.genlattice('angled', [90, 90, 90]);
+ testCase.verify_val(testCase.default_latt, ...
+ testCase.swobj.lattice)
+ end
+
+ function test_angle_and_angled_provided(testCase)
+ value = [1,1,1];
+ testCase.verifyWarning(...
+ @() testCase.swobj.genlattice('angled', value, ...
+ 'angle', value), ...
+ 'spinw:genlattice:WrongInput');
+ expected_latt = testCase.default_latt;
+ expected_latt.angle = deg2rad(value); % 'angled' used
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_spgr_throws_deprecation_warning(testCase)
+ testCase.verifyWarning(...
+ @() testCase.swobj.genlattice('spgr', 'P 2'), ...
+ 'spinw:genlattice:DeprecationWarning');
+ end
+
+ function test_spgr_and_sym_throws_error(testCase)
+ testCase.disable_warnings('spinw:genlattice:DeprecationWarning');
+ testCase.verifyError(...
+ @() testCase.swobj.genlattice('spgr', 3, 'sym', 3), ...
+ 'spinw:genlattice:WrongInput');
+ end
+
+ function test_spacegroup_property(testCase, sym_param_name, spgr)
+ testCase.disable_warnings('spinw:genlattice:DeprecationWarning');
+ testCase.swobj.genlattice(sym_param_name, spgr);
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = 'P 2';
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_label_always_used(testCase, sym_param_name, spgr_type)
+ testCase.disable_warnings('spinw:genlattice:DeprecationWarning');
+ label = 'label';
+ testCase.swobj.genlattice(sym_param_name, spgr_type, ...
+ 'label', label);
+ testCase.verify_val(testCase.swobj.lattice.label, label)
+ end
+
+ function test_spacegroup_with_sym_operation_matrix(testCase, sym_param_name)
+ testCase.disable_warnings('spinw:genlattice:DeprecationWarning');
+ testCase.swobj.genlattice(sym_param_name, testCase.P2_sym);
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = '';
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_spacegroup_with_sym_operation_string(testCase)
+ % test perm supplied without symmetry throws warning
+ perm = 'bac';
+ testCase.verifyWarning(...
+ @() testCase.swobj.genlattice('perm', perm), ...
+ 'spinw:genlattice:WrongInput');
+ testCase.verify_val(testCase.default_latt, ...
+ testCase.swobj.lattice) % object unchanged
+ % supply spacegroup and check a and b swapped
+ spgr_str = 'P 2';
+ testCase.swobj.genlattice('sym', spgr_str, 'perm', 'bac');
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.sym(:, :, end) = [1 0 0 0; 0 -1 0 0; 0 0 -1 0];
+ expected_latt.label = spgr_str;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_spacegroup_with_axes_permutation(testCase)
+ sym_str = '-x,y,-z';
+ testCase.swobj.genlattice('sym', sym_str);
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = sym_str;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_valid_label(testCase)
+ label = 'P 4';
+ testCase.swobj.genlattice('label', label);
+ expected_latt = testCase.default_latt;
+ expected_latt.label = label;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_origin_set_only_when_spgr_provided(testCase)
+ origin = [0.5, 0, 0];
+ testCase.verifyWarning(...
+ @() testCase.swobj.genlattice('origin', origin), ...
+ 'spinw:genlattice:WrongInput');
+ testCase.verify_val(testCase.default_latt, ...
+ testCase.swobj.lattice); % origin unchanged without spgr
+ % define spacegroup
+ testCase.swobj.genlattice('sym', testCase.P2_sym, ...
+ 'origin', origin);
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = '';
+ expected_latt.origin = origin;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ end
+
+ function test_nformula_unit(testCase)
+ nformula = int32(2);
+ testCase.swobj.genlattice('nformula', nformula);
+ testCase.verify_val(testCase.swobj.unit.nformula, nformula);
+ end
+
+ function test_basis_vector_and_rotation_matrix(testCase, basis_vecs)
+ args = {'lat_const',[4.5 4.5 4.5], 'sym','F 2 3'};
+ % if no basis vectors are supplied then rot matrix always I
+ R = testCase.swobj.genlattice(args{:});
+ testCase.verify_val(R, eye(3));
+ % add basis vectors for primitive cell
+ R = testCase.swobj.genlattice(args{:}, 'bv', basis_vecs);
+ sw_basis_vecs = R*basis_vecs;
+ % check first spinwave basis vec is along x
+ testCase.verify_val(sw_basis_vecs(:,1), [sqrt(2)/2; 0; 0]);
+ end
+
+ function test_spacegroup_with_cell_input(testCase, sym_param_name)
+ spgr_str = '-x,y,-z';
+ label = 'label';
+ testCase.disable_warnings('spinw:genlattice:DeprecationWarning');
+ testCase.swobj.genlattice(sym_param_name, {spgr_str, label});
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = label;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ % provide label in cell and as separate argument
+ new_label = 'new label';
+ testCase.verifyWarning(...
+ @() testCase.swobj.genlattice(sym_param_name, ...
+ {spgr_str, label},'label', new_label), ...
+ 'spinw:genlattice:WrongInput');
+ expected_latt.label = new_label;
+ testCase.verify_val(testCase.swobj.lattice, expected_latt);
+ end
+
+ function test_spacegroup_cell_input_invalid_numel(testCase, nelem)
+ testCase.verifyError(...
+ @() testCase.swobj.genlattice('sym', cell(1, nelem)), ...
+ 'spinw:genlattice:WrongInput');
+ end
+
+ function test_invalid_permutation(testCase, invalid_origin)
+ testCase.verifyError(...
+ @() testCase.swobj.genlattice('sym', 'P 2', ...
+ 'origin', invalid_origin), ...
+ 'spinw:genlattice:WrongInput');
+ end
+
+ function test_invalid_origin(testCase, invalid_perm)
+ testCase.verifyError(...
+ @() testCase.swobj.genlattice('sym', 'P 2', ...
+ 'perm', invalid_perm), ...
+ 'spinw:genlattice:WrongInput');
+ end
+
+ function test_invalid_spgr(testCase, invalid_spgr)
+ testCase.verifyError(...
+ @() testCase.swobj.genlattice('sym', invalid_spgr), ...
+ 'generator:WrongInput');
+ end
+
+ function test_non_default_spacegroup_overwritten(testCase)
+ testCase.swobj.genlattice('sym','F 2 3');
+ testCase.swobj.genlattice('sym', 'P 2');
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = 'P 2';
+ testCase.verify_val(testCase.swobj.lattice, expected_latt);
+ end
+
+ function test_zero_spacegroup(testCase)
+ testCase.swobj.genlattice('sym','P 2');
+ testCase.swobj.genlattice('sym', 0);
+ expected_latt = testCase.default_latt;
+ expected_latt.label = 'No sym';
+ expected_latt.sym = [eye(3) zeros(3,1)];% actually equiv. to P 1
+ testCase.verify_val(testCase.swobj.lattice, expected_latt);
+ end
+
+ function test_invalid_label(testCase, invalid_label)
+ testCase.swobj.genlattice('label', invalid_label)
+ testCase.verify_val(testCase.swobj.lattice, ...
+ testCase.default_latt); % not overwritten
+ end
+
+ function test_lookup_new_lines_in_symmetry_dat_file(testCase)
+ % copy file as backup (would need to do this anyway)
+ spinw_dir = sw_rootdir();
+ filesep = spinw_dir(end);
+ dat_dir = [spinw_dir 'dat_files' filesep];
+ dat_path = [dat_dir 'symmetry.dat'];
+ backup_path = [dat_dir 'symmetry_backup.dat'];
+ copyfile(dat_path, backup_path);
+ % add line to file (same P2 sym op but new number and label)
+ extra_line = ' 231 P P : -x,y,-z\n';
+ fid = fopen(dat_path, 'a');
+ fprintf(fid, extra_line);
+ fclose(fid);
+ % run test
+ testCase.swobj.genlattice('sym', 231);
+ expected_latt = testCase.default_latt;
+ expected_latt.sym = testCase.P2_sym;
+ expected_latt.label = 'P P';
+ testCase.verify_val(testCase.swobj.lattice, expected_latt)
+ % restore backup - note movefile errored on windows server
+ copyfile(backup_path, dat_path);
+ delete(backup_path);
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_genmagstr.m b/+sw_tests/+unit_tests/unittest_spinw_genmagstr.m
new file mode 100644
index 000000000..04e70d787
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_genmagstr.m
@@ -0,0 +1,656 @@
+classdef unittest_spinw_genmagstr < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ swobj_tri = [];
+ default_mag_str = struct('nExt', int32([1 1 1]), ...
+ 'k', [0; 0; 0], ...
+ 'F', [0; 1; 0]);
+ end
+ properties (TestParameter)
+ fm_chain_input_errors = { ...
+ % varargin, identifier
+ {{'mode', 'something'}, 'spinw:genmagstr:WrongMode'}; ...
+ {{'unit', 'something'}, 'spinw:genmagstr:WrongInput'}; ...
+ % nExt can't be zero
+ {{'nExt', [0 1 1]}, 'spinw:genmagstr:WrongInput'}; ...
+ % n must have dimensions nK, 3
+ {{'n', ones(2, 3), 'k', [0 0 1/2]}, 'sw_readparam:ParameterSizeMismatch'}; ...
+ % S with direct must have same number of spins as atoms in nExt supercell
+ {{'mode', 'direct', 'S', [0; 1; 0], 'nExt', [2 1 1]}, 'spinw:genmagstr:WrongSpinSize'}; ...
+ % S with helical must have 1 spin, or same number of spins as
+ % atoms in unit or supercell
+ {{'mode', 'helical', 'S', [0 1; 1 0; 0 0]}, 'spinw:genmagstr:WrongNumberSpin'}; ...
+ {{'mode', 'direct', 'S', [1 0 0]}, 'spinw:genmagstr:WrongInput'}; ...
+ {{'mode', 'direct', 'k', [1/2; 0; 0]}, 'spinw:genmagstr:WrongInput'}; ...
+ {{'mode', 'direct', 'S', [1 0 0], 'k', [1/2; 0; 0]}, 'spinw:genmagstr:WrongInput'}; ...
+ {{'mode', 'tile', 'S', [0 0; 1 1; 1 1]}, 'spinw:genmagstr:WrongInput'}; ...
+ % S with helical must be real
+ {{'mode', 'helical', 'S', [1.5; 0; 1.5i]}, 'spinw:genmagstr:WrongInput'}; ...
+ % Rotate mode must first initialise a magnetic structure
+ {{'mode', 'rotate'}, 'spinw:genmagstr:WrongInput'}
+ };
+ rotate_input_errors = { ...
+ % varargin, identifier
+ % rotation angle must be real (previously phi=i had special
+ % behaviour)
+ {{'mode', 'rotate', 'phi', i}, 'spinw:genmagstr:ComplexPhi'}; ...
+ % If no angle is supplied to rotate, the rotation axis is set
+ % orthogonal to both the first spin and n. If the first spin
+ % and n are parallel, this should therefore cause an error
+ {{'mode', 'rotate'}, 'spinw:genmagstr:InvalidRotation'}; ...
+ };
+ ignored_inputs = { ...
+ % arguments
+ {'mode', 'random', 'S', [1; 0; 0], 'epsilon', 1}, ...
+ {'n', [0 1 1], 'mode', 'direct', 'S', [1; 0; 0]}, ...
+ {'mode', 'tile', 'k', [0 0 1/2], 'n', [0 0 1], 'S', [1; 0; 0]}, ...
+ {'mode', 'helical', 'k', [0 0 1/3], 'S', [1; 0; 0;], 'x0', []}, ...
+ {'mode', 'func', 'x0', [pi/2 -pi/4 0 0 [0 0 1/3] pi pi/2], 'unit', 'lu', 'next', [1 2 1]}, ...
+ {'mode', 'fourier', 'S', [1; i; 0], 'n', [1 0 0]}
+ };
+ complex_n_input = {[1 1+i 0], [i 0 0]};
+ rotate_phi_inputs = {{'phi', pi/4}, {'phid', 45}, {'phi', pi/4, 'phid', 90}};
+ input_norm_output_F = {
+ % norm_bool, mag_str.F
+ {true, [2; 0; -2i]}, ...
+ {false, [1; 0; -1i]}
+ };
+ end
+ methods (TestMethodSetup)
+ function setup_chain_model(testCase)
+ % Create a simple FM 1D chain model
+ testCase.swobj = spinw();
+ testCase.swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ testCase.swobj.addatom('r', [0 0 0],'S', 1);
+ end
+ function setup_tri_model(testCase)
+ % Create a simple triangular lattice model
+ testCase.swobj_tri = spinw;
+ testCase.swobj_tri.genlattice('lat_const', [4 4 6], 'angled', [90 90 120]);
+ testCase.swobj_tri.addatom('r', [0 0 0], 'S', 3/2);
+ end
+ end
+
+ methods (Test)
+ function test_invalid_fm_chain_input_raises_error(testCase, fm_chain_input_errors)
+ varargin = fm_chain_input_errors{1};
+ identifier = fm_chain_input_errors{2};
+ testCase.verifyError(...
+ @() testCase.swobj.genmagstr(varargin{:}), ...
+ identifier)
+ end
+ function test_invalid_rotate_input_raises_error(testCase, rotate_input_errors)
+ varargin = rotate_input_errors{1};
+ identifier = rotate_input_errors{2};
+ swobj = copy(testCase.swobj);
+ swobj.genmagstr('mode', 'direct', 'S', [0; 0; 1]);
+ testCase.verifyError(...
+ @() swobj.genmagstr(varargin{:}), ...
+ identifier)
+ end
+ function test_no_magnetic_atoms_raises_error(testCase)
+ swobj = spinw;
+ testCase.verifyError(...
+ @() swobj.genmagstr(), ...
+ 'spinw:genmagstr:NoMagAtom')
+ end
+ function test_complex_n_input_raises_error(testCase, complex_n_input)
+ testCase.verifyError(...
+ @() testCase.swobj.genmagstr('mode', 'helical', 'S', [1; 0; 0], 'n', complex_n_input), ...
+ 'spinw:genmagstr:WrongInput')
+ end
+ function test_tile_too_few_S_raises_error(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ testCase.verifyError(...
+ @() swobj.genmagstr('mode', 'tile', 'S', [0; 1; 1]), ...
+ 'spinw:genmagstr:WrongInput')
+ end
+ function test_ignored_input_warns(testCase, ignored_inputs)
+ testCase.verifyWarning(...
+ @() testCase.swobj.genmagstr(ignored_inputs{:}), ...
+ 'spinw:genmagstr:UnreadInput')
+ end
+ function test_rotate_ignored_input_warns(testCase)
+ swobj = copy(testCase.swobj);
+ % Need to initialise structure before rotating it
+ swobj.genmagstr('mode', 'direct', 'S', [1; 0; 0]);
+ testCase.verifyWarning(...
+ @() swobj.genmagstr('mode', 'rotate', 'k', [0 0 1/3]), ...
+ 'spinw:genmagstr:UnreadInput')
+ end
+ function test_helical_spin_size_incomm_with_nExt_warns(testCase)
+ swobj = copy(testCase.swobj);
+ nExt = [2 1 1];
+ k = [1/3 0 0];
+ testCase.verifyWarning(...
+ @() swobj.genmagstr('mode', 'helical', ...
+ 'S', [1 0; 0 1; 0 0], ...
+ 'k', k, ...
+ 'nExt', nExt), ...
+ 'spinw:genmagstr:UCExtNonSuff')
+ expected_mag_str = struct('nExt', int32(nExt), ...
+ 'k', k', ...
+ 'F', [1 -1i; 1i 1; 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_spin_size_incomm_with_epsilon_warns(testCase)
+ swobj = copy(testCase.swobj);
+ delta = 1e-6;
+ k = [(delta+1)/3 0 0];
+ nExt = [3 1 1];
+ testCase.verifyWarning(...
+ @() swobj.genmagstr('mode', 'helical', ...
+ 'S', [1; 0; 0], ...
+ 'k', k, ...
+ 'nExt', nExt, ...
+ 'epsilon', 0.99*delta), ...
+ 'spinw:genmagstr:UCExtNonSuff')
+ expected_mag_str = struct('nExt', int32(nExt), ...
+ 'k', k', ...
+ 'F', [1 -0.5-0.866i -0.5+0.866i; ...
+ 1i 0.866-0.5i -0.866-0.5i; ...
+ 0 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str, 'rel_tol', 1e-4);
+ end
+ function test_fourier_too_large_nExt_warns(testCase)
+ swobj = copy(testCase.swobj);
+ nExt = [6 1 1];
+ k = [1/3 0 0];
+ testCase.verifyWarning(...
+ @() swobj.genmagstr('mode', 'fourier', ...
+ 'S', [1; 1i; 0], ...
+ 'k', k, ...
+ 'nExt', nExt), ...
+ 'spinw:genmagstr:UCExtOver')
+ F_rep = [ 1 -0.5-1i*sqrt(3)/2 -0.5+1i*sqrt(3)/2; ...
+ 1i sqrt(3)/2-0.5i -sqrt(3)/2-0.5i; ...
+ 0 0 0];
+ expected_mag_str = struct('nExt', int32(nExt), ...
+ 'k', k', ...
+ 'F', cat(2, F_rep, F_rep));
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_fourier_nExt_wrong_direction_warns(testCase)
+ swobj = copy(testCase.swobj);
+ nExt = [2 1 2];
+ k = [1/2 0 0];
+ testCase.verifyWarning(...
+ @() swobj.genmagstr('mode', 'fourier', ...
+ 'S', [1; 1i; 0], ...
+ 'k', k, ...
+ 'nExt', nExt), ...
+ 'spinw:genmagstr:UCExtOver')
+ F_rep = [1 -1; 1i -1i; 0 0];
+ expected_mag_str = struct('nExt', int32(nExt), ...
+ 'k', k', ...
+ 'F', cat(2, F_rep, F_rep));
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_any_S_parallel_to_n_warns(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ k = [1/3 0 0];
+ testCase.verifyWarning(...
+ @() swobj_tri.genmagstr('mode', 'helical', ...
+ 'S', [0; 1; 1], ...
+ 'k', k), ...
+ 'spinw:genmagstr:SnParallel')
+ expected_mag_str = struct( ...
+ 'nExt', int32([1 1 1]), ...
+ 'k', k', ...
+ 'F', [-sqrt(9/8)*1i; sqrt(9/8); sqrt(9/8)]);
+ testCase.verify_obj(swobj_tri.mag_str, expected_mag_str);
+ end
+ function test_helical_S2_norm(testCase, input_norm_output_F)
+ swobj = spinw();
+ swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ swobj.addatom('r', [0 0 0], 'S', 2);
+ k = [1/3 0 0];
+ swobj.genmagstr('mode', 'helical', 'S', [1; 0; 0], 'k', k, ...
+ 'n', [0 1 0], 'norm', input_norm_output_F{1});
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = k';
+ expected_mag_str.F = input_norm_output_F{2};
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_direct_fm_chain(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.genmagstr('mode', 'direct', 'k', [0 0 0], ...
+ 'S', [0; 1; 0]);
+ testCase.verify_obj(swobj.mag_str, testCase.default_mag_str);
+
+ end
+ function test_direct_fm_chain_nok(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.genmagstr('mode', 'direct', 'S', [0; 1; 0]);
+ testCase.verify_obj(swobj.mag_str, testCase.default_mag_str);
+ end
+ function test_direct_multiatom_nExt(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.5], 'S', 2);
+ S = [1 0 1 -1; ...
+ 1 1 0 0; ...
+ 0 -1 0 0];
+ nExt = [2 1 1];
+ k = [0 1/3 0];
+ swobj.genmagstr('mode', 'direct', 'S', S, 'nExt', nExt, 'k', k);
+ expected_mag_str = struct('nExt', int32(nExt), ...
+ 'k', k', ...
+ 'F', [sqrt(2)/2 0 1 -2; ...
+ sqrt(2)/2 sqrt(2) 0 0; ...
+ 0 -sqrt(2) 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_direct_multiatom_multik(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.5], 'S', 2);
+ S_k = [1 0; 1 1; 0 -1];
+ S = cat(3, S_k, S_k);
+ k = [0 1/3 0; 1/2 0 0];
+ swobj.genmagstr('mode', 'direct', 'S', S, 'k', k);
+ F_k = [sqrt(2)/2 0; ...
+ sqrt(2)/2 sqrt(2); ...
+ 0 -sqrt(2)];
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = k';
+ expected_mag_str.F = cat(3, F_k, F_k);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_direct_multik_scalar_nExt(testCase)
+ % Test if a scalar is used for nExt it is treated as a
+ % tolerance to automatically determine nExt
+ swobj = copy(testCase.swobj);
+ S_k = [1 0 1 0 1 0; 0 0 1 1 0 0; 0 0 0 1 1 1];
+ S = cat(3, S_k, S_k);
+ nExt = 0.01;
+ k = [0 1/3+0.5*nExt 0; 1/2+0.5*nExt 0 0];
+ swobj.genmagstr('mode', 'direct', 'S', S, 'k', k, 'nExt', nExt);
+ F_k = [1 0 sqrt(2)/2 0 sqrt(2)/2 0; ...
+ 0 0 sqrt(2)/2 sqrt(2)/2 0 0; ...
+ 0 0 0 sqrt(2)/2 sqrt(2)/2 1];
+ expected_mag_str = struct('nExt', int32([2 3 1]), ...
+ 'k', k', ...
+ 'F', cat(3, F_k, F_k));
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_tri(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ k = [1/3 1/3 0];
+ swobj_tri.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'k', k);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = k';
+ expected_mag_str.F = [1.5; 1.5i; 0];
+ testCase.verify_obj(swobj_tri.mag_str, expected_mag_str);
+ end
+ function test_helical_tri_n(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ k = [1/3 1/3 0];
+ swobj_tri.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'k', k, 'n', [0 1 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = k';
+ expected_mag_str.F = [1.5; 0; -1.5i];
+ testCase.verify_obj(swobj_tri.mag_str, expected_mag_str);
+ end
+ function test_helical_tri_lu_unit(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ swobj_tri.genmagstr('mode', 'helical', 'S', [0; 1; 0], ...
+ 'k', [1/3 1/3 0], 'unit', 'lu');
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = [1/3; 1/3; 0];
+ expected_mag_str.F = [-0.75-1.299038105676658i; ...
+ 1.299038105676658-0.75i; ...
+ 0];
+ testCase.verify_obj(swobj_tri.mag_str, expected_mag_str);
+ end
+ function test_fourier_tri(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ swobj_tri.genmagstr('mode', 'fourier', 'S', [1; 0; 0], ...
+ 'k', [1/3 1/3 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = [1/3; 1/3; 0];
+ expected_mag_str.F = [1.5; 0; 0];
+ testCase.verify_obj(swobj_tri.mag_str, expected_mag_str);
+ end
+ function test_helical_multiatom_nExt_1spin(testCase)
+ % Test where only 1 spin is provided
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.0], 'S', 2);
+ k = [0 0 1/2];
+ nExt = int32([1 1 2]);
+ swobj.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'nExt', nExt, 'k', k);
+ expected_mag_str = struct(...
+ 'k', k', ...
+ 'nExt', nExt, ...
+ 'F', [1 2 -1 -2; 1i 2i -1i -2i; 0 0 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_multiatom_nExt_1spin_r0(testCase)
+ swobj = spinw();
+ swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ % Need to have nonzero r for first atom for r0 to have an effect
+ swobj.addatom('r', [0.5 0.5 0.5],'S', 1);
+ swobj.addatom('r', [0 0 0], 'S', 2);
+ k = [0 0 1/2];
+ nExt = int32([1 1 2]);
+ swobj.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'nExt', nExt, 'k', k, 'r0', false);
+ expected_mag_str = struct(...
+ 'k', k', ...
+ 'nExt', nExt, ...
+ 'F', [1 2i -1 -2i; 1i -2 -1i 2; 0 0 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_fourier_multiatom_nExt_nMagAtom_spins(testCase)
+ % Test where there are the same number of spins provided as in
+ % the unit cell. Note result is the same as helical with
+ % complex spins or S=[0 1; 1 0; 0 0]
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.0], 'S', 2);
+ k = [0 1/2 0];
+ nExt = int32([1 2 1]);
+ swobj.genmagstr('mode', 'fourier', 'S', [-1i 1; 1 1i; 0 0], ...
+ 'nExt', nExt, 'k', k);
+ expected_mag_str = struct( ...
+ 'k', k', ...
+ 'nExt', nExt, ...
+ 'F', [-1i 2 1i -2; 1 2i -1 -2i; 0 0 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_multiatom_nExt_nMagAtom_spins(testCase)
+ % Test where there are the same number of spins provided as in
+ % the unit cell
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.0], 'S', 2);
+ k = [0 1/2 0];
+ nExt = int32([1 2 1]);
+ swobj.genmagstr('mode', 'helical', 'S', [0 1; 1 0; 0 0], ...
+ 'nExt', nExt, 'k', k);
+ expected_mag_str = struct( ...
+ 'k', k', ...
+ 'nExt', nExt, ...
+ 'F', [-1i 2 1i -2; 1 2i -1 -2i; 0 0 0 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_multiatom_nExt_nMagExt_spins(testCase)
+ % Test where there are the same number of spins provided as in
+ % the supercell
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.0], 'S', 2);
+ k = [0 1/2 0];
+ nExt = int32([1 2 1]);
+ testCase.verifyWarning(...
+ @() swobj.genmagstr( ...
+ 'mode', 'helical', ...
+ 'S', [0 1 0 -1; 1 0 0 0; 0 0 1 0], ...
+ 'nExt', nExt, 'k', k), ...
+ 'spinw:genmagstr:SnParallel');
+ expected_mag_str = struct(...
+ 'k', k', ...
+ 'nExt', nExt, ...
+ 'F', [-1i 2 0 -2; 1 2i 0 -2i; 0 0 1 0]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_helical_multiatom_multik_multin(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0.5], 'S', 2);
+ S = cat(3, [1; 0; 0], [0; 0; 1]);
+ k = [0 1/3 0; 1/2 0 0];
+ n = [0 0 1; 0 1 0];
+ % Ensure warning is not emitted as there are no S parallel to
+ % n within a single k
+ testCase.verifyWarningFree(...
+ @() swobj.genmagstr('mode', 'helical', ...
+ 'S', S, ...
+ 'k', k, ...
+ 'n', n), ...
+ 'spinw:genmagstr:SnParallel')
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = k';
+ expected_mag_str.F = cat(3, ...
+ [1 1-sqrt(3)*1i; 1i sqrt(3)+1i; 0 0], ...
+ [1i 2; 0 0; 1 -2i]);
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_random_structure(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.genmagstr('mode','random');
+ mag_str1 = swobj.mag_str;
+ swobj.genmagstr('mode','random');
+ mag_str2 = swobj.mag_str;
+ % Check structure is random each time - F is different
+ testCase.verifyNotEqual(mag_str1.F, mag_str2.F);
+ testCase.verifyEqual(size(mag_str1.F), size(mag_str2.F));
+ % Check F size and magnitude
+ testCase.verifySize(mag_str1.F, [3 1]);
+ testCase.verify_val(vecnorm(real(swobj.mag_str.F), 2), 1);
+ % Check imaginary component of F is perpendicular to default n
+ testCase.verify_val(dot(imag(swobj.mag_str.F), [0 0 1]), 0);
+ % Check other fields
+ expected_mag_str = testCase.default_mag_str;
+ testCase.verify_obj(rmfield(mag_str1, 'F'), ...
+ rmfield(expected_mag_str, 'F'));
+ end
+ function test_random_structure_k_and_n(testCase)
+ swobj = copy(testCase.swobj);
+ k = [0; 0; 1/4];
+ n = [1 1 0];
+ swobj.genmagstr('mode','random', 'k', k', 'n', n);
+ mag_str1 = swobj.mag_str;
+ % Check F size and magnitude
+ testCase.verifySize(mag_str1.F, [3 1]);
+ testCase.verify_val(vecnorm(real(swobj.mag_str.F), 2), 1);
+ % Check imaginary component of F is perpendicular to n
+ testCase.verify_val(dot(imag(swobj.mag_str.F), n), 0);
+ % Check other fields
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.k = k;
+ testCase.verify_obj(rmfield(mag_str1, 'F'),...
+ rmfield(expected_mag_str, 'F'));
+ end
+ function test_random_structure_multiatom_and_nExt(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 2);
+ nExt = int32([2 2 2]);
+ swobj.genmagstr('mode', 'random', 'nExt', nExt);
+ mag_str1 = swobj.mag_str;
+ swobj.genmagstr('mode', 'random', 'nExt', nExt);
+ mag_str2 = swobj.mag_str;
+ % Check structure is random each time - F is different
+ testCase.verifyNotEqual(mag_str1.F, mag_str2.F);
+ % Check F size and magnitude
+ testCase.verifySize(mag_str1.F, [3 16]);
+ testCase.verify_val( ...
+ vecnorm(real(swobj.mag_str.F(:, 1:2:end)), 2), ones(1, 8));
+ testCase.verify_val( ...
+ vecnorm(real(swobj.mag_str.F(:, 2:2:end)), 2), 2*ones(1, 8));
+ % Check imaginary component of F is perpendicular to default n
+ testCase.verifyEqual( ...
+ dot(imag(swobj.mag_str.F), repmat([0; 0; 1], 1, 16)), zeros(1, 16));
+ % Check other fields
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.nExt = nExt;
+ testCase.verify_obj(rmfield(mag_str1, 'F'), ...
+ rmfield(expected_mag_str, 'F'));
+ end
+ function test_tile_existing_struct_extend_cell(testCase)
+ % Test that tile and increasing nExt will correctly tile
+ % initialised structure
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ nExt = int32([1 2 1]);
+ % Also test if we input 'k' it is set to 0 in final struct
+ swobj.genmagstr('mode', 'direct', 'S', [1 0; 0 1; 0 0], 'k', [1/2 0 0]);
+ swobj.genmagstr('mode', 'tile', 'nExt', nExt);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.nExt = nExt;
+ expected_mag_str.F = [1 0 1 0; 0 1 0 1; 0 0 0 0];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_tile_existing_struct_same_size(testCase)
+ % Test that tile with nExt same as initialised structure
+ % does nothing
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ nExt = int32([1 2 1]);
+ S = [1 0 0 -1; 0 1 0 0; 0 0 1 0];
+ swobj.genmagstr('mode', 'direct', 'S', S, 'nExt', nExt);
+ swobj.genmagstr('mode', 'tile', 'nExt', nExt);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.nExt = nExt;
+ expected_mag_str.F = S;
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_tile_input_S_extend_cell(testCase)
+ % Test that tile and input S less than nExt will correctly tile
+ % input S
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ nExt = int32([3 1 1]);
+ swobj.genmagstr('mode', 'tile', 'nExt', nExt, ...
+ 'S', [1 0; 0 1; 0 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.nExt = nExt;
+ expected_mag_str.F = [1 0 1 0 1 0; 0 1 0 1 0 1; 0 0 0 0 0 0];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_tile_multik(testCase)
+ % Test that S is summed over third dimension with tile, and k
+ % is not needed (is this the behaviour we want?)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ S = cat(3, [1 0; 0 1; 0 0], [0 1; 0 0; 1 0]);
+ swobj.genmagstr('mode', 'tile', ...
+ 'S', S);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = sqrt(2)/2*[1 1; 0 1; 1 0];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_tile_multik_provided_k_set_to_zero(testCase)
+ % Test that S is summed over third dimension with tile, and if
+ % k is provided, it is set to zero anyway
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ S = cat(3, [1 0; 0 1; 0 0], [0 1; 0 0; 1 0]);
+ k = 0.5*ones(size(S, 3), 3);
+ testCase.assertWarning(...
+ @() swobj.genmagstr('mode', 'tile', 'S', S, 'k', k), ...
+ 'spinw:genmagstr:UnreadInput');
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = sqrt(2)/2*[1 1; 0 1; 1 0];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_extend_mode_input_S_extend_cell_and_warns(testCase)
+ % Test undocumented 'extend' mode does same as tile
+ % Test that tile and input S less than nExt will correctly tile
+ % input S
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ nExt = int32([3 1 1]);
+ testCase.verifyWarning(...
+ @() swobj.genmagstr('mode', 'extend', 'nExt', nExt, ...
+ 'S', [1 0; 0 1; 0 0]), ...
+ 'spinw:genmagstr:DeprecationWarning');
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.nExt = nExt;
+ expected_mag_str.F = [1 0 1 0 1 0; 0 1 0 1 0 1; 0 0 0 0 0 0];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_rotate_phi(testCase, rotate_phi_inputs)
+ swobj = copy(testCase.swobj);
+ k = [1/2 0 0];
+ % Need to initialise structure before rotating it
+ swobj.genmagstr('mode', 'direct', 'S', [1; 0; 0], 'k', k);
+ swobj.genmagstr('mode', 'rotate', rotate_phi_inputs{:});
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = sqrt(2)/2*[1; 1; 0];
+ expected_mag_str.k = k';
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_rotate_multiatom_n(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ k = [1/2 0 0];
+ swobj.genmagstr('mode', 'direct', 'S', [1 0; 0 1; 0 0], 'k', k);
+ swobj.genmagstr('mode', 'rotate', 'phi', pi/2, 'n', [1 1 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = [0.5 0.5; 0.5 0.5; -sqrt(2)/2 sqrt(2)/2];
+ expected_mag_str.k = k';
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_rotate_no_phi_collinear(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ swobj.genmagstr('mode', 'direct', 'S', [1 -1; 0 0; 0 0]);
+ swobj.genmagstr('mode', 'rotate', 'n', [0 1 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = [0 0; 1 -1; 0 0];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_rotate_no_phi_coplanar(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ swobj.genmagstr('mode', 'direct', 'S', [1 0; 0 1; 0 0]);
+ swobj.genmagstr('mode', 'rotate', 'n', [0 1 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = [1 0; 0 0; 0 -1];
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_rotate_no_phi_incomm(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ k = [1/3 1/3 0];
+ swobj_tri.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'k', k);
+ swobj_tri.genmagstr('mode', 'rotate', 'n', [1 0 0]);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = [0; 1.5i; -1.5];
+ expected_mag_str.k = k';
+ testCase.verify_obj(swobj_tri.mag_str, expected_mag_str);
+ end
+ function test_func_multiatom_default(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.addatom('r', [0.5 0.5 0], 'S', 1);
+ k = [1/3 0 0];
+ x0 = [pi/2 -pi/4 0 0 k pi pi/2];
+ swobj.genmagstr('mode', 'func', 'x0', x0);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = [sqrt(2)/2*(1-i) 0; -sqrt(2)/2*(1+i) 0; 0 1];
+ expected_mag_str.k = k';
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ function test_func_custom(testCase)
+ function [S, k, n] = func(S0, x0)
+ S = [-S0; 0; 0];
+ k = x0;
+ n = x0;
+ end
+ swobj = copy(testCase.swobj);
+ x0 = [1/3 0 0];
+ swobj.genmagstr('mode', 'func', 'func', @func, 'x0', x0);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = [-1; 0; 0];
+ expected_mag_str.k = x0';
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ end
+ methods (Test, TestTags = {'Symbolic'})
+ function test_func_custom_symbolic(testCase)
+ function [S, k, n] = func(S0, x0)
+ S = [-S0; 0; 0];
+ k = x0;
+ n = x0;
+ end
+ swobj = copy(testCase.swobj);
+ swobj.symbolic(true);
+ x0 = [1/3 0 0];
+ swobj.genmagstr('mode', 'func', 'func', @func, 'x0', x0);
+ expected_mag_str = testCase.default_mag_str;
+ expected_mag_str.F = sym([-1; 0; 0]);
+ expected_mag_str.k = sym(x0');
+ testCase.verify_obj(swobj.mag_str, expected_mag_str);
+ end
+ end
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_intmatrix.m b/+sw_tests/+unit_tests/unittest_spinw_intmatrix.m
new file mode 100644
index 000000000..c8f5ac562
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_intmatrix.m
@@ -0,0 +1,321 @@
+classdef unittest_spinw_intmatrix < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ % icoupling = 1 2 3 4
+ default_SS = struct('all', [1 1 0 0; % dl_a
+ 0 0 1 1; % dl_b
+ 0 0 0 0; % dl_c
+ 1 2 1 2; % matom1
+ 1 2 1 2; % matom2
+ 1 1 -2 -2; % J11
+ 0 0 0 0; % J12
+ 0 0 0 0; % J13
+ 0 0 0 0; % J21
+ 1 1 -2 -2; % J22
+ 0 0 0 0; % J23
+ 0 0 0 0; % J31
+ 0 0 0 0; % J32
+ 1 1 -2 -2; % J33
+ 0 0 1 1], ... % 0=quad, 1=biquad
+ 'dip', [1 1;
+ 0 0;
+ 0 0;
+ 1 2;
+ 1 2;
+ -0.0537 -0.0537;
+ 0 0;
+ 0 0;
+ 0 0;
+ 0.0067 0.0067;
+ 0 0;
+ 0 0;
+ 0 0;
+ 0.0067 0.0067;
+ 0 0]);
+ default_SI = struct('aniso', repmat([0 0 0; 0 0 0; 0 0 -0.1],1,1,2), ...
+ 'g', repmat([2 0 0; 0 1 0; 0 0 1],1,1,2), ...
+ 'field', [0 0 0.5]);
+ default_RR = [0 0.5; 0 0.5; 0 0.5];
+ end
+
+ methods (TestMethodSetup)
+ function setup_spinw_model(testCase)
+ testCase.swobj = spinw();
+ testCase.swobj.genlattice('lat_const', [2 3 5], 'sym', 'I m m m')
+ testCase.swobj.addatom('r',[0; 0; 0],'S',1)
+ testCase.swobj.addmatrix('label','g1','value',diag([2 1 1]))
+ testCase.swobj.addmatrix('label', 'A', ...
+ 'value', diag([0 0 -0.1])) % c easy
+ testCase.swobj.addmatrix('label', 'J1', 'value', 1)
+ testCase.swobj.addmatrix('label', 'J2', 'value', -2)
+ testCase.swobj.addmatrix('label','D','value',[0 -1 0])
+ testCase.swobj.addmatrix('label','gen','value', ...
+ reshape(2:2:18, [3, 3]))
+ testCase.swobj.gencoupling('maxDistance', 5);
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1); % bond // a
+ testCase.swobj.addcoupling('mat', 'J2', 'bond', 2, 'type', 'biquadratic'); % bond // b
+ testCase.swobj.addaniso('A');
+ testCase.swobj.addg('g1');
+ testCase.swobj.field([0 0 0.5]);
+ testCase.swobj.coupling.rdip = 3; % set max dist for dipolar
+ end
+ end
+
+ methods
+ function expected_SS = get_expected_SS_fitmode_false(testCase)
+ expected_SS = testCase.default_SS;
+ expected_SS.iso = expected_SS.all(1:6,1:2);
+ expected_SS.bq = expected_SS.all(1:6,3:4);
+ expected_SS.ani = [1; 0; 0; 2; 1; 1; 2; 3];
+ expected_SS.dm = [1; 0; 0; 2; 1; 0; -1; 0];
+ expected_SS.gen = [1 0 0 2 1 2 4 6 8 10 12 14 16 18]';
+ expected_SS.all = [expected_SS.all, zeros(15, 3)];
+ expected_SS.all(1:5, 5:end) = repmat(expected_SS.ani(1:5), ...
+ 1, 3);
+ expected_SS.all(1:end-1, 6) = expected_SS.gen;
+ expected_SS.all([8, 12], 5) = [-1 1]; % DM
+ expected_SS.all([6, 10, 14], 7) = [1, 2, 3]; % aniso
+ end
+ end
+
+ methods (Test)
+
+ function test_intmatrix_no_matoms(testCase)
+ [SS, SI, RR] = spinw().intmatrix('fitmode', true);
+ expected_SS = struct('all', zeros(15,0), 'dip', zeros(15,0));
+ expected_SI = struct('aniso', zeros(3,3,0), 'g', zeros(3,3,0), ...
+ 'field', zeros(1,3));
+ expected_RR = zeros(3,0);
+ testCase.verify_val(expected_SS, SS)
+ testCase.verify_val(expected_SI, SI)
+ testCase.verify_val(expected_RR, RR)
+ end
+
+ function test_intmatrix_no_couplings(testCase)
+ sw = spinw();
+ sw.addatom('r',[0; 0; 0],'S',1);
+ [SS, SI, RR] = sw.intmatrix('fitmode', true);
+ expected_SS = struct('all', zeros(15,0), 'dip', zeros(15,0));
+ expected_SI = struct('aniso', zeros(3,3), 'g', 2*eye(3), ...
+ 'field', zeros(1,3));
+ expected_RR = zeros(3,1);
+ testCase.verify_val(expected_SS, SS)
+ testCase.verify_val(expected_SI, SI)
+ testCase.verify_val(expected_RR, RR)
+ end
+
+ function test_intmatrix_fitmode_true(testCase)
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true);
+ testCase.verify_val(testCase.default_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+
+ function test_intmatrix_fitmode_true_plotmode_true(testCase)
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'plotmode', true);
+ expected_SS = testCase.default_SS;
+ expected_SS.all = [expected_SS.all; zeros(3, 4)];
+ expected_SS.all(15:end,:) = [3 3 4 4;
+ 1 1 2 2;
+ 0 0 1 1;
+ 1 2 3 4];
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function test_intmatrix_fitmode_true_DM_interaction(testCase)
+ testCase.verifyWarning( ...
+ @() testCase.swobj.addcoupling('mat', 'D', 'bond', 3, 'subIdx', 1), ...
+ 'spinw:addcoupling:SymetryLowered');
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true);
+
+ dm_elems = [1; 0; 0; 2; 1; 0; 0; -1; 0; 0; 0; 1; 0; 0; 0];
+ expected_SS = testCase.default_SS;
+ expected_SS.all = [expected_SS.all, dm_elems];
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function test_intmatrix_fitmode_false(testCase)
+ % add all different types of interaction
+ testCase.disable_warnings('spinw:addcoupling:SymetryLowered');
+ testCase.swobj.addmatrix('label','Janiso','value', ...
+ diag([1,2,3]))
+ for mat_name = {'D', 'gen', 'Janiso'}
+ testCase.swobj.addcoupling('mat', mat_name, 'bond', 3, 'subIdx', 1);
+ end
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', false);
+
+ expected_SS = testCase.get_expected_SS_fitmode_false();
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function test_intmatrix_zeroC_false_removes_zero_matrices(testCase)
+ testCase.swobj.addmatrix('label', 'J1', 'value', 0)
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'zeroC', false);
+
+ expected_SS = testCase.default_SS;
+ expected_SS.all = expected_SS.all(:,3:end);
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function test_intmatrix_conjugate(testCase)
+ % add coupling between two different atoms
+ testCase.verifyWarning( ...
+ @() testCase.swobj.addcoupling('mat', 'gen', 'bond', 3, 'subIdx', 1), ...
+ 'spinw:addcoupling:SymetryLowered');
+ % zero other couplings for brevity (will be omitted by zeroC)
+ testCase.swobj.addmatrix('label', 'J1', 'value', 0)
+ testCase.swobj.addmatrix('label', 'J2', 'value', 0)
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'conjugate', true);
+
+ expected_SS = testCase.default_SS;
+ % two bonds 2->1 and 1->2 with half interaction
+ bond1 = [1, 0, 0, 2, 1, 1:9, 0]';
+ bond2 = [-1, 0, 0, 1, 2, 1, 4, 7, 2, 5, 8, 3, 6, 9, 0]';
+ expected_SS.all = [bond1, bond2];
+ expected_SS.dip = repmat(expected_SS.dip, 1, 2);
+ expected_SS.dip(1:3,3:end) = -expected_SS.dip(1:3,3:end);
+ expected_SS.dip(6:end-1,:) = 0.5*expected_SS.dip(6:end-1,:);
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function test_extend_false_with_supercell(testCase)
+ % make a supercell
+ testCase.swobj.genmagstr('mode', 'random', 'nExt', [2 1 1]);
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'extend', false);
+
+ testCase.verify_val(testCase.default_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function test_extend_true_with_supercell(testCase)
+ % make a supercell
+ testCase.swobj.genmagstr('mode', 'random', 'nExt', [2 1 1]);
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'extend', true);
+
+ % SS
+ expected_SS = testCase.default_SS;
+ expected_SS.all = repmat(expected_SS.all, 1, 2);
+ expected_SS.all(1,1:2) = 0;
+ expected_SS.all(5,1:2) = [3, 4];
+ expected_SS.all(4,5:8) = repmat([3, 4], 1, 2);
+ expected_SS.all(5,7:8) = [3, 4];
+ expected_SS.dip = repmat(expected_SS.dip, 1, 2);
+ expected_SS.dip(1:5,:) = expected_SS.all(1:5,[1:2 5:6]);
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 1e-4)
+ % SI
+ expected_SI = testCase.default_SI;
+ expected_SI.aniso = repmat(expected_SI.aniso, 1,1,2);
+ expected_SI.g = repmat(expected_SI.g, 1,1,2);
+ testCase.verify_val(expected_SI, SI)
+ % RR
+ expected_RR = [0 0.25 0.5 0.75;
+ 0 0.5 0 0.5;
+ 0 0.5 0 0.5];
+ testCase.verify_val(expected_RR, RR)
+ end
+
+ function test_sortDM_reorders_bonds(testCase)
+ testCase.disable_warnings('spinw:addcoupling:SymetryLowered');
+ testCase.swobj.addmatrix('label', 'J2', 'value', 0);
+ % make face-centred to have bond order depend on sortDM
+ testCase.swobj.genlattice('sym', 'F m m m');
+ testCase.swobj.gencoupling();
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1, 'subIdx',4:6);
+
+ [SS_sort, ~, ~] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'sortDM', true);
+ [SS_unsort, ~, ~] = testCase.swobj.intmatrix('fitmode', true, ...
+ 'sortDM', false);
+ % first bond same
+ testCase.verify_val(SS_sort.all(:,1), SS_unsort.all(:,1));
+ % just check the atom indices of the subsequent bonds
+ testCase.verify_val([2 1; 1 2], SS_unsort.all(4:5,2:end));
+ testCase.verify_val([1 2; 2 1], SS_sort.all(4:5,2:end));
+ end
+
+ function test_2_atoms_in_unit_cell_P1_sym(testCase)
+ % Revert P1 sym and add atom at body-center
+ testCase.swobj.genlattice('sym', 'P 1')
+ testCase.swobj.addatom('r',[0.5; 0.5; 0.5],'S',1)
+ testCase.swobj.gencoupling('maxDistance', 5);
+ % need to add each matrix twice (once for each atom)
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1:2); % bond // a
+ testCase.swobj.addcoupling('mat', 'J2', 'bond', 3:4, 'type', ...
+ 'biquadratic'); % bond // b
+ testCase.swobj.addaniso('A');
+ testCase.swobj.addg('g1');
+ testCase.swobj.field([0 0 0.5]);
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true);
+ testCase.verify_val(testCase.default_SS, SS, 'abs_tol', 1e-4)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+ end
+
+ methods (Test, TestTags = {'Symbolic'})
+ function symbolic_obj_with_fitmode_true(testCase)
+ testCase.swobj.symbolic(true);
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', true);
+ % replace symbolic variables with 1
+ SS = structfun(@sw_sub1, SS, 'UniformOutput', false);
+ SI = structfun(@sw_sub1, SI, 'UniformOutput', false);
+ expected_SS = testCase.default_SS;
+ expected_SS.dip(6:end,:) = 298.3569*expected_SS.dip(6:end,:);
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 5e-3)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+ function symbolic_obj_with_fitmode_false(testCase)
+ % add all different types of interaction
+ testCase.disable_warnings('spinw:addcoupling:SymetryLowered');
+ testCase.swobj.addmatrix('label','Janiso','value', ...
+ diag([1,2,3]))
+ for mat_name = {'D', 'gen', 'Janiso'}
+ testCase.swobj.addcoupling('mat', mat_name, 'bond', 3, ...
+ 'subIdx', 1);
+ end
+ testCase.swobj.symbolic(true);
+
+ [SS, SI, RR] = testCase.swobj.intmatrix('fitmode', false);
+ % replace symbolic variables with 1
+ SS = structfun(@sw_sub1, SS, 'UniformOutput', false);
+ SI = structfun(@sw_sub1, SI, 'UniformOutput', false);
+
+ expected_SS = testCase.get_expected_SS_fitmode_false();
+ expected_SS.dip(6:end,:) = 298.3569*expected_SS.dip(6:end,:);
+
+ testCase.verify_val(expected_SS, SS, 'abs_tol', 5e-3)
+ testCase.verify_val(testCase.default_SI, SI)
+ testCase.verify_val(testCase.default_RR, RR)
+ end
+
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_optmagk.m b/+sw_tests/+unit_tests/unittest_spinw_optmagk.m
new file mode 100644
index 000000000..2d0486294
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_optmagk.m
@@ -0,0 +1,124 @@
+classdef unittest_spinw_optmagk < sw_tests.unit_tests.unittest_super
+
+ 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]);
+ orig_rng_state = [];
+ end
+ properties (TestParameter)
+ kbase_opts = {[1; 1; 0], [1 0; 0 1; 0 0]};
+ end
+ methods (TestClassSetup)
+ function set_seed(testCase)
+ testCase.orig_rng_state = rng;
+ rng('default');
+ end
+ 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(TestMethodTeardown)
+ function reset_seed(testCase)
+ rng(testCase.orig_rng_state);
+ 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('seed', 1);
+ 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', 2e-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', 2e-4);
+ end
+ function test_afm_chain_optk(testCase)
+ testCase.swobj.addmatrix('label', 'J1', 'value', 1);
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1);
+ % Use seed for reproducibility
+ testCase.swobj.optmagk('seed', 1);
+ 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
diff --git a/+sw_tests/+unit_tests/unittest_spinw_optmagsteep.m b/+sw_tests/+unit_tests/unittest_spinw_optmagsteep.m
new file mode 100644
index 000000000..29dd8f882
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_optmagsteep.m
@@ -0,0 +1,238 @@
+classdef unittest_spinw_optmagsteep < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ default_magstr = struct('S', [0 0; 0 0; -1 1], 'n', [0 0 1], ...
+ 'N_ext', [2 1 1], 'k', [0 0 0], ...
+ 'exact', true)
+ % output from optmagsteep
+ default_opt = struct('M', [0 0; 0 0; -1 1],...
+ 'dM', 6.6e-17,...
+ 'e', -1.1, ...
+ 'nRun', 1, ...
+ 'title', ['Optimised magnetic structure ', ...
+ 'using the method of steepest ', ...
+ 'descent']);
+ orig_rng_state = []
+ end
+ properties (TestParameter)
+ existing_plot = {true, false};
+ end
+ methods (TestClassSetup)
+ function set_seed(testCase)
+ testCase.orig_rng_state = rng;
+ rng('default');
+ end
+ end
+ methods (TestMethodSetup)
+ function setup_afm_chain_model_easy_axis(testCase)
+ testCase.swobj = spinw();
+ testCase.swobj.genlattice('lat_const', [2 3 6])
+ testCase.swobj.addatom('r',[0; 0; 0],'S',1)
+ testCase.swobj.addmatrix('label', 'A', ...
+ 'value', diag([0 0 -0.1])) % c easy
+ testCase.swobj.addmatrix('label', 'J1', 'value', 1) % AFM
+ testCase.swobj.gencoupling('maxDistance', 6);
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 1); % along a
+ testCase.swobj.addaniso('A');
+ end
+ end
+ methods(TestMethodTeardown)
+ function reset_seed(testCase)
+ rng(testCase.orig_rng_state);
+ end
+ end
+ methods (Test)
+ function test_only_one_spin_throws_error(testCase)
+ testCase.swobj.genmagstr('mode', 'direct', 'S', [0; 0; 1], ...
+ 'k',[0, 0, 0]);
+ func_call = @() testCase.swobj.optmagsteep('random', true);
+ testCase.verifyError(func_call, 'spinw:optmagsteep:NoField')
+ end
+
+ function test_invalid_nExt_throws_error(testCase)
+ func_call = @() testCase.swobj.optmagsteep('nExt', [0, 0, 0]);
+ testCase.verifyError(func_call, 'spinw:optmagsteep:WrongInput')
+ end
+
+ function test_warns_if_not_converged(testCase)
+ % init moment along hard-axis (far from minima) and run 1 iter
+ testCase.swobj.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'k',[0.5,0,0], 'n', [0,1,0], ...
+ 'nExt', [2,1,1]);
+ func_call = @() testCase.swobj.optmagsteep('nRun', 1);
+ testCase.verifyWarning(func_call, ...
+ 'spinw:optmagsteep:NotConverged')
+ end
+
+ function test_warns_of_self_coupling_spins(testCase)
+ % add AFM exchange to bond along b (will double cell along b)
+ testCase.swobj.addmatrix('label', 'J2', 'value', 0.25) % AFM
+ testCase.swobj.addcoupling('mat', 'J2', 'bond', 2); % along b
+ % run opt with k only along a
+ func_call = @() testCase.swobj.optmagsteep('NExt', [2, 1, 1], ...
+ 'nRun', 1);
+ testCase.verifyWarning(func_call, ...
+ 'spinw:optmagsteep:SelfCoupling')
+ end
+
+ function test_starting_from_groundstate_executes_one_iteration(testCase)
+ % generate ground state magnetic structure
+ testCase.swobj.genmagstr('mode', 'helical', 'S', [0; 0; -1], ...
+ 'k',[0.5,0,0], 'n', [0,1,0], ...
+ 'nExt', [2,1,1]);
+ opt = testCase.swobj.optmagsteep;
+ testCase.verify_val(testCase.swobj.magstr, ...
+ testCase.default_magstr, 'abs_tol', 1e-6);
+ fields = {'datestart', 'dateend', 'obj', 'param'};
+ testCase.verify_val(rmfield(opt, fields), testCase.default_opt);
+ testCase.verify_obj(opt.obj, testCase.swobj)
+ end
+
+ function test_converges_local_minimum_along_hard_axis(testCase)
+ % FM with S//a (hard axis) - no component along easy-axis
+ testCase.swobj.genmagstr('mode', 'direct', ...
+ 'S', [1 1; 0 0; 0 0], ...
+ 'k',[0,0,0], 'nExt', [2,1,1]);
+ testCase.swobj.optmagsteep; % results in AFM S//a
+ expected_magstr = testCase.default_magstr;
+ expected_magstr.S = [-1, 1; 0, 0; 0, 0];
+ testCase.verify_val(testCase.swobj.magstr, expected_magstr);
+ testCase.verify_val(testCase.swobj.energy, -1.0)
+ end
+
+ function test_spins_align_along_easy_axis_external_field(testCase)
+ testCase.swobj.field([0 0 1]);
+ % FM with S//a (hard axis) - no component along easy-axis
+ testCase.swobj.genmagstr('mode', 'direct', ...
+ 'S', [1 1; 0 0; 0 0], ...
+ 'k',[0,0,0], 'nExt', [2,1,1]);
+ testCase.swobj.optmagsteep('nRun', 150); % results in AFM S//c
+ expected_magstr = testCase.default_magstr;
+ expected_magstr.S = [0, 0; 0, 0; 1, -1];
+ testCase.verify_val(testCase.swobj.magstr, expected_magstr, ...
+ 'abs_tol', 1e-6);
+ testCase.verify_val(testCase.swobj.energy, -1.1, ...
+ 'abs_tol', 1e-10) % same energy as grd state without field
+ end
+
+ function test_converges_global_minimum(testCase)
+ % FM with component of S // c (easy-axis)
+ testCase.swobj.genmagstr('mode', 'direct', ...
+ 'S', [0 0; 1 1; 1 1], ...
+ 'k',[0,0,0], 'nExt', [2,1,1]);
+ % try with one iterations - will not convergence
+ testCase.verifyWarning(...
+ @() testCase.swobj.optmagsteep('nRun', 1), ...
+ 'spinw:optmagsteep:NotConverged');
+ testCase.swobj.optmagsteep('nRun', 150); % results in AFM S//c
+ testCase.verify_val(testCase.swobj.magstr, ...
+ testCase.default_magstr, 'abs_tol', 1e-8);
+ testCase.verify_val(testCase.swobj.energy, -1.1)
+ end
+
+ function test_random_init_of_spins(testCase)
+ % FM with S//a (hard axis) - no component along easy-axis
+ % optmagsteep would converge at local not global minimum
+ testCase.swobj.genmagstr('mode', 'direct', ...
+ 'S', [1 1; 0 0; 0 0], ...
+ 'k',[0,0,0], 'nExt', [2,1,1]);
+ testCase.swobj.optmagsteep('random', true, 'nRun', 200)
+ expected_magstr = testCase.default_magstr;
+ % first moment can be up/down (same energy) so adjust
+ % expected value to have same z-component sign on 1st S
+ expected_magstr.S = -sign(testCase.swobj.magstr.S(end,1))*...
+ expected_magstr.S;
+ testCase.verify_val(testCase.swobj.magstr, ...
+ expected_magstr, 'abs_tol', 1e-6);
+ testCase.verify_val(testCase.swobj.energy, -1.1);
+ end
+
+ function test_random_init_spins_if_no_initial_magstr(testCase)
+ testCase.swobj.optmagsteep('nExt', [2,1,1], 'nRun', 250);
+ testCase.verify_val(testCase.swobj.energy, -1.1);
+ end
+
+ function test_output_to_fid(testCase)
+ % generate ground state magnetic structure
+ testCase.swobj.genmagstr('mode', 'helical', 'S', [0; 0; 1], ...
+ 'k',[0.5,0,0], 'n', [0,1,0], ...
+ 'nExt', [2,1,1]);
+ mock_fprintf = sw_tests.utilities.mock_function('fprintf0');
+ fid = 3;
+ testCase.swobj.optmagsteep('fid', fid)
+ testCase.assertEqual(mock_fprintf.n_calls, 2);
+ % check fid used to write file
+ for irow = 1:mock_fprintf.n_calls
+ testCase.assertEqual(mock_fprintf.arguments{irow}{1}, fid)
+ end
+ end
+
+ function test_plot_moment_each_iteration(testCase, existing_plot)
+ testCase.disable_warnings('MATLAB:dispatcher:nameConflict');
+ if existing_plot
+ existing_fig = testCase.swobj.plot();
+ end
+ % generate ground state magnetic structure
+ testCase.swobj.genmagstr('mode', 'helical', 'S', [0; 0; -1], ...
+ 'k',[0.5,0,0], 'n', [0,1,0], ...
+ 'nExt', [2,1,1]);
+ mock_pause = sw_tests.utilities.mock_function('pause');
+ tpause = 1e-5;
+ testCase.swobj.optmagsteep('plot', true, 'pause', tpause);
+ % check magstr plotted (with arrow)
+ fig = swplot.activefigure;
+ testCase.assertEqual(numel(findobj(fig,'Tag', 'arrow')), 2)
+ if existing_plot
+ testCase.assertEqual(fig, existing_fig);
+ end
+ close(fig);
+ testCase.assertEqual(mock_pause.n_calls, 1);
+ testCase.assertEqual(mock_pause.arguments{1}, {tpause})
+ end
+
+ function test_convergence_stops_with_given_xtol(testCase)
+ testCase.swobj.genmagstr('mode', 'direct', ...
+ 'S', [0 0; 1 1; 1 1], ...
+ 'k',[0,0,0], 'nExt', [2,1,1]);
+ dM_tol = 1e-3;
+ opt_struct = testCase.swobj.optmagsteep('TolX', dM_tol, ...
+ 'saveAll', true);
+ testCase.verify_val(opt_struct.dM, dM_tol, 'abs_tol', 1e-4);
+ % check M saved for each iteration
+ testCase.assertEqual(size(opt_struct.M, 3), opt_struct.nRun);
+ end
+
+ function test_not_move_moments_in_field_more_than_Hmin(testCase)
+ testCase.swobj.genmagstr('mode', 'direct', ...
+ 'S', [0 0; 1 1; 1 1], ...
+ 'k',[0,0,0], 'nExt', [2,1,1]);
+ opt_struct = testCase.swobj.optmagsteep('Hmin', 2);
+ % check moments haven't moved
+ testCase.verify_val(opt_struct.dM, 0);
+ % check structure is unchanged
+ expected_magstr = testCase.default_magstr;
+ expected_magstr.S = [0, 0; 1, 1; 1, 1]/sqrt(2);
+ testCase.verify_val(testCase.swobj.magstr, expected_magstr);
+ end
+
+ function test_multiple_atoms_in_unit_cell(testCase)
+ testCase.swobj.addatom('r',[0.5; 0.5; 0.5],'S',1)
+ testCase.swobj.gencoupling('maxDistance', 6, 'dMin', 0.1);
+ testCase.swobj.addaniso('A'); % add again as cleared above
+ % add AFM coupling of spins in same unit cell
+ testCase.swobj.addcoupling('mat', 'J1', 'bond', 3);
+ testCase.swobj.genmagstr('mode', 'direct', 'S', [0 0; 0 0; 1 1], ...
+ 'k',[0, 0, 0]); % FM initial state
+
+ testCase.swobj.optmagsteep();
+
+ expected_magstr = testCase.default_magstr;
+ expected_magstr.N_ext = [1 1 1];
+ testCase.verify_val(testCase.swobj.magstr, expected_magstr);
+ testCase.verify_val(testCase.swobj.energy, -4.1)
+ end
+
+ end
+
+end
\ No newline at end of file
diff --git a/+sw_tests/+unit_tests/unittest_spinw_optmagstr.m b/+sw_tests/+unit_tests/unittest_spinw_optmagstr.m
new file mode 100644
index 000000000..df947af59
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_optmagstr.m
@@ -0,0 +1,303 @@
+classdef unittest_spinw_optmagstr < sw_tests.unit_tests.unittest_super
+
+ properties
+ tri = [];
+ afc = [];
+ opt_tri_mag_str = struct('nExt', int32([1 1 1]), ...
+ 'k', [1/3; 1/3; 0], ...
+ 'F', [1; 1i; 0]);
+ tri_optmagstr_args = {'func', @gm_planar, ...
+ 'xmin', [0 0 0 0 0 0], ...
+ 'xmax', [0 1/2 1/2 0 0 0]};
+ orig_rng_state = []
+ end
+ properties (TestParameter)
+ xparams = {'xmin', 'xmax', 'x0'};
+ optparams = {{'maxfunevals', 5}, ...
+ {'maxiter', 10}, ...
+ {'tolx', 1e-3}, ...
+ {'tolfun', 1e-4}, ...
+ {'maxfunevals', 5, 'maxiter', 10}};
+ end
+ methods (Static)
+ function [S, k, n] = optmagstr_custom_func(S0, x)
+ S = [1; 0; 0];
+ k = [1/3 1/3 0];
+ n = [0 0 1];
+ end
+ end
+ methods (TestClassSetup)
+ function set_seed(testCase)
+ testCase.orig_rng_state = rng;
+ rng('default');
+ end
+ end
+ methods (TestMethodSetup)
+ function setup_afm_tri(testCase)
+ testCase.tri = spinw();
+ testCase.tri.genlattice('lat_const',[3 3 9],'angled',[90 90 120]);
+ testCase.tri.addatom('r',[0 0 0],'S',1);
+ testCase.tri.gencoupling('maxDistance',10);
+ testCase.tri.addmatrix('value', 1,'label','J1');
+ testCase.tri.addcoupling('mat', 'J1','bond', 1);
+ end
+ function setup_afc(testCase)
+ % From tutorial 22
+ testCase.afc = spinw();
+ testCase.afc.genlattice('lat_const',[3 4 4],'angled',[90 90 90]);
+ testCase.afc.addatom('r',[0 0 0],'S',1);
+ testCase.afc.addmatrix('label', 'A', 'value', diag([0 0 0.1]));
+ testCase.afc.addmatrix('label','J1', 'value', 1);
+ testCase.afc.addmatrix('label','J2', 'value', 1/3);
+ testCase.afc.gencoupling;
+ testCase.afc.addcoupling('mat', 'J1', 'bond', 1);
+ testCase.afc.addcoupling('mat', 'J2', 'bond', 5);
+ testCase.afc.addaniso('A');
+ end
+ end
+ methods (TestMethodTeardown)
+ function reset_seed(testCase)
+ rng(testCase.orig_rng_state);
+ end
+ end
+ methods (Test)
+ function test_no_mag_atom_throws_error(testCase)
+ swobj = spinw();
+ testCase.verifyError(@() swobj.optmagstr, 'spinw:optmagstr:NoMagAtom');
+ end
+
+ function test_wrong_xparam_length_warns(testCase, xparams)
+ params = struct('func', @gm_planar, ...
+ 'xmin', [0 0 0 0 0 0], ...
+ 'xmax', [0 1/2 1/2 0 0 0], ...
+ 'x0', [0 1/4 1/4 0 0 0]);
+ xparam = params.(xparams);
+ params.(xparams) = xparam(1:end-1);
+ testCase.verifyWarning(...
+ @() testCase.tri.optmagstr(params), ...
+ 'spinw:optmagstr:WrongLengthXParam');
+ end
+
+ function test_optmagstr_tri_af_out_planar_xmin_xmax(testCase)
+ out = testCase.tri.optmagstr(testCase.tri_optmagstr_args{:});
+ xmin = testCase.tri_optmagstr_args{4};
+ xmax = testCase.tri_optmagstr_args{6};
+
+ % Note double {} in 'xname', 'boundary' below, otherwise MATLAB
+ % creates a cell array of structs
+ expected_out = struct( ...
+ 'x', [0 1/3 1/3 0 0 0], ...
+ 'e', -1.5, ...
+ 'exitflag', 1, ...
+ 'param', struct('epsilon', 1e-5, ...
+ 'func', @gm_planar, ...
+ 'boundary', {{'per', 'per', 'per'}}, ...
+ 'xmin', xmin, ...
+ 'xmax', xmax, ...
+ 'x0', [], ...
+ 'tolx', 1e-4, ...
+ 'tolfun', 1e-5, ...
+ 'maxfunevals', 1e7, ...
+ 'nRun', 1, ...
+ 'maxiter', 1e4, ...
+ 'title', 'Optimised magnetic structure using simplex search', ...
+ 'tid', 1), ...
+ 'fname', '2D planar structure', ...
+ 'xname', {{'Phi1_rad' 'kx_rlu' 'ky_rlu' 'kz_rlu' 'nTheta_rad' 'nPhi_rad'}}, ...
+ 'title', 'Optimised magnetic structure using simplex search');
+ % Some values will change on each call, just check they
+ % exist and are of the right type
+ assert(isa(out.datestart, 'char'));
+ assert(isa(out.dateend, 'char'));
+ assert(isa(out.output.iterations, 'double'));
+ assert(isa(out.output.funcCount, 'double'));
+ assert(isa(out.output.algorithm, 'char'));
+ assert(isa(out.output.message, 'char'));
+ testCase.verify_obj(out.obj, testCase.tri);
+ testCase.verify_val( ...
+ rmfield(out, {'output', 'datestart', 'dateend', 'obj'}), ...
+ expected_out, 'rel_tol', 1e-3);
+
+ testCase.verify_val(testCase.tri.mag_str, testCase.opt_tri_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_af_nExt_init(testCase)
+ % Test that if a magnetic structure is initialised with nExt,
+ % it is used in optmagstr
+ testCase.disable_warnings('spinw:genmagstr:SnParallel');
+ testCase.tri.genmagstr('mode', 'random', 'nExt', [3 1 1]);
+ testCase.tri.optmagstr('func', @gm_planar, ...
+ 'xmin', [0 pi/2 pi 0 0 0 0 0], ...
+ 'xmax', [0 pi 3*pi/2 0 1/2 0 0 0]);
+ testCase.verify_val(testCase.tri.mag_str.k, [0; 1/3; 0], ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_af_named_xparam(testCase)
+ % Test using named params
+ testCase.tri.optmagstr('func', @gm_planar, ...
+ 'Phi1_rad', [0 0], ...
+ 'kx_rlu', [0 0.5], ...
+ 'ky_rlu', [0 0.5], ...
+ 'kz_rlu', [0 0], ...
+ 'nTheta_rad', [0 0], ...
+ 'nPhi_rad', [0 0]);
+ testCase.verify_val(testCase.tri.mag_str, testCase.opt_tri_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_af_x0(testCase)
+ % Test initialising near a min converges to min
+ diff = 0.05;
+ testCase.tri.optmagstr('func', @gm_planar, 'x0', [0 1/3+diff 1/3+diff 0 0 0]);
+ expected_mag_str = testCase.opt_tri_mag_str;
+ expected_mag_str.F = [-1i; 1; 0];
+ % Use abs tol for F = 0
+ testCase.verify_val(testCase.tri.mag_str, expected_mag_str, ...
+ 'rel_tol', 1e-3, 'abs_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_af_custom_func(testCase)
+ testCase.tri.optmagstr('func', @testCase.optmagstr_custom_func, 'xmin', [0], 'xmax', [0]);
+ testCase.verify_val(testCase.tri.mag_str, testCase.opt_tri_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_af_custom_func_requires_xmin_and_xmax(testCase)
+ custom_func = @testCase.optmagstr_custom_func;
+ testCase.verifyError(@() testCase.tri.optmagstr('func', custom_func), ...
+ 'spinw:optmagtr:WrongInput');
+ testCase.verifyError(@() testCase.tri.optmagstr('func', custom_func, 'xmin', [0]), ...
+ 'spinw:optmagtr:WrongInput');
+ testCase.verifyError(@() testCase.tri.optmagstr('func', custom_func, 'xmax', [0]), ...
+ 'spinw:optmagtr:WrongInput');
+ end
+
+ function test_optmagstr_tri_af_custom_func_wrong_number_of_outputs(testCase)
+ function [S, k] = custom_func(S0, x)
+ S = [1; 0; 0];
+ k = [0 0 0];
+ end
+ testCase.verifyError(@() testCase.tri.optmagstr(...
+ 'func', @custom_func, 'xmin', [0], 'xmax', [0]), ...
+ 'MATLAB:TooManyOutputs');
+ end
+
+ function test_optmagstr_tri_af_custom_func_wrong_number_of_inputs(testCase)
+ function [S, k, n] = custom_func(S0)
+ S = [1; 0; 0];
+ k = [0 0 0];
+ n = [0 0 1];
+ end
+ testCase.verifyError(@() testCase.tri.optmagstr(...
+ 'func', @custom_func, 'xmin', [0], 'xmax', [0]), ...
+ 'MATLAB:TooManyInputs');
+ end
+
+ function test_optmagstr_tri_af_epsilon(testCase)
+ testCase.disable_warnings('spinw:genmagstr:SnParallel');
+ % Test that large epsilon doesn't rotate spins
+ testCase.tri.optmagstr('epsilon', 1.);
+ expected_mag_str = testCase.opt_tri_mag_str;
+ expected_mag_str.k = [0 0 0]';
+ expected_mag_str.F = [0 0 1]';
+ testCase.verify_val(testCase.tri.mag_str, expected_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_nRun(testCase)
+ % sw_timeit called in each nRun loop, and before and after
+ nRun = 4;
+ mock_sw_timeit = sw_tests.utilities.mock_function('sw_timeit');
+ testCase.tri.optmagstr(testCase.tri_optmagstr_args{:}, 'nRun', nRun);
+ testCase.assertEqual(mock_sw_timeit.n_calls, nRun + 2);
+ testCase.verify_val(testCase.tri.mag_str, testCase.opt_tri_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_set_title(testCase)
+ title = 'Test';
+ out = testCase.tri.optmagstr(testCase.tri_optmagstr_args{:}, 'title', title);
+ testCase.assertEqual(out.title, title);
+ testCase.verify_val(testCase.tri.mag_str, testCase.opt_tri_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_tri_tid(testCase)
+ sw_timeit_mock = sw_tests.utilities.mock_function('sw_timeit');
+ tid = 2;
+ testCase.tri.optmagstr(testCase.tri_optmagstr_args{:}, 'tid', tid);
+ % check tid used in timing
+ for irow = 1:sw_timeit_mock.n_calls
+ testCase.assertEqual(sw_timeit_mock.arguments{irow}{3}, tid)
+ end
+ testCase.verify_val(testCase.tri.mag_str, testCase.opt_tri_mag_str, ...
+ 'rel_tol', 1e-3);
+ end
+
+ function test_optmagstr_optimisation_params(testCase, optparams)
+ xmin = [0 0 0 0 0 0 0];
+ xmax = [pi/2 0 1/2 0 0 0 0];
+ mock_optimset = sw_tests.utilities.mock_function('optimset', ...
+ optimset('Display', 'off', optparams{:}));
+ testCase.disable_warnings('spinw:genmagstr:SnParallel');
+ testCase.afc.optmagstr('xmin', xmin, 'xmax', xmax, ...
+ optparams{:});
+ testCase.assertEqual(mock_optimset.n_calls, 1);
+ argslower = cellfun(@(c) lower(c), mock_optimset.arguments{1}(1:2:end), 'UniformOutput', false);
+ for ii = find(ismember(argslower, optparams(1:2:end)))
+ jj = find(ismember(optparams(1:2:end), argslower{ii}));
+ testCase.verifyEqual(mock_optimset.arguments{1}{2*ii}, optparams{2*jj});
+ end
+ end
+
+ function test_afc_no_init(testCase)
+ % Test without initialising parameters, doesn't converge k
+ converged_k = [0.385; 0; 0];
+ testCase.afc.optmagstr();
+ actual_k = testCase.afc.mag_str.k;
+ testCase.verifyGreaterThan(sum(abs(converged_k - actual_k)), 0.1);
+ end
+
+ function test_afc_gm_spherical3d(testCase)
+ xmin = [0 0 0 0 0 0 0];
+ xmax = [pi/2 0 1/2 0 0 0 0];
+ testCase.afc.optmagstr(...
+ 'func', @gm_spherical3d, 'xmin', xmin, 'xmax', xmax);
+ expected_k = [0.385; 0; 0];
+ testCase.verify_val(testCase.afc.mag_str.k, expected_k, ...
+ 'rel_tol', 1e-3, 'abs_tol', 1e-3);
+ end
+
+ function test_dm_multiatom_spherical3d(testCase)
+ sq = spinw();
+ sq.genlattice('lat_const', [4 4 4], 'angled', [90 90 90]);
+ sq.addatom('r', [0 0 0], 'S', 1);
+ sq.addatom('r', [0.5 0.5 0.5], 'S', 1);
+ sq.gencoupling('maxDistance', 10);
+ % This is the DM interaction, with the vector along [1 1 1]
+ sq.addmatrix('value', [1 1 1], 'label', 'DM');
+ sq.addcoupling('mat', 'DM', 'bond', 1);
+ sq.addmatrix('value', 1, 'label', 'J1');
+ sq.addcoupling('mat', 'J1', 'bond', 2);
+ testCase.disable_warnings('spinw:genmagstr:SnParallel');
+ % Sometimes fails to find min, run multiple times
+ sq.optmagstr('func', @gm_spherical3d, ...
+ 'xmin', [-pi/2 -pi -pi/2 -pi, 0 0 0, 0 0], ...
+ 'xmax', [pi/2 pi pi/2 pi, 0 0 0, 0 0], ...
+ 'nRun', 5);
+ spin_angles = {{90, 121}, {31, 180}}; % theta, phi
+ expected_F = zeros(3, length(spin_angles));
+ for i=1:length(spin_angles)
+ [theta, phi] = spin_angles{i}{:};
+ expected_F(1, i) = sind(theta)*cosd(phi); % a
+ expected_F(2, i) = sind(theta)*sind(phi); % b
+ expected_F(3, i) = cosd(theta); % c
+ end
+ testCase.verify_val(sq.mag_str.F, expected_F, 'abs_tol', 0.02);
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_spinw_spec2MDHisto.m b/+sw_tests/+unit_tests/unittest_spinw_spec2MDHisto.m
new file mode 100644
index 000000000..05f906e52
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_spec2MDHisto.m
@@ -0,0 +1,56 @@
+classdef unittest_spinw_spec2MDHisto < sw_tests.unit_tests.unittest_super
+ properties
+ swModel = [];
+ tmpdir = '';
+ testfilename = '';
+ nsteps = {100};
+ end
+
+ properties (TestParameter)
+ testpars = struct(...
+ 'test_1_0_0', struct('q0', [-3 0 0], 'qmax', [1 0 0], 'proj', [[1 0 0]' [0 1 0]' [0 0 1]'], 'nxs', 'test100mdh.nxs'), ...
+ 'test_1_1_0', struct('q0', [-3 -3 0], 'qmax', [1 1 0], 'proj', [[1 1 0]' [1 -1 0]' [0 0 1]'], 'nxs', 'test110mdh.nxs'), ...
+ 'test_1_1_1', struct('q0', [0 0 0], 'qmax', [1 1 1], 'proj', [[1 1 1]' [1 -1 0]' [1 1 -2]'], 'nxs', 'test111mdh.nxs'), ...
+ 'test_1_1_2', struct('q0', [0 0 2], 'qmax', [1 1 2], 'proj', [[1 1 0]' [1 -1 0]' [0 0 1]'], 'nxs', 'test112mdh.nxs'), ...
+ 'test_1_1_2_2', struct('q0', [0 0 2], 'qmax', [1 1 2], 'proj', [[1 -1 0]' [1 1 0]' [0 0 1]'], 'nxs', 'test112_2mdh.nxs'), ...
+ 'test_2_2_2', struct('q0', [2 2 2], 'qmax', [3 3 2], 'proj', [[1 1 0]' [1 -1 0]' [0 0 1]'], 'nxs', 'test222mdh.nxs'));
+ end
+
+ methods (TestClassSetup)
+ function setup_model(testCase)
+ testCase.swModel = sw_model('triAF', 1);
+ end
+ function setup_tempdir(testCase)
+ testCase.tmpdir = tempdir;
+ end
+ end
+
+ methods (TestMethodTeardown)
+ function remove_tmpdir(testCase)
+ delete(testCase.testfilename);
+ end
+ end
+
+ methods (Test)
+ function test_qdirs(testCase, testpars)
+ q0 = testpars.q0;
+ qmax = testpars.qmax;
+ proj = testpars.proj;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ spec = sw_egrid(spinwave(testCase.swModel, {q0 qmax testCase.nsteps{1}}));
+ % dproj = [(qmax-q0)/testCase.nsteps{1}, 1e-6, 1e-6];
+ dproj = [1, 1e-6, 1e-6];
+ testCase.testfilename = fullfile(testCase.tmpdir, testpars.nxs);
+ sw_spec2MDHisto(spec, proj, dproj, testCase.testfilename);
+ end
+
+ function test_non_ortho(testCase)
+ q0 = [0 0 2];
+ qdir = [1 1 0];
+ spec = sw_egrid(spinwave(testCase.swModel, {q0 q0+qdir testCase.nsteps{1}}));
+ proj = [qdir(:) [1 0 0]' [0 0 1]'];
+ dproj = [1e-6, norm((qdir-q0))/testCase.nsteps{1}, 1e-6];
+ verifyError(testCase,@() sw_spec2MDHisto(spec, proj, dproj, 'tmp/test_blank.nxs'), "read_struct:nonorthogonal")
+ end
+ end
+end
\ No newline at end of file
diff --git a/+sw_tests/+unit_tests/unittest_spinw_spinwave.m b/+sw_tests/+unit_tests/unittest_spinw_spinwave.m
new file mode 100644
index 000000000..35b2018ec
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_spinw_spinwave.m
@@ -0,0 +1,714 @@
+classdef unittest_spinw_spinwave < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for @spinw/spinwave.m
+
+ properties
+ swobj = [];
+ swobj_tri = [];
+ default_spinwave = struct('formfact', false, ...
+ 'incomm', false, ...
+ 'helical', false, ...
+ 'norm', false, ...
+ 'nformula', int32(0), ...
+ 'param', struct('notwin', true, ...
+ 'sortMode', true, ...
+ 'tol', 1e-4, ...
+ 'omega_tol', 1e-5, ...
+ 'hermit', true), ...
+ 'title', 'Numerical LSWT spectrum', ...
+ 'gtensor', false, ...
+ 'datestart', '', ...
+ 'dateend', '');
+ qh5 = [0:0.25:1; zeros(2,5)];
+ end
+
+ properties (TestParameter)
+ % Test directions and literal qpts work
+ qpts_h5 = {{[0 0 0], [1 0 0], 5}, ...
+ [0:0.25:1; zeros(2,5)]};
+ mex = {0, 'old', 1};
+ end
+
+ methods (TestClassSetup)
+ function setup_chain_model(testCase)
+ % Just create a very simple FM 1D chain model
+ testCase.swobj = spinw;
+ testCase.swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ testCase.swobj.addatom('r', [0 0 0],'S', 1, 'label', 'MNi2');
+ testCase.swobj.gencoupling('maxDistance', 7);
+ testCase.swobj.addmatrix('value', -eye(3), 'label', 'Ja');
+ testCase.swobj.addcoupling('mat', 'Ja', 'bond', 1);
+ testCase.swobj.genmagstr('mode', 'direct', 'k', [0 0 0], 'S', [0; 1; 0]);
+ end
+ function setup_tri_model(testCase)
+ % Create a simple triangular lattice model
+ testCase.swobj_tri = spinw;
+ testCase.swobj_tri.genlattice('lat_const', [4 4 6], 'angled', [90 90 120]);
+ testCase.swobj_tri.addatom('r', [0 0 0], 'S', 3/2, 'label', 'MCr3');
+ testCase.swobj_tri.genmagstr('mode', 'helical', 'S', [1; 0; 0], ...
+ 'n', [0 0 1], 'k', [1/3 1/3 0]);
+ J1 = 1;
+ testCase.swobj_tri.addmatrix('label','J1','value',J1);
+ testCase.swobj_tri.gencoupling;
+ testCase.swobj_tri.addcoupling('mat','J1','bond',1);
+ end
+ end
+ methods (TestMethodSetup)
+ function disable_mex_setup(testCase)
+ swpref.setpref('usemex', false);
+ end
+ end
+ methods (TestMethodTeardown)
+ function disable_mex_teardown(testCase)
+ swpref.setpref('usemex', false);
+ end
+ end
+
+ methods
+ function sw = get_expected_sw_qh5(testCase)
+ % Expected output for the chain model for 5 q-points from
+ % [0 0 0] to [1 0 0]
+ expected_hkl = testCase.qh5;
+ expected_Sab = zeros(3, 3, 2, 5);
+ Sab1 = [0.5 0 0.5j; 0 0 0; -0.5j 0 0.5];
+ Sab2 = [0.5 0 -0.5j; 0 0 0; 0.5j 0 0.5];
+ expected_Sab(:, :, 1, 1:4) = repmat(Sab1, 1, 1, 1, 4);
+ expected_Sab(:, :, 2, 5) = Sab1;
+ expected_Sab(:, :, 2, 1:4) = repmat(Sab2, 1, 1, 1, 4);
+ expected_Sab(:, :, 1, 5) = Sab2;
+
+ sw = testCase.default_spinwave;
+ sw.omega = [ 1e-5 2. 4. 2. -1e-5; ...
+ -1e-5 -2. -4. -2. 1e-5];
+ sw.Sab = expected_Sab;
+ sw.hkl = expected_hkl;
+ sw.hklA = expected_hkl*2/3*pi;
+ sw.obj = copy(testCase.swobj);
+ end
+ end
+ % Put tests with mocks in own block - prevent interference with other
+ % tests
+ methods (Test)
+ function test_noInput(testCase)
+ % Tests that if call spinwave with no input, it calls the help
+ % First mock the help call
+ help_function = sw_tests.utilities.mock_function('swhelp');
+ testCase.swobj.spinwave();
+ testCase.assertEqual(help_function.n_calls, 1);
+ testCase.assertEqual(help_function.arguments, {{'spinw.spinwave'}});
+ end
+ function test_sw_qh5_optmem(testCase)
+ qpts = testCase.qh5;
+ optmem = 3;
+ % Test that calculation is split into optmem chunks - sw_timeit
+ % is called on each chunk plus once at beginning and end of
+ % function. This is a bit fragile, may need to change the
+ % target function, number of calls, or not check at all if
+ % spinwave is refactored
+ sw_timeit_mock = sw_tests.utilities.mock_function('sw_timeit');
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(qpts, 'optmem', optmem);
+ testCase.assertEqual(sw_timeit_mock.n_calls, optmem + 2);
+ % Test that with optmem gives the same result as without
+ expected_sw = testCase.get_expected_sw_qh5();
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_zero_freemem_warns(testCase)
+ % Mock sw_freemem to return 0 to trigger warning
+ sw_freemem_mock = sw_tests.utilities.mock_function( ...
+ 'sw_freemem', 0);
+ sw_out = testCase.verifyWarning(...
+ @() testCase.swobj.spinwave(testCase.qh5), ...
+ 'spinw:spinwave:FreeMemSize');
+ testCase.verify_spinwave(sw_out, testCase.get_expected_sw_qh5);
+ end
+ function test_sw_qh5_low_freemem(testCase)
+ % Check with low free memory calculation still attempted
+ sw_freemem_mock = sw_tests.utilities.mock_function( ...
+ 'sw_freemem', 100);
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(testCase.qh5);
+ testCase.verify_spinwave(sw_out, testCase.get_expected_sw_qh5);
+ end
+ function test_sw_qh5_fid(testCase)
+ fprintf_mock = sw_tests.utilities.mock_function('fprintf0');
+ fid = 3;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(testCase.qh5, 'fid', fid);
+ % check fid used to write file
+ for irow = 1:fprintf_mock.n_calls
+ testCase.assertEqual(fprintf_mock.arguments{irow}{1}, fid)
+ end
+ expected_sw = testCase.get_expected_sw_qh5();
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_tid(testCase)
+ sw_timeit_mock = sw_tests.utilities.mock_function('sw_timeit');
+ tid = 2;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(testCase.qh5, 'tid', tid);
+ % check tid used in timing
+ for irow = 1:sw_timeit_mock.n_calls
+ testCase.assertEqual(sw_timeit_mock.arguments{irow}{3}, tid)
+ end
+ expected_sw = testCase.get_expected_sw_qh5();
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ end
+ methods (Test)
+ function test_sw_qh5(testCase, qpts_h5, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(qpts_h5);
+ expected_sw = testCase.get_expected_sw_qh5();
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_sortmode(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(testCase.qh5, 'sortMode', false);
+ expected_sw = testCase.get_expected_sw_qh5();
+ % Sortmode swaps the last 2 modes
+ expected_sw.omega([1 2], 5) = expected_sw.omega([2 1], 5);
+ expected_sw.Sab(:, :, [1 2], 5) = expected_sw.Sab(:, :, [2 1], 5);
+ expected_sw.param.sortMode = false;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_nformula(testCase, mex)
+ swpref.setpref('usemex', mex);
+ % Create copy to avoid changing obj for other tests
+ swobj = copy(testCase.swobj);
+ nformula = int32(2);
+ swobj.unit.nformula = nformula;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out_nformula = swobj.spinwave(testCase.qh5);
+ expected_sw = testCase.swobj.spinwave(testCase.qh5);
+ expected_sw.Sab = expected_sw.Sab/2;
+ expected_sw.obj.unit.nformula = nformula;
+ expected_sw.nformula = nformula;
+ testCase.verify_spinwave(sw_out_nformula, expected_sw);
+ end
+ function test_sw_qh5_periodic(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ % Test qpts in different BZ give same omega, Sab
+ qpts = testCase.qh5 + 1;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(qpts);
+ expected_sw = testCase.get_expected_sw_qh5();
+ expected_sw.hkl = qpts;
+ expected_sw.hklA = [qpts(1, :)*2/3; qpts(2:end, :)*0.25 ]*pi;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_perpendicular(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ % Test qpts in perpendicular direction give flat modes
+ qpts = [zeros(1, 5); 0:0.25:1; 0:0.25:1];
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(qpts);
+ expected_sw = testCase.get_expected_sw_qh5();
+ expected_sw.hkl = qpts;
+ expected_sw.hklA = [qpts(1, :)*2/3; qpts(2:end, :)*0.25 ]*pi;
+ expected_sw.omega = 1e-5*[ones(1, 5); -ones(1, 5)];
+ expected_sw.Sab(1, 3, :, 5) = -expected_sw.Sab(1, 3, :, 5);
+ expected_sw.Sab(3, 1, :, 5) = -expected_sw.Sab(3, 1, :, 5);
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_saveH_saveV(testCase, mex)
+ swpref.setpref('usemex', mex);
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(testCase.qh5, ...
+ 'saveV', true, 'saveH', true);
+ expected_V = repmat(eye(2), 1, 1, 5);
+ expected_H = zeros(2, 2, 5);
+ expected_H(:, :, [2 4]) = 2*repmat(eye(2), 1, 1, 2);
+ expected_H(:, :, 3) = 4*eye(2);
+
+ expected_sw = testCase.get_expected_sw_qh5();
+ expected_sw.V = expected_V;
+ expected_sw.H = expected_H;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_title(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ title = 'Example title';
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(testCase.qh5, 'title', title);
+ expected_sw = testCase.get_expected_sw_qh5();
+ expected_sw.title = title;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_with_nExt(testCase, mex)
+ swpref.setpref('usemex', mex);
+ % Create copy to avoid changing obj for other tests
+ afm_chain = copy(testCase.swobj);
+ afm_chain.matrix.mat = eye(3);
+ afm_chain.genmagstr('mode', 'direct', 'k',[1/2 0 0], ...
+ 'S',[0 0; 1 -1;0 0], 'nExt',[2 1 1]);
+ sw_afm = afm_chain.spinwave(testCase.qh5);
+ omega_vals = [0 2. 0 -2. 0];
+ expected_omega = [omega_vals; omega_vals; -omega_vals; -omega_vals];
+ testCase.verify_val(sw_afm.omega, expected_omega, 'abs_tol', 1e-7);
+ end
+ function test_sw_with_multiple_matom(testCase, mex)
+ swpref.setpref('usemex', mex);
+ fe_cu_chain = spinw;
+ fe_cu_chain.genlattice('lat_const', [3 8 4], 'sym', 'P 1');
+ fe_cu_chain.addatom('label', 'MCu2', 'r', [0 0 0]);
+ fe_cu_chain.addatom('label', 'MFe2', 'r', [0 1/2 0]);
+ fe_cu_chain.gencoupling;
+ fe_cu_chain.addmatrix('label', 'J_{Cu-Cu}', 'value', 1);
+ fe_cu_chain.addmatrix('label','J_{Fe-Fe}', 'value', 1);
+ fe_cu_chain.addmatrix('label', 'J_{Cu-Fe}', 'value', -0.1)
+ fe_cu_chain.addcoupling('mat','J_{Cu-Cu}','bond',1);
+ fe_cu_chain.addcoupling('mat','J_{Fe-Fe}','bond',2);
+ fe_cu_chain.addcoupling('mat','J_{Cu-Fe}','bond',[4 5]);
+ fe_cu_chain.genmagstr('mode','helical','S',[0 0;1 1;0 0],'k',[1/2 0 0])
+
+ testCase.disable_warnings('spinw:magstr:NotExact', 'spinw:spinwave:Twokm');
+ sw_out = fe_cu_chain.spinwave(testCase.qh5, 'sortMode', false, 'omega_tol', 1e-12);
+ om1 = 4.11473;
+ om2 = 1.36015;
+ om3 = 1.38527;
+ expected_omega = zeros(12, 5);
+ expected_omega(1:6, [1 3 5])= repmat([om2 0 0 -om2 om2 0]', 1, 3);
+ expected_omega(1:6, [2 4])= repmat([om1 om3 -om3 -om1 om1 om3]', 1, 2);
+ expected_omega(7:end, :) = -expected_omega(6:-1:1, :);
+ testCase.verify_val(sw_out.omega, expected_omega, 'abs_tol', 5e-6);
+ end
+ function test_sw_saveSabp_commensurate_warns(testCase, mex)
+ swpref.setpref('usemex', mex);
+ sw = testCase.verifyWarning(...
+ @() testCase.swobj.spinwave(testCase.qh5, 'saveSabp', true), ...
+ 'spinw:spinwave:CommensurateSabp');
+ testCase.verify_spinwave(sw, testCase.get_expected_sw_qh5);
+ end
+ function test_sw_incom_in_supercell_warns(testCase, mex)
+ swpref.setpref('usemex', mex);
+ cycloid = spinw;
+ cycloid.genlattice('lat_const',[3 8 10], 'sym',0);
+ cycloid.addatom('r',[0 0 0],'S',1,'label','Cu1');
+ cycloid.gencoupling('maxDistance',7);
+ cycloid.addmatrix('label','J2','value', 1);
+ cycloid.addcoupling('mat','J2','bond',2);
+ % modulation of [1/4 0 0] gets transformed in genmagstr to
+ % [0.5 0 0] for nExt = [2 1 1]
+ testCase.disable_warnings('spinw:genmagstr:UCExtNonSuff');
+ cycloid.genmagstr('mode', 'helical', ...
+ 'S', [1 0; 0 1; 0 0], 'n', [0 0 1], ...
+ 'nExt', [2 1 1], 'k', [0.25, 0, 0]);
+ testCase.verifyWarning(@() cycloid.spinwave({[0 0 0], [1 0 0], 30}), ...
+ {'spinw:spinwave:IncommKinSupercell', ...
+ 'spinw:spinwave:Twokm'});
+ end
+ function test_sw_qh5_saveSabp_incommensurate(testCase, mex)
+ swpref.setpref('usemex', mex);
+ qpts = testCase.qh5;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj_tri.spinwave(qpts, ...
+ 'saveSabp', true);
+ expected_Sabp = zeros(3, 3, 2, 5);
+ expected_Sabp(:, :, :, [1 5]) = repmat( ...
+ diag([435.71079, 435.71079, 6.45497e-4]), 1, 1, 2, 2);
+ expected_Sabp(:, :, :, [2 4]) = repmat( ...
+ diag([0.59293, 0.59293, 0.47434]), 1, 1, 2, 2);
+ expected_Sabp(:, :, :, 3) = repmat( ...
+ diag([0.1875, 0.1875, 1.5]), 1, 1, 2, 1);
+ omegap_vals = [1.16190e-2 4.74342 3];
+ expected_omegap = [ omegap_vals omegap_vals(2:-1:1); ...
+ -omegap_vals -omegap_vals(2:-1:1)];
+
+ testCase.verify_val(sw_out.Sabp, expected_Sabp, 'rel_tol', 1e-5);
+ testCase.verify_val(sw_out.omegap, expected_omegap, 'rel_tol', 1e-5);
+ end
+ function test_sw_qh5_fitmode(testCase, mex)
+ swpref.setpref('usemex', mex);
+ qpts = testCase.qh5;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(qpts, 'fitmode', true);
+ % fitmode automatically turns off sortMode
+ expected_sw = testCase.swobj.spinwave(qpts, 'sortMode', false);
+ expected_sw = rmfield(expected_sw, {'obj', 'datestart', 'dateend'});
+ obj = testCase.swobj;
+ expected_sw.obj = struct('single_ion', obj.single_ion, 'twin', obj.twin, ...
+ 'unit', obj.unit, 'basisvector', obj.basisvector, 'nmagext', obj.nmagext);
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_incommensurate(testCase, mex)
+ swpref.setpref('usemex', mex);
+ if mex
+ err ='chol_omp:notposdef';
+ else
+ err= 'spinw:spinwave:NonPosDefHamiltonian';
+ end
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ % Tests that incommensurate calculation is ok
+ hkl = {[0 0 0] [0 1 0] [1 0 0] 5};
+ % Create copy to avoid changing obj for other tests
+ swobj = copy(testCase.swobj);
+ commensurate_spec = swobj.spinwave(hkl);
+
+ % Test incomm 2nd neighbour afm chain fails with only nearest
+ % neighbour interactions
+ swobj.genmagstr('mode', 'helical', 'k', [0.25 0 0], ...
+ 'n', [0 0 1], 'S', [1; 0; 0]);
+ testCase.verifyError(@() swobj.spinwave(hkl), err);
+ % Add 2nd neighbour interactions and test this incommensurate
+ % structure produces 3x number of modes as commensurate
+ swobj.addmatrix('value', 2, 'label', 'Jb');
+ swobj.addcoupling('mat', 'Jb', 'bond', 2);
+ incomm_spec = swobj.spinwave(hkl);
+ testCase.assertEqual(size(incomm_spec.omega, 1), ...
+ size(commensurate_spec.omega, 1) * 3);
+ end
+ function test_twin(testCase, mex)
+ swpref.setpref('usemex', mex);
+ % Tests that setting twins gives correct outputs
+ % Create copy to avoid changing obj for other tests
+ swobj_twin = copy(testCase.swobj);
+ swobj_twin.addtwin('axis', [0 0 1], 'phid', [60 120], 'vol', [1 2]);
+ rotc = swobj_twin.twin.rotc;
+ hkl = [1 2; 3 4; 5 6];
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = swobj_twin.spinwave(hkl);
+
+ expected_sw = testCase.default_spinwave;
+ expected_sw.param.notwin = false;
+ expected_sw.omega = {};
+ expected_sw.Sab = {};
+ expected_sw.obj = copy(swobj_twin);
+ expected_sw.hkl = hkl;
+ expected_sw.hklA = [2/3 4/3; 0.75, 1; 1.25, 1.5]*pi;
+ % Recalculate without twins for each set of hkl's and compare
+ [qTwin, rotQ] = swobj_twin.twinq(hkl);
+ for itwin = 1:3
+ sw_single = testCase.swobj.spinwave(qTwin{itwin});
+ expected_sw.omega = [expected_sw.omega sw_single.omega];
+ rot = rotc(:, :, itwin);
+ rot_Sab = zeros(3, 3, 2, 2);
+ % Twin Sab is a rotation of single Sab
+ for imode = 1:2
+ for iq = 1:2
+ rot_Sab(:, :, imode, iq) = rot*sw_single.Sab(:, :, imode, iq)*rot';
+ end
+ end
+ expected_sw.Sab = [expected_sw.Sab rot_Sab];
+ end
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_notwin(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ % Create copy to avoid changing obj for other tests
+ swobj_twin = copy(testCase.swobj);
+ swobj_twin.addtwin('axis', [0 0 1], 'phid', [60 120], 'vol', [1 2]);
+ qpts = testCase.qh5;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = swobj_twin.spinwave(qpts, 'notwin', true);
+ % Test even if twin is added to structure it is not actually
+ % calculated if notwin is specified
+ expected_sw = testCase.get_expected_sw_qh5;
+ expected_sw.obj.twin = swobj_twin.twin;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_cmplxBase_equivalent_with_tri(testCase, mex)
+ swpref.setpref('usemex', mex);
+ % For this structure, both cmplxBase true and false give the
+ % same e-vectors so should give the same result
+ qpts = {[0 0 0], [1 0 0], 5};
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj_tri.spinwave(qpts, 'cmplxBase', false);
+ sw_out_cmplx = testCase.swobj_tri.spinwave(qpts, 'cmplxBase', true);
+ testCase.verify_spinwave(sw_out_cmplx, sw_out);
+ end
+ function test_cmplxBase_fails_with_chain(testCase, mex)
+ swpref.setpref('usemex', mex);
+ if ischar(mex)
+ err ='chol_omp:notposdef';
+ elseif mex == 1
+ err ='swloop:notconverge';
+ else
+ err= 'spinw:spinwave:NonPosDefHamiltonian';
+ end
+ % Test cmplxBase actually does something - it should fail with
+ % chain
+ qpts = {[0 0 0], [1 0 0], 5};
+ testCase.verifyError(...
+ @() testCase.swobj.spinwave(qpts, 'cmplxBase', true), ...
+ err);
+ end
+ function test_formfact(testCase, mex)
+ swpref.setpref('usemex', mex);
+ qpts = {[0 0 0] [10 5 1] 19};
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_ff = testCase.swobj.spinwave(qpts, 'formfact', true);
+ % Test that Sab with the form factor (ff) is explicitly the
+ % same as Sab with no ff multiplied by ff
+ % ff calculated with sw_mff and the scaling is F(Q)^2.
+ expected_sw = testCase.swobj.spinwave(qpts, 'formfact', false);
+ ff = sw_mff(testCase.swobj.unit_cell.label{1}, sw_ff.hklA);
+ expected_sw.Sab = expected_sw.Sab.*permute(ff.^2, [1 3 4 2]);
+ expected_sw.formfact = true;
+
+ testCase.verify_spinwave(sw_ff, expected_sw);
+ end
+ function test_formfactfun(testCase, mex)
+ swpref.setpref('usemex', mex);
+ function F = formfactfun(atom_label, Q)
+ F = sum(Q, 1);
+ end
+ qpts = {[0 0 0] [10 5 1] 19};
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_ff = testCase.swobj.spinwave(qpts, 'formfact', true, ...
+ 'formfactfun', @formfactfun);
+ % Test that Sab with the form factor (ff) is explicitly the
+ % same as Sab with no ff multiplied by ff
+ expected_sw = testCase.swobj.spinwave(qpts, 'formfact', false);
+ ff = formfactfun(testCase.swobj.unit_cell.label{1}, sw_ff.hklA);
+ expected_sw.Sab = expected_sw.Sab.*permute(ff.^2, [1 3 4 2]);
+ expected_sw.formfact = true;
+
+ testCase.verify_spinwave(sw_ff, expected_sw, 'rel_tol', 1e-15);
+ end
+ function test_gtensor(testCase, mex)
+ swpref.setpref('usemex', mex);
+ qpts = {[0 0 0], [1 1 1], 5};
+ gmat = diag([1, 2, 3]);
+ % Create copy to avoid changing obj for other tests
+ swobj_g = copy(testCase.swobj);
+ swobj_g.addmatrix('label','g_1','value', gmat)
+ swobj_g.addg('g_1')
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_g = swobj_g.spinwave(qpts, 'gtensor', true);
+ % Also check that it warns that gtensor is not being used
+ expected_sw = testCase.verifyWarning(...
+ @() swobj_g.spinwave(qpts, 'gtensor', false), ...
+ 'spinw:spinwave:NonZerogTensor');
+ expected_sw.Sab = expected_sw.Sab.*[1 2 3; 2 4 6; 3 6 9];
+ expected_sw.gtensor = true;
+ testCase.verify_spinwave(sw_g, expected_sw);
+ end
+ function test_gtensor_incomm(testCase, mex)
+ swpref.setpref('usemex', mex);
+ qpts = {[0 0 0], [1 1 1], 5};
+ gmat = diag([1, 2, 3]);
+ % Create copy to avoid changing obj for other tests
+ swobj_g = copy(testCase.swobj_tri);
+ swobj_g.addmatrix('label','g_1','value', gmat)
+ swobj_g.addg('g_1')
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_g = swobj_g.spinwave(qpts, 'gtensor', true);
+ % Check that Sab with g is same as Sab without g but multiplied
+ % by g in each direction
+ expected_sw = swobj_g.spinwave(qpts);
+ expected_sw.Sab = expected_sw.Sab.*[2.25 2.25 4.5; ...
+ 2.25 2.25 4.5; ...
+ 4.5 4.5 9];
+ expected_sw.gtensor = true;
+ testCase.verify_spinwave(sw_g, expected_sw, 'rel_tol', 1e-15);
+ end
+ function test_hermit(testCase, mex)
+ swpref.setpref('usemex', mex);
+ % Create copy to avoid changing obj for other tests
+ swobj_h = copy(testCase.swobj);
+ % Tests that the 'hermit' option to switch to a non-hermitian calculation works
+ % First make the model non-Hermitian by adding a large axial SIA perpendicular to the spins
+ swobj_h.addmatrix('label', 'K', 'value', diag([-1 0 0]));
+ swobj_h.addaniso('K');
+ hkl = {[0 0 0] [0 1 0] [1 0 0] 50};
+ % Check that calling it with 'hermit' on gives an error
+ testCase.assertError(@() swobj_h.spinwave(hkl, 'hermit', true), ?MException);
+ % Now check that there are imaginary eigenvalues/energies in the output with 'hermit' off
+ spec = swobj_h.spinwave(hkl, 'hermit', false);
+ testCase.assertGreaterThan(sum(abs(imag(spec.omega(:)))), 0);
+ end
+ function test_sw_qh5_tol(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ tol = 5e-4;
+ qpts = testCase.qh5;
+ swobj_tol = copy(testCase.swobj);
+ % Generate magstr that deviates slightly from commensurate
+ swobj_tol.genmagstr('mode', 'helical', 'k', [tol 0 0], ...
+ 'n', [0 0 1], 'S', [0; 1; 0]);
+ if ~mex
+ % Check that without tol it is incommensurate - causes error
+ testCase.verifyError(...
+ @() swobj_tol.spinwave(qpts), ...
+ 'spinw:spinwave:NonPosDefHamiltonian');
+ end
+ % Check that with tol is approximated to commensurate
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = swobj_tol.spinwave(qpts, 'tol', tol);
+ expected_sw = testCase.get_expected_sw_qh5;
+ expected_sw.obj = swobj_tol;
+ expected_sw.param.tol = tol;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_sw_qh5_omega_tol(testCase, mex)
+ testCase.assumeNotEqual(mex, 1); % swloop outputs c.c. Sab so fails here
+ swpref.setpref('usemex', mex);
+ if mex
+ err ='chol_omp:notposdef';
+ else
+ err= 'spinw:spinwave:NonPosDefHamiltonian';
+ end
+ qpts = testCase.qh5;
+ % Check that with no added omega_tol Hamiltonian isn't positive
+ % definite - causes error
+ testCase.verifyError(...
+ @() testCase.swobj.spinwave(qpts, 'omega_tol', 0), ...
+ err);
+ % Check with omega_tol the omega is changed appropriately
+ omega_tol = 1;
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.swobj.spinwave(qpts, 'omega_tol', omega_tol);
+ expected_sw = testCase.get_expected_sw_qh5;
+ expected_sw.omega(:, 1) = [omega_tol -omega_tol];
+ expected_sw.omega(:, end) = [-omega_tol omega_tol];
+ expected_sw.param.omega_tol = omega_tol;
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ function test_biquadratic_with_incomm_causes_error(testCase)
+ swobj_tri = copy(testCase.swobj_tri);
+ % Set coupling to biquadratic
+ swobj_tri.coupling.type(:) = 1;
+ testCase.verifyError(...
+ @() swobj_tri.spinwave(testCase.qh5), ...
+ 'spinw:spinwave:Biquadratic');
+ end
+ function test_no_magstr_causes_error(testCase)
+ swobj = spinw;
+ swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ swobj.addatom('r', [0 0 0]);
+ testCase.verifyError(...
+ @() swobj.spinwave(testCase.qh5), ...
+ 'spinw:spinwave:NoMagneticStr');
+ end
+ function test_mex_prefs_nthreads(testCase)
+ % Tests that when nthreads set to -1 mex will parallelise over all cores
+ hkl = {[0 0 0] [1 0 0] [0 1 0] 2000};
+ nCores = maxNumCompThreads();
+ if nCores > 2
+ % If only 2 cores, times may not be different enough to notice
+ mexpref = swpref.getpref('usemex');
+ nthrprf = swpref.getpref('nthread');
+ swpref.setpref('usemex', 1);
+ swpref.setpref('nthread', -1);
+ swobj = copy(testCase.swobj_tri);
+ spec1 = swobj.spinwave({[0 0 0] [1 1 1] 50}); % Run once for the JIT compiler
+ t0 = tic; spec1 = swobj.spinwave(hkl); t1 = toc(t0);
+ swpref.setpref('nthread', 1);
+ t0 = tic; spec1 = swobj.spinwave(hkl); t2 = toc(t0);
+ testCase.verifyTrue(t1 < t2);
+ swpref.setpref('usemex', mexpref.val);
+ swpref.setpref('nthread', nthrprf.val);
+ end
+ end
+ function test_mex_prefs_nspinlarge(testCase)
+ swobj = copy(testCase.swobj_tri);
+ swobj.addmatrix('label', 'K', 'value', 0.1);
+ swobj.addaniso('K');
+ mexpref = swpref.getpref('usemex');
+ nslpref = swpref.getpref('nspinlarge');
+ swpref.setpref('usemex', 1);
+ omega = [1e-5; -1e-5];
+ Sab = reshape([[0.5 0 -0.5j; 0 0 0; 0.5j 0 0.5] [0.5 0 0.5j; 0 0 0; -0.5j 0 0.5]], 3, 3, 2);
+ swloop_mock = sw_tests.utilities.mock_function('swloop', {omega, Sab, 1, 0});
+ spec1 = swobj.spinwave([1; 1; 1], 'hermit', false);
+ testCase.assertEqual(swloop_mock.n_calls, 1);
+ swobj.genmagstr('nExt', 0.01); % Convert to commensurate with 9 spins in unit cell (3x3)
+ swpref.setpref('nspinlarge', 5);
+ spec1 = swobj.spinwave([1; 1; 1], 'hermit', false);
+ testCase.assertEqual(swloop_mock.n_calls, 1); % Make sure swloop wasn't called this time
+ swpref.setpref('usemex', mexpref.val);
+ swpref.setpref('nspinlarge', nslpref.val);
+ end
+ function test_neutron_output(testCase, mex)
+ swpref.setpref('usemex', mex);
+ objs = {testCase.swobj, testCase.swobj_tri};
+ hkl = {[0 0 0] [1 0 0] [0 1 0] 50};
+ for ii = 1:numel(objs)
+ swobj = copy(objs{ii});
+ spec0 = sw_neutron(swobj.spinwave(hkl, 'sortMode', false));
+ spec1 = swobj.spinwave(hkl, 'neutron_output', true);
+ testCase.verify_val(spec0.omega, spec1.omega, 'abs_tol', 1e-8);
+ testCase.verify_val(spec0.Sperp, spec1.Sperp, 'abs_tol', 1e-8);
+ end
+ end
+ function test_neutron_twin(testCase, mex)
+ swpref.setpref('usemex', mex);
+ swobj = copy(testCase.swobj);
+ swobj.addtwin('axis', [1 1 1], 'phid', 54);
+ swobj.unit.nformula = int32(2);
+ hkl = {[1 0 0] [0 1 0] [0 0 0] 50};
+ spec0 = sw_neutron(swobj.spinwave(hkl, 'sortMode', false));
+ spec1 = swobj.spinwave(hkl, 'neutron_output', true);
+ testCase.verify_val(spec0.omega{1}, spec1.omega{1}, 'abs_tol', 1e-8);
+ testCase.verify_val(spec0.omega{2}, spec1.omega{2}, 'abs_tol', 1e-8);
+ testCase.verify_val(spec0.Sperp{1}, spec1.Sperp{1}, 'abs_tol', 1e-8);
+ testCase.verify_val(spec0.Sperp{2}, spec1.Sperp{2}, 'abs_tol', 1e-8);
+ end
+ function test_fastmode(testCase, mex)
+ swpref.setpref('usemex', mex);
+ swobj = copy(testCase.swobj);
+ hkl = {[0 0 0] [1 0 0] [0 1 0] 50};
+ spec0 = sw_neutron(swobj.spinwave(hkl));
+ spec1 = swobj.spinwave(hkl, 'fastmode', true);
+ spec2 = swobj.spinwave(hkl, 'hermit', false, 'fastmode', true);
+ nMode = size(spec1.omega, 1);
+ testCase.verify_val(size(spec0.omega, 1), 2*nMode);
+ testCase.verify_val(spec0.omega(1:nMode,:), spec1.omega, 'abs_tol', 1e-4);
+ testCase.verify_val(spec0.omega(1:nMode,:), spec2.omega, 'abs_tol', 1e-4);
+ testCase.verify_val(spec0.Sperp(1:nMode,:), spec1.Sperp, 'abs_tol', 1e-8);
+ testCase.verify_val(spec0.Sperp(1:nMode,:), spec2.Sperp, 'abs_tol', 1e-8);
+ end
+ function test_fastmode_mex_nomex(testCase)
+ % Tests that fast mode gives same results for mex, no-mex and non-fastmode
+ swobj = copy(testCase.swobj);
+ hkl = {[0 0 0] [1 0 0] [0 1 0] 50};
+ swpref.setpref('usemex', 0);
+ spec0 = sw_neutron(swobj.spinwave(hkl));
+ spec1 = swobj.spinwave(hkl, 'fastmode', true);
+ swpref.setpref('usemex', 1);
+ spec2 = swobj.spinwave(hkl, 'fastmode', true);
+ nMode = size(spec1.omega, 1);
+ testCase.verify_val(size(spec0.omega, 1), 2*nMode);
+ testCase.verify_val(spec0.omega(1:nMode,:), spec2.omega, 'abs_tol', 1e-4);
+ testCase.verify_val(spec0.omega(1:nMode,:), spec1.omega, 'abs_tol', 1e-4);
+ testCase.verify_val(spec0.Sperp(1:nMode,:), spec1.Sperp, 'abs_tol', 1e-8);
+ testCase.verify_val(spec0.Sperp(1:nMode,:), spec2.Sperp, 'abs_tol', 1e-8);
+ end
+ function test_fastmode_memory_chunked_nomex(testCase)
+ swpref.setpref('usemex', 0);
+ sw_obj = copy(testCase.swobj);
+ hkl = {[0 0 0] [1 0 0] [0 1 0] 50};
+ % check that executes with no error
+ spec_fast = sw_obj.spinwave(hkl, 'hermit', false, 'fastmode', true, 'optmem', 2);
+ % eval with fastmode=false for comparison
+ spec_slow = sw_obj.spinwave(hkl, 'hermit', false, 'fastmode', false);
+ testCase.verify_val(spec_fast.omega, spec_slow.omega(1,:))
+ end
+ end
+ methods (Test, TestTags = {'Symbolic'})
+ function test_sw_symbolic_no_qpts(testCase)
+ swobj = copy(testCase.swobj);
+ swobj.symbolic(true);
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ sw_out = testCase.verifyWarning(@() swobj.spinwave(), 'spinw:spinwave:MissingInput');
+
+ symstr = '-Ja*exp(-pi*h*2i)*(exp(pi*h*2i) - 1)^2';
+ expected_sw.ham = [str2sym(symstr) sym(0); ...
+ sym(0) str2sym(symstr)];
+ expected_sw.omega = [str2sym(symstr(2:end)); str2sym(symstr)];
+ expected_sw.obj = swobj;
+ expected_sw.datestart = '';
+ expected_sw.dateend = '';
+ expected_sw.title = 'Symbolic LSWT spectrum';
+ testCase.verify_spinwave(sw_out, expected_sw);
+ end
+ end
+end
diff --git a/+sw_tests/+unit_tests/unittest_super.m b/+sw_tests/+unit_tests/unittest_super.m
new file mode 100644
index 000000000..7d40536da
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_super.m
@@ -0,0 +1,112 @@
+classdef unittest_super < matlab.mock.TestCase
+ properties
+ cleanup_warnings = {};
+ end
+ methods (Static)
+ function udir = get_unit_test_dir()
+ udir = fullfile('.', 'test_data', 'unit_tests');
+ end
+ end
+ methods
+ function obj = load_spinw(testCase, filename)
+ obj = load(fullfile(testCase.get_unit_test_dir(), 'spinw', filename));
+ obj = obj.data;
+ end
+ function obj = load_figure(testCase, filename)
+ path = fullfile(testCase.get_unit_test_dir(), 'Figure', filename);
+ obj = openfig(path, 'invisible');
+ end
+ function verify_obj(testCase, actual_obj, expected_obj, varargin)
+ testCase.assertClass(actual_obj, class(expected_obj));
+ all_fieldnames = fieldnames(expected_obj);
+ if isa(expected_obj, 'struct')
+ all_fieldnames = union(all_fieldnames, fieldnames(actual_obj));
+ end
+ for i=1:length(all_fieldnames)
+ field = all_fieldnames(i);
+ if strcmp(field{:}, "cache")
+ continue;
+ end
+ expected_value = expected_obj.(field{:});
+ actual_value = actual_obj.(field{:});
+ if isstruct(expected_value)
+ testCase.verify_obj(actual_value, expected_value, varargin{:});
+ else
+ testCase.verify_val(actual_value, expected_value, ...
+ 'field', field{:}, varargin{:});
+ end
+ end
+ end
+ function verify_val(testCase, actual_val, expected_val, varargin)
+ import matlab.unittest.constraints.IsEqualTo
+ import matlab.unittest.constraints.RelativeTolerance
+ import matlab.unittest.constraints.AbsoluteTolerance
+
+ field = "";
+ abs_tol = 10*eps; % abs_tol comparsion is default
+ rel_tol = 0;
+ for iarg = 1:2:numel(varargin)
+ switch varargin{iarg}
+ case 'abs_tol'
+ abs_tol = varargin{iarg + 1};
+ case 'rel_tol'
+ rel_tol = varargin{iarg + 1};
+ case 'field'
+ field = varargin{iarg + 1};
+ end
+ end
+ bounds = RelativeTolerance(rel_tol) | AbsoluteTolerance(abs_tol);
+ testCase.verifyThat(actual_val, ...
+ IsEqualTo(expected_val, 'Within', bounds), field);
+ end
+ function verify_spinw_matrix(testCase, actual_matrix, expected_matrix, varargin)
+ % compare excl. color (which is randomly generated)
+ testCase.verify_val(rmfield(actual_matrix, 'color'), ...
+ rmfield(expected_matrix, 'color'), varargin{:})
+ % check size and data type of color
+ testCase.assertEqual(size(actual_matrix.color), ...
+ [3, size(expected_matrix.mat, 3)]);
+ testCase.assertTrue(isa(actual_matrix.color, ...
+ class(expected_matrix.color)));
+ end
+ function verify_spinwave(testCase, actual_spinwave, ...
+ expected_spinwave, varargin)
+ % List of fields to test separately, only remove fields that
+ % exist
+ rmfields = intersect(fields(expected_spinwave), ...
+ {'datestart', 'dateend', 'obj', 'V'});
+ testCase.verify_obj(rmfield(actual_spinwave, rmfields), ...
+ rmfield(expected_spinwave, rmfields), varargin{:})
+ for field = ["datestart", "dateend"]
+ if isfield(expected_spinwave, field)
+ testCase.assertTrue(isa(actual_spinwave.(field), 'char'));
+ end
+ end
+ % obj is not always in spinwave output (e.g. if fitmode ==
+ % true)
+ if isfield(expected_spinwave, 'obj')
+ testCase.verify_obj(actual_spinwave.obj, expected_spinwave.obj);
+ end
+ % verify abs of V (matrix of eigenvecs) - sign doesn't matter
+ % get sign by comaparing max abs value
+ if isfield(expected_spinwave, 'V')
+ ifinite = find(isfinite(expected_spinwave.V));
+ [~, imax] = max(abs(expected_spinwave.V(ifinite)));
+ imax = ifinite(imax); % get index in array incl. non-finite
+ scale_sign = sign(expected_spinwave.V(imax)./actual_spinwave.V(imax));
+ if ~isfinite(scale_sign)
+ % in case actual V(imax) is not finite - verify should fail!
+ scale_sign = 1;
+ end
+ testCase.verify_val(actual_spinwave.V, ...
+ scale_sign.*expected_spinwave.V,...
+ varargin{:});
+ end
+ end
+ function disable_warnings(testCase, varargin)
+ testCase.cleanup_warnings = [testCase.cleanup_warnings, ...
+ {onCleanup(@(c) cellfun(@(c) warning('on', c), varargin))}];
+ cellfun(@(c) warning('off', c), varargin);
+ end
+ end
+end
diff --git a/+sw_tests/+unit_tests/unittest_sw_egrid.m b/+sw_tests/+unit_tests/unittest_sw_egrid.m
new file mode 100644
index 000000000..64e9cc7cc
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_sw_egrid.m
@@ -0,0 +1,406 @@
+classdef unittest_sw_egrid < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for @spinw/spinwave.m
+
+ properties
+ swobj = [];
+ swobj_tri = [];
+ spectrum = struct();
+ sw_egrid_out = struct();
+ sw_egrid_out_pol = struct();
+ sw_egrid_out_sperp = struct();
+ qh5 = [0:0.25:1; zeros(2,5)];
+ end
+
+ properties (TestParameter)
+ % Components that require sw_neutron be called first
+ pol_components = {'Mxx', 'Pxy', 'Pz'};
+ end
+
+ methods (TestClassSetup)
+ function setup_chain_model(testCase)
+ % Just create a very simple FM 1D chain model
+ testCase.swobj = spinw;
+ testCase.swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ testCase.swobj.addatom('r', [0 0 0],'S', 1, 'label', 'MNi2');
+ testCase.swobj.gencoupling('maxDistance', 7);
+ testCase.swobj.addmatrix('value', -eye(3), 'label', 'Ja');
+ testCase.swobj.addcoupling('mat', 'Ja', 'bond', 1);
+ testCase.swobj.genmagstr('mode', 'direct', 'k', [0 0 0], ...
+ 'S', [0; 1; 0]);
+ end
+ end
+
+ methods (TestMethodSetup)
+ function setup_default_spectrum_and_egrid(testCase)
+ % Spectrum input to sw_egrid
+ testCase.spectrum.obj = testCase.swobj;
+ testCase.spectrum.formfact = false;
+ testCase.spectrum.incomm = false;
+ testCase.spectrum.helical = false;
+ testCase.spectrum.norm = false;
+ testCase.spectrum.nformula = int32(0);
+ testCase.spectrum.param = struct('notwin', true, ...
+ 'sortMode', true, ...
+ 'tol', 1e-4, ...
+ 'omega_tol', 1e-5, ...
+ 'hermit', true);
+ testCase.spectrum.title = 'Numerical LSWT spectrum';
+ testCase.spectrum.gtensor = false;
+ testCase.spectrum.datestart = '01-Jan-2023 00:00:01';
+ testCase.spectrum.dateend = '01-Jan-2023 00:00:02';
+ testCase.spectrum.hkl = [0:0.25:1; zeros(2,5)];
+ testCase.spectrum.hklA = testCase.spectrum.hkl*2/3*pi;
+ testCase.spectrum.omega = [ 1e-5 2. 4. 2. -1e-5; ...
+ -1e-5 -2. -4. -2. 1e-5];
+ testCase.spectrum.Sab = zeros(3, 3, 2, 5);
+ Sab1 = [0.5 0 0.5j; 0 0 0; -0.5j 0 0.5];
+ Sab2 = [0.5 0 -0.5j; 0 0 0; 0.5j 0 0.5];
+ testCase.spectrum.Sab(:, :, 1, 1:4) = repmat(Sab1, 1, 1, 1, 4);
+ testCase.spectrum.Sab(:, :, 2, 5) = Sab1;
+ testCase.spectrum.Sab(:, :, 2, 1:4) = repmat(Sab2, 1, 1, 1, 4);
+ testCase.spectrum.Sab(:, :, 1, 5) = Sab2;
+
+ % Output from sw_egrid when 'component' is specified
+ testCase.sw_egrid_out = testCase.spectrum;
+ testCase.sw_egrid_out.param.sumtwin = true;
+ testCase.sw_egrid_out.T = 0;
+ testCase.sw_egrid_out.Evect = linspace(0, 4.4, 501);
+ testCase.sw_egrid_out.swConv = zeros(500, 5);
+ testCase.sw_egrid_out.swConv([728, 1455, 1728]) = 0.5;
+ testCase.sw_egrid_out.swInt = 0.5*ones(2, 5);
+ testCase.sw_egrid_out.component = 'Sperp';
+
+ % Default output from sw_egrid - when Sperp is used there are
+ % extra fields
+ testCase.sw_egrid_out_sperp = testCase.sw_egrid_out;
+ testCase.sw_egrid_out_sperp.intP = [];
+ testCase.sw_egrid_out_sperp.Mab = [];
+ testCase.sw_egrid_out_sperp.Pab = [];
+ testCase.sw_egrid_out_sperp.Sperp = 0.5*ones(2, 5);
+ testCase.sw_egrid_out_sperp.param.n = [0 0 1];
+ testCase.sw_egrid_out_sperp.param.pol = false;
+ testCase.sw_egrid_out_sperp.param.uv = {};
+ testCase.sw_egrid_out_sperp.param.sumtwin = true;
+
+ % Output from sw_egrid when polarisation required - when
+ % sw_neutron has to be called first
+ testCase.sw_egrid_out_pol = testCase.sw_egrid_out_sperp;
+ testCase.sw_egrid_out_pol.param.pol = true;
+ testCase.sw_egrid_out_pol.intP = 0.5*ones(3, 2, 5);
+ testCase.sw_egrid_out_pol.Pab = repmat(diag([-0.5 -0.5 0.5]), 1, 1, 2, 5);
+ Mab_mat = [0.5 0 1i*0.5; 0 0 0; -1i*0.5 0 0.5];
+ testCase.sw_egrid_out_pol.Mab = repmat(Mab_mat, 1, 1, 2, 5);
+ testCase.sw_egrid_out_pol.Mab(:, :, 1, 5) = conj(Mab_mat);
+ testCase.sw_egrid_out_pol.Mab(:, :, 2, :) = conj(testCase.sw_egrid_out_pol.Mab(:, :, 1, :));
+
+ end
+ end
+
+ methods (Test)
+ function test_noInput(testCase)
+ % Tests that if sw_egrid is called with no input, it calls the help
+ % First mock the help call
+ help_function = sw_tests.utilities.mock_function('swhelp');
+ sw_egrid();
+ testCase.assertEqual(help_function.n_calls, 1);
+ testCase.assertEqual(help_function.arguments, {{'sw_egrid'}});
+ end
+ function test_pol_component_no_sw_neutron_causes_error(testCase, pol_components)
+ testCase.verifyError(...
+ @() sw_egrid(testCase.spectrum, 'component', pol_components), ...
+ 'sw_egrid:WrongInput');
+ end
+ function test_invalid_component(testCase)
+ testCase.verifyError(...
+ @() sw_egrid(testCase.spectrum, 'component', 'Sab'), ...
+ 'sw_parstr:WrongString');
+ end
+ function test_epsilon_deprecated_warning(testCase)
+ testCase.verifyWarning(...
+ @() sw_egrid(testCase.spectrum, 'epsilon', 1e-5), ...
+ 'sw_egrid:DeprecationWarning');
+ end
+ function test_defaults(testCase)
+ out = sw_egrid(testCase.spectrum);
+ testCase.verify_obj(out, testCase.sw_egrid_out_sperp);
+ end
+ function test_Sperp(testCase)
+ out = sw_egrid(testCase.spectrum, 'component', 'Sperp');
+ testCase.verify_obj(out, testCase.sw_egrid_out_sperp);
+ end
+ function test_Sxx(testCase)
+ component = 'Sxx';
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ out = sw_egrid(testCase.spectrum, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_Sxy(testCase)
+ component = 'Sxy';
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ expected_out.swInt = zeros(2, 5);
+ expected_out.swConv = zeros(500, 5);
+ out = sw_egrid(testCase.spectrum, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_diff_Sxx_Szz(testCase)
+ component = 'Sxx-Szz';
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ expected_out.swInt = zeros(2, 5);
+ expected_out.swConv = zeros(500, 5);
+ out = sw_egrid(testCase.spectrum, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_sum_Sxx_Szz_Sxy(testCase)
+ component = 'Sxx+Szz+Sxy';
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ expected_out.swInt = 2*expected_out.swInt;
+ expected_out.swConv = 2*expected_out.swConv;
+ out = sw_egrid(testCase.spectrum, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_cell_array_component(testCase)
+ component = {'Sxx', 'Sxy'};
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ expected_out.swInt = {expected_out.swInt; zeros(2, 5)};
+ expected_out.swConv = {expected_out.swConv; zeros(500, 5)};
+ out = sw_egrid(testCase.spectrum, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_Mzz(testCase)
+ component = 'Mzz';
+ expected_out = testCase.sw_egrid_out_pol;
+ expected_out.component = component;
+
+ neutron_out = sw_neutron(testCase.spectrum, 'pol', true);
+ out = sw_egrid(neutron_out, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_sum_Pzz_Mxx(testCase)
+ component = 'Pzz+Mxx';
+ expected_out = testCase.sw_egrid_out_pol;
+ expected_out.component = component;
+ expected_out.swInt = 2*expected_out.swInt;
+ expected_out.swConv = 2*expected_out.swConv;
+
+ neutron_out = sw_neutron(testCase.spectrum, 'pol', true);
+ out = sw_egrid(neutron_out, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+
+ function test_Px(testCase)
+ component = 'Px';
+ expected_out = testCase.sw_egrid_out_pol;
+ expected_out.component = component;
+ neutron_out = sw_neutron(testCase.spectrum, 'pol', true);
+ out = sw_egrid(neutron_out, 'component', component);
+ testCase.verify_obj(out, expected_out);
+ end
+
+ function test_fName_component(testCase)
+ % add field to spectrum
+ component = 'Sperp';
+ spectrum = testCase.spectrum;
+ spectrum.(component) = ones(2,5); % double actual
+
+ % note do not add fields normally added to output when Sperp
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ expected_out.(component) = spectrum.(component);
+ expected_out.swInt = 2*expected_out.swInt;
+ expected_out.swConv = 2*expected_out.swConv;
+
+ out = sw_egrid(spectrum, 'component', component);
+
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_Evect(testCase)
+ Evect = linspace(1, 3, 201);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv = zeros(200, 5);
+ expected_out.swConv([301, 701]) = 0.5;
+ expected_out.Evect = Evect;
+ out = sw_egrid(testCase.spectrum, 'Evect', Evect);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_Evect_cbin(testCase)
+ Evect_in = linspace(1.005, 2.995, 200);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv = zeros(200, 5);
+ expected_out.swConv([301, 701]) = 0.5;
+ expected_out.Evect = linspace(1, 3, 201);
+ out = sw_egrid(testCase.spectrum, 'Evect', Evect_in, 'binType', 'cbin');
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_temp(testCase)
+ temp = 300;
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv([728, 1728]) = 6.70976913583173;
+ expected_out.swConv(1455) = 3.48826657260066;
+ expected_out.T = temp;
+ out = sw_egrid(testCase.spectrum, 'T', temp);
+ testCase.verify_obj(out, expected_out, 'rel_tol', 1e-10);
+ end
+ function test_single_ion_temp(testCase)
+ temp = 300;
+ spectrum = testCase.spectrum;
+ % Copy swobj here so don't interfere with other tests
+ spectrum.obj = copy(testCase.swobj);
+ spectrum.obj.single_ion.T = temp;
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.obj = spectrum.obj;
+ expected_out.swConv([728, 1728]) = 6.70976913583173;
+ expected_out.swConv(1455) = 3.48826657260066;
+ expected_out.T = temp;
+ out = sw_egrid(spectrum);
+ testCase.verify_obj(out, expected_out, 'rel_tol', 1e-10);
+ end
+ function test_twin(testCase)
+ swobj_twin = copy(testCase.swobj);
+ swobj_twin.addtwin('axis', [0 0 1], 'phid', [60 120], 'vol', [1 2]);
+ spectrum = swobj_twin.spinwave(testCase.spectrum.hkl);
+ spectrum.datestart = testCase.spectrum.datestart;
+ spectrum.dateend = testCase.spectrum.dateend;
+ out = sw_egrid(spectrum);
+
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.obj = spectrum.obj;
+ expected_out.omega = spectrum.omega;
+ expected_out.Sab = spectrum.Sab;
+ expected_out.param.notwin = false;
+ expected_out.swConv = zeros(500, 5);
+ expected_out.swConv([728, 1455, 1728]) = 0.125;
+ expected_out.swConv([567, 1228, 1888, 2455]) = 0.65625;
+ expected_out.swInt = 0.78125*ones(2, 5);
+ expected_out.intP = cell(1,3);
+ expected_out.Pab = cell(1,3);
+ expected_out.Mab = cell(1,3);
+ expected_out.Sperp = {0.5*ones(2, 5) 0.875*ones(2, 5) 0.875*ones(2, 5)};
+
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_twin_nosum(testCase)
+ swobj_twin = copy(testCase.swobj);
+ swobj_twin.addtwin('axis', [0 0 1], 'phid', [60 120], 'vol', [1 2]);
+ spectrum = swobj_twin.spinwave(testCase.spectrum.hkl);
+ spectrum.datestart = testCase.spectrum.datestart;
+ spectrum.dateend = testCase.spectrum.dateend;
+ out = sw_egrid(spectrum, 'sumtwin', false);
+
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.obj = spectrum.obj;
+ expected_out.omega = spectrum.omega;
+ expected_out.Sab = spectrum.Sab;
+ expected_out.param.notwin = false;
+ expected_out.param.sumtwin = false;
+ expected_out.component = {'Sperp'};
+ expected_out.swConv = cell(1, 3);
+ expected_out.swConv{1} = testCase.sw_egrid_out_sperp.swConv;
+ expected_out.swInt = cell(1, 3);
+ expected_out.swInt{1} = testCase.sw_egrid_out_sperp.swInt;
+ expected_out.intP = cell(1,3);
+ expected_out.Pab = cell(1,3);
+ expected_out.Mab = cell(1,3);
+ expected_out.Sperp = {0.5*ones(2, 5) 0.875*ones(2, 5) 0.875*ones(2, 5)};
+
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_imagChk(testCase)
+ dE = 1;
+ testCase.spectrum.omega(1) = 0 + 2i*dE; % imag > dE bins
+ testCase.verifyError(...
+ @() sw_egrid(testCase.spectrum, ...
+ 'Evect', 0:dE:4, 'imagChk', true), ...
+ 'egrid:BadSolution');
+ end
+ function test_autoEmin(testCase)
+ eps_imag = 1e-8;
+ imag_omega = 0 + 1i*eps_imag;
+ testCase.spectrum.omega(1) = imag_omega;
+ component = 'Sxx';
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ expected_out.Evect(1) = expected_out.Evect(1) + eps_imag;
+ expected_out.omega(1) = imag_omega;
+ expected_out.swConv(1) = 0;
+ out = sw_egrid(testCase.spectrum, 'component', component, 'autoEmin', true);
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_modeIdx(testCase)
+ % only consisder -ve mode (magnon anhillation/ energy gain)
+ component = 'Sxx';
+ expected_out = testCase.sw_egrid_out;
+ expected_out.component = component;
+ for modeIdx = 1:2
+ out = sw_egrid(testCase.spectrum, 'component', component, ...
+ 'modeIdx', modeIdx);
+ if modeIdx == 2
+ % only consisder -ve mode (magnon anhillation/energy gain)
+ % which is not in range of default energy bins
+ expected_out.swConv = zeros(500, 5);
+ end
+ testCase.verify_obj(out, expected_out);
+ end
+ end
+ function test_zeroEnergyTol(testCase)
+ % set zeroEnergyTol > max energy to prodcuce zero intensity
+ out = sw_egrid(testCase.spectrum, ...
+ 'binType', 'ebin' , 'zeroEnergyTol', 5);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv = zeros(size(expected_out.swConv));
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_negative_zeroEnergyTol(testCase)
+ out = sw_egrid(testCase.spectrum, 'zeroEnergyTol', -1);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv([1,2001]) = 0.5; % intensity at zero energy
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_maxDSF(testCase)
+ % set maxDSF low to zero all intensity
+ out = sw_egrid(testCase.spectrum, ...
+ 'binType', 'ebin' , 'maxDSF', 1e-2);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv = zeros(size(expected_out.swConv));
+ testCase.verify_obj(out, expected_out);
+ end
+ function test_dE_single_number(testCase)
+ % use small dE so only intensity in a single ebin at each q
+ out = sw_egrid(testCase.spectrum, 'component', 'Sperp', 'dE', 0.001);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv(228,[2, 4]) = 0.00929494936601533;
+ expected_out.swConv(455,3) = 2.36709828275952;
+ testCase.verify_obj(out, expected_out, 'abs_tol', 1e-10);
+ end
+ function test_dE_matrix_correct_numel(testCase)
+ % use small dE so only intensity in a single ebin at each q
+ dE = 0.001*ones(1, numel(testCase.sw_egrid_out_sperp.Evect)-1);
+ out = sw_egrid(testCase.spectrum, 'component', 'Sperp', 'dE', dE);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv(228,[2, 4]) = 0.00929494936601533;
+ expected_out.swConv(455,3) = 2.36709828275952;
+ testCase.verify_obj(out, expected_out, 'abs_tol', 1e-10);
+ end
+ function test_dE_matrix_incorrect_numel(testCase)
+ % use small dE so only intensity in a single ebin at each q
+ dE = [0.001, 0.001];
+ testCase.verifyError(...
+ @() sw_egrid(testCase.spectrum, 'component', 'Sperp', 'dE', dE), ...
+ 'sw_egrid:WrongInput');
+ end
+ function test_dE_callable_func(testCase)
+ % use small dE so only intensity in a single ebin at each q
+ dE_func = @(en) 0.001;
+ out = sw_egrid(testCase.spectrum, 'component', 'Sperp', 'dE', dE_func);
+ expected_out = testCase.sw_egrid_out_sperp;
+ expected_out.swConv(228,[2, 4]) = 0.00929494936601533;
+ expected_out.swConv(455,3) = 2.36709828275952;
+ testCase.verify_obj(out, expected_out, 'abs_tol', 1e-10);
+ end
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_sw_fitpowder.m b/+sw_tests/+unit_tests/unittest_sw_fitpowder.m
new file mode 100644
index 000000000..4a8766de4
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_sw_fitpowder.m
@@ -0,0 +1,685 @@
+classdef unittest_sw_fitpowder < sw_tests.unit_tests.unittest_super
+
+ properties
+ swobj = [];
+ data_1d_cuts = arrayfun(@(qmin) struct('x', 1:3, 'y', 1:3, ...
+ 'e', 1:3, 'qmin', qmin, ...
+ 'qmax',qmin+1), 3.5:4.5);
+ data_2d = struct('x', {{1:3, 4:5}}, 'y', [1:3; 1:3], ...
+ 'e', [1:3; 1:3]);
+ fit_func = @(obj, p) matparser(obj, 'param', p, 'mat', {'J_1'}, 'init', true);
+ j1 = 2;
+ default_fitpow = [];
+ default_fields = [];
+ default_modQ_cens_1d = 3.55:0.1:5.45; % integrate over nQ pts
+ end
+
+ properties (TestParameter)
+ fit_params = {{}, {'resid_handle', true}};
+ end
+
+ methods (TestClassSetup)
+ function setup_spinw_obj_and_expected_result(testCase)
+ % setup spinw object
+ testCase.swobj = sw_model('triAF', 1);
+ % subset of default fitpow object fields
+ testCase.default_fitpow = struct('y', testCase.data_2d.y', ...
+ 'e', testCase.data_2d.e', ...
+ 'ebin_cens', testCase.data_2d.x{1}, ...
+ 'modQ_cens', testCase.data_2d.x{2}, ...
+ 'params', [2;0;0;0;1], ...
+ 'bounds', [-Inf Inf;
+ -Inf Inf;
+ -Inf Inf;
+ -Inf Inf;
+ 0 Inf], ...
+ 'ibg', [], ...
+ 'npoly_modQ', 1, ...
+ 'npoly_en', 1);
+ testCase.default_fields = fieldnames(testCase.default_fitpow);
+ end
+ end
+
+ methods
+ function verify_results(testCase, observed, expected, fieldnames, varargin)
+ if nargin < 4
+ fieldnames = testCase.default_fields;
+ end
+ for ifld = 1:numel(fieldnames)
+ fld = fieldnames{ifld};
+ testCase.verify_val(observed.(fld), expected.(fld), varargin{:});
+ end
+ end
+ end
+
+ methods (Test)
+ function test_init_data_2d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ testCase.verify_results(out, testCase.default_fitpow);
+ end
+
+ function test_init_data_1d_planar_bg(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;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_init_data_1d_indep_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent");
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
+ % add extra background param
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end],:);
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ 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 for 1D cuts equivalent to a planar bg
+ % with slope_en=1, slope_q=2, intercept = 3
+ out.set_bg_parameters(1, 1); % en_slope = 1
+ out.set_bg_parameters(2, 11, 1); % intercept = 4*2 + 3 for cut 1
+ out.set_bg_parameters(2, 13, 2); % intercept = 5*2 + 3 for cut 2
+ out.set_background_strategy("planar");
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(2:end-1) = [2,1,3];
+ 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_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=1, slope_q=2, intercept = 3
+ out.set_bg_parameters(1:3, [2,1,3]); % 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) = 1; % en slope
+ expected_fitpow.params(3) = 11; % intercept = 4*2 + 3 for cut 1
+ expected_fitpow.params(5) = 13; % intercept = 5*2 + 3 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)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ qcens = [4, 5];
+ out.replace_2D_data_with_1D_cuts(qcens-0.5, qcens+0.5)
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.modQ_cens = qcens;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_replace_2D_data_with_1D_cuts_specify_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ qcens = [4, 5];
+ out.replace_2D_data_with_1D_cuts(qcens-0.5, qcens+0.5,...
+ "independent")
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.modQ_cens = qcens; % not nQ values as using bins in 2D data
+ % add extra background param
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end],:);
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_init_data_1d_specify_nQ(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "planar", 1);
+ testCase.verify_results(out, testCase.default_fitpow);
+ end
+
+ function test_set_model_parameters(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, 5);
+ out.set_model_parameters(1, testCase.j1);
+ testCase.verify_results(out, testCase.default_fitpow);
+ end
+
+ function test_set_model_parameter_bounds(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ lb = -5;
+ ub = 5;
+ out.set_model_parameter_bounds(1, -5, 5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.bounds(1, :) = [lb, ub];
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_fix_model_parameters(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.fix_model_parameters(1);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.bounds(1, :) = [testCase.j1, testCase.j1];
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameters_planar_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ bg_pars = [-5, 5];
+ out.set_bg_parameters(1:2, bg_pars);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(2:3) = bg_pars;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameters_with_char(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.set_bg_parameters('E0', -5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(end-1) = -5;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameters_with_cell_of_char(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ bg_pars = [-5, 5];
+ out.set_bg_parameters({'Q1', 'E1'}, bg_pars);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(2:3) = bg_pars;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameters_indep_bg_all_cuts(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ bg_pars = [-5, 5];
+ out.set_bg_parameters(1:2, bg_pars);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = [expected_fitpow.params(1);
+ bg_pars(:); bg_pars(:); 1];
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameters_indep_bg_specify_icut(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ bg_pars = [-5, 5];
+ out.set_bg_parameters(1:2, bg_pars, 2);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = [expected_fitpow.params(1:3);
+ bg_pars(:); 1];
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_fix_bg_parameters_indep_bg_specify_icut(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ out.fix_bg_parameters(1:2, 2);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end],:);
+ expected_fitpow.bounds = [expected_fitpow.bounds(1:3,:);
+ zeros(2,2);
+ expected_fitpow.bounds(end,:)];
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_fix_bg_parameters_indep_bg_all_cuts(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ out.fix_bg_parameters(1:2);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end],:);
+ expected_fitpow.bounds = [expected_fitpow.bounds(1,:);
+ zeros(4,2);
+ expected_fitpow.bounds(end,:)];
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameter_bounds_indep_bg_all_cuts(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ ub = 5;
+ out.set_bg_parameter_bounds(1:2, [], [ub, ub]); % lb unchanged
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end]);
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ expected_fitpow.bounds(2:5, 2) = ub;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_parameter_bounds_indep_bg_specify_icut(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ ub = 5;
+ out.set_bg_parameter_bounds(1:2, [], [ub, ub], 1); % lb unchanged
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end]);
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ expected_fitpow.bounds(2:3, 2) = ub;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_scale(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ scale = 5;
+ out.set_scale(scale);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(end) = scale;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_fix_scale(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.fix_scale();
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.bounds(end, :) = 1;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_scale_bounds(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ lb = 0.5;
+ out.set_scale_bounds(0.5, []);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.bounds(end, 1) = lb;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_crop_energy_range_1d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1);
+ out.crop_energy_range(2,5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
+ expected_fitpow.y = expected_fitpow.y(2:end,:);
+ expected_fitpow.e = expected_fitpow.e(2:end,:);
+ expected_fitpow.ebin_cens = expected_fitpow.ebin_cens(2:end);
+ testCase.verify_results(out, expected_fitpow);
+ testCase.verify_val(out.powspec_args.Evect, ...
+ expected_fitpow.ebin_cens)
+ end
+ function test_crop_energy_range_2d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.crop_energy_range(2,5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.y(1,:) = NaN;
+ expected_fitpow.e(1,:) = NaN;
+ testCase.verify_results(out, expected_fitpow);
+ testCase.verify_val(out.powspec_args.Evect, ...
+ expected_fitpow.ebin_cens)
+ end
+
+ function test_exclude_energy_range_1d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1);
+ out.exclude_energy_range(1.5,2.5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
+ expected_fitpow.y = expected_fitpow.y([1,end],:);
+ expected_fitpow.e = expected_fitpow.e([1,end],:);
+ expected_fitpow.ebin_cens = expected_fitpow.ebin_cens([1,end]);
+ testCase.verify_results(out, expected_fitpow);
+ testCase.verify_val(out.powspec_args.Evect, ...
+ expected_fitpow.ebin_cens)
+ end
+
+ function test_crop_q_range(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.crop_q_range(4.5,5.5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.y = expected_fitpow.y(:, 2:end);
+ expected_fitpow.e = expected_fitpow.e(:, 2:end);
+ expected_fitpow.modQ_cens = expected_fitpow.modQ_cens(2:end);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_estimate_constant_background_fails(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ % background estimation fails as no minimum in skew
+ testCase.verifyError(...
+ @() out.estimate_constant_background(), ...
+ 'spinw:find_indices_and_mean_of_bg_bins');
+ testCase.verify_results(out, testCase.default_fitpow);
+ end
+
+ function test_estimate_constant_background(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.y(1) = 10; % higher so other bins are background
+ out.estimate_constant_background();
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.y(1) = 10;
+ expected_fitpow.ibg = [3;6;2;5;4];
+ expected_fitpow.params(end-1) = 2.2;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_fit_background(testCase, fit_params)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.y(1) = 10; % higher so other bins are background
+ out.fix_bg_parameters(1:2); % fix slopes of background to 0
+ out.set_bg_parameters(3, 1.5); % initial guess
+ out.fit_background(fit_params{:});
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.y(1) = 10;
+ expected_fitpow.ibg = [3;6;2;5;4];
+ expected_fitpow.params(end-1) = 2.2002;
+ expected_fitpow.bounds(2:3,:) = 0; % fixed bg slopes
+ testCase.verify_results(out, expected_fitpow, ...
+ testCase.default_fields, ...
+ '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);
+ out.y(1) = 10; % higher so other bins are background
+ bg_pars = out.params(2:end-1);
+ bg_pars(2:2:end) = 1;
+ out.estimate_constant_background(); % so ibg is set
+ cost = out.calc_cost_func_of_background(bg_pars);
+ testCase.verify_val(cost, 10);
+ end
+
+ function test_calc_cost_func_of_background_planar(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "planar", 2);
+ out.y(1) = 10; % higher so other bins are background
+ bg_pars = out.params(2:end-1);
+ bg_pars(end) = 1;
+ out.estimate_constant_background(); % so ibg is set
+ cost = out.calc_cost_func_of_background(bg_pars);
+ testCase.verify_val(cost, 10);
+ end
+
+ function test_set_errors_of_bg_bins(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.y(1) = 10; % high so other bins are background
+ out.set_errors_of_bg_bins(100);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.y(1) = 10;
+ expected_fitpow.ibg = [3;6;2;5;4];
+ expected_fitpow.e(expected_fitpow.ibg) = 100;
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_reset_errors_of_bg_bins(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.y(1) = 10; % high so other bins are background
+ out.set_errors_of_bg_bins(100);
+ out.reset_errors_of_bg_bins();
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.y(1) = 10;
+ expected_fitpow.ibg = [3;6;2;5;4];
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_calc_uncertainty(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ % perform a fit of const background only
+ out.set_scale(0);
+ out.powspec_args.hermit = true;
+ out.fix_model_parameters(1);
+ out.fix_bg_parameters(1:2);
+ out.fix_scale()
+ out.set_bg_parameters(3, mean(out.y, 'all'));
+ [param_errors, cov] = out.calc_uncertainty(out.params);
+ % test errors determined for only 1 parameter (const bg)
+ testCase.verify_val(param_errors, [0; 0; 0; 0.3651; 0], 'abs_tol', 1e-4);
+ end
+
+ 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.36;
+ testCase.verify_results(out, expected_fitpow, ...
+ testCase.default_fields, 'abs_tol', 1e-1);
+ end
+
+ function test_calc_cost_func_1d_planar_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "planar", 1);
+ out.powspec_args.dE = 0.1; % constant energy resolution
+ out.powspec_args.hermit = true;
+ cost = out.calc_cost_func(out.params);
+ testCase.verify_val(cost, 25.3, 'abs_tol', 0.1);
+ end
+
+ function test_calc_cost_func_1d_indep_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ out.powspec_args.dE = 0.1; % constant energy resolution
+ out.powspec_args.hermit = true;
+ cost = out.calc_cost_func(out.params);
+ testCase.verify_val(cost, 25.3, 'abs_tol', 0.1);
+ end
+
+ function test_calc_cost_func_2d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.powspec_args.dE = 0.1; % constant energy resolution
+ out.powspec_args.hermit = true;
+ cost = out.calc_cost_func(out.params);
+ testCase.verify_val(cost, 25.3, 'abs_tol', 0.1);
+ end
+
+ function test_add_1Dcuts_after_2D_data(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.add_data(testCase.data_1d_cuts);
+ expected_fitpow = testCase.default_fitpow;
+ 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
+
+ function test_set_npoly_modQ(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.set_bg_npoly_modQ(2);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.npoly_modQ = 2;
+ expected_fitpow.params = [expected_fitpow.params(1:end-1);
+ 0;
+ expected_fitpow.params(end)];
+ expected_fitpow.bounds = [expected_fitpow.bounds(1:end-1,:);
+ -Inf, Inf;
+ expected_fitpow.bounds(end,:)];
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_npoly_en(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.set_bg_npoly_en(2);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.npoly_en = 2;
+ % add extra row in params and bounds
+ expected_fitpow.params = expected_fitpow.params([1:2,2:end]);
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_npoly_modQ_1d_data_indep_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent");
+ testCase.verifyError(...
+ @() out.set_bg_npoly_modQ(2), ...
+ 'sw_fitpowder:invalidinput');
+ end
+
+ function test_set_npoly_en_1d_data_indep_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent");
+ out.set_bg_npoly_en(2)
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.npoly_en = 2;
+ expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
+ % add 3 extra rows:
+ % 3 params x 2 cuts vs 3 planar parmas order=1
+ expected_fitpow.params = expected_fitpow.params([1:4,2:end]);
+ expected_fitpow.bounds = expected_fitpow.bounds([1:4,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_npoly_modQ_1d_data_planar_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "planar");
+ % try adding order larger than numebr cuts
+ testCase.verifyError(...
+ @() out.set_bg_npoly_modQ(2), ...
+ 'sw_fitpowder:invalidinput');
+ % set it to constant in modQ
+ out.set_bg_npoly_modQ(0)
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.npoly_modQ = 0;
+ expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d;
+ % remove a row
+ expected_fitpow.params = expected_fitpow.params([1,3:end]);
+ expected_fitpow.bounds = expected_fitpow.bounds([1,3:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_export_data_2d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ data = out.export_data();
+ testCase.verify_val(data, testCase.data_2d);
+ end
+
+ function test_export_data_2d_with_filename(testCase)
+ import matlab.unittest.fixtures.TemporaryFolderFixture
+ fixture = testCase.applyFixture(TemporaryFolderFixture);
+ tmp_file = fullfile(fixture.Folder,"data.mat");
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ data = out.export_data(tmp_file);
+ testCase.verify_val(data, testCase.data_2d);
+ testCase.assertTrue(isfile(tmp_file));
+ end
+
+ function test_export_data_1d(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1);
+ data = out.export_data();
+ testCase.verify_val(data, testCase.data_1d_cuts);
+ end
+
+ function test_set_bg_param_with_name_planar_bg(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ out.set_bg_parameters("Q1", -5);
+ out.set_bg_parameters("E1", 5);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(2:4) = [-5, 5,0]; % Q1, E1, E0
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_set_bg_param_with_invalid_name(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ testCase.verifyError(...
+ @() out.set_bg_parameters("Q3", -5), ...
+ 'sw_fitpowder:invalidinput');
+ end
+
+ function test_set_bg_param_with_name_indep_bg_all_cuts(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ...
+ testCase.fit_func, testCase.j1, "independent", 1);
+ bg_pars = [-5, 5];
+ out.set_bg_parameters(["E1", "E0"], bg_pars);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params = [expected_fitpow.params(1);
+ bg_pars(:); bg_pars(:); 1];
+ expected_fitpow.bounds = expected_fitpow.bounds([1:2,2:end],:);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_fix_bg_param_with_array_of_names(testCase)
+ out = sw_fitpowder(testCase.swobj, testCase.data_2d, ...
+ testCase.fit_func, testCase.j1);
+ bg_pars = 1:3;
+ bg_labels = ["Q1","E1","E0"];
+ out.set_bg_parameters(bg_labels, bg_pars);
+ out.fix_bg_parameters(bg_labels);
+ expected_fitpow = testCase.default_fitpow;
+ expected_fitpow.params(2:4) = bg_pars;
+ expected_fitpow.bounds(2:4, :) = repmat(bg_pars(:), 1,2);
+ testCase.verify_results(out, expected_fitpow);
+ end
+
+ function test_add_1Dcuts_withs_qs_specified(testCase)
+ cuts = arrayfun(@(qs) struct('x', 1:3, 'y', 1:3, 'e', 1:3, 'qs', qs), ...
+ 4:5);
+ out = sw_fitpowder(testCase.swobj, cuts, ...
+ testCase.fit_func, testCase.j1);
+ testCase.verify_results(out, testCase.default_fitpow);
+ end
+
+ end
+
+end
diff --git a/+sw_tests/+unit_tests/unittest_sw_neutron.m b/+sw_tests/+unit_tests/unittest_sw_neutron.m
new file mode 100644
index 000000000..de4ae7180
--- /dev/null
+++ b/+sw_tests/+unit_tests/unittest_sw_neutron.m
@@ -0,0 +1,36 @@
+classdef unittest_sw_neutron < sw_tests.unit_tests.unittest_super
+ % Runs through unit test for sw_neutron.m
+
+ properties
+ swobj = [];
+ end
+
+ methods (TestClassSetup)
+ function setup_spinw_model(testCase)
+ % Just create a very simple FM 1D chain model
+ testCase.swobj = spinw;
+ testCase.swobj.genlattice('lat_const', [3 8 8], 'angled', [90 90 90]);
+ testCase.swobj.addatom('r', [0 0 0],'S', 1, 'label', 'MNi2');
+ testCase.swobj.gencoupling('maxDistance', 7);
+ testCase.swobj.addmatrix('value', -eye(3), 'label', 'Ja');
+ testCase.swobj.addcoupling('mat', 'Ja', 'bond', 1);
+ testCase.swobj.genmagstr('mode', 'direct', 'k', [0 0 0], 'S', [0; 1; 0]);
+ end
+ end
+ methods (Test)
+ function test_formfact(testCase)
+ % Tests that the form factor calculation is applied correctly
+ hkl = {[0 0 0] [10 0 0] 100};
+ testCase.disable_warnings('spinw:spinwave:NonPosDefHamiltonian');
+ % Runs calculation with/without formfactor
+ spec_no_ff = sw_neutron(testCase.swobj.spinwave(hkl, 'formfact', false));
+ spec_ff = sw_neutron(testCase.swobj.spinwave(hkl, 'formfact', true));
+ % The form factor is calculated using sw_mff, and the scaling is F(Q)^2 not F(Q).
+ implied_ff = spec_ff.Sperp ./ spec_no_ff.Sperp;
+ ff = sw_mff(testCase.swobj.unit_cell.label{1}, spec_ff.hklA);
+ testCase.verify_val(ff.^2, implied_ff(1,:), ...
+ 'rel_tol', 0.01, 'abs_tol', 1e-6);
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/+sw_tests/+utilities/average_profile_timings.m b/+sw_tests/+utilities/average_profile_timings.m
new file mode 100644
index 000000000..acf2a82b7
--- /dev/null
+++ b/+sw_tests/+utilities/average_profile_timings.m
@@ -0,0 +1,83 @@
+function average_profile_timings(save_dir)
+ % function to average tic/toc timings of performance tests
+ %
+ % file structure produced by profile_spinwave looks like:
+ % save_dir
+ % |- TestName_param_1_value1_param2_value2
+ % |- 01
+ % |- 02
+ % |- tictoc_times_profile_0.txt
+ % |- tictoc_times_profile_1.txt
+ arguments
+ save_dir string
+ end
+ % get list of sub-directories (TestName_param_1_value1_param2_value2)
+ test_dirs = dir(save_dir);
+ test_dirs = test_dirs([test_dirs.isdir]);
+ % get list unique test names (want one file per test per profile
+ % setting
+ test_names = {};
+ for idir = 1:numel(test_dirs)
+ if contains(test_dirs(idir).name, '_')
+ parts = split(test_dirs(idir).name,'_');
+ name = parts{1};
+ if ~any(strcmp(test_names, name))
+ test_names = [test_names, name];
+ end
+ end
+ end
+
+ for itest = 1:numel(test_names)
+ % get directory names that contain test name (e.g. FMChain)
+ tests = dir(fullfile(save_dir, ...
+ sprintf('*%s*', test_names{itest})));
+ tests = tests([tests.isdir]);
+ max_test_name = max(cellfun(@numel, {tests.name}));
+ fmt_str = ['%-', num2str(max_test_name + 2, '%.0f'), 's'];
+ for do_profile = 0:1
+ lines = {};
+ for idir = 1:numel(tests)
+ % search subdirectories for tictoc files
+ files = dir(fullfile(tests(idir).folder, ...
+ tests(idir).name, ...
+ "*", sprintf('tictoc*%.0f.txt', ...
+ do_profile)));
+ if ~isempty(files)
+ times = [];
+ for ifile = 1:numel(files)
+ contents = importdata(fullfile(files(ifile).folder, ...
+ files(ifile).name));
+ times = [times contents.data];
+ end
+ col_names = contents.textdata(2:end,1);
+ % col = avg (std) with 1 col for each func measured
+ time_str = sprintf('%.4e(%.4e)\t', ...
+ [mean(times,2) std(times,0,2)]');
+ % add test dir name to beginning of each line
+ line = sprintf([fmt_str, '\t%s'], tests(idir).name, ...
+ time_str);
+ lines = [lines line];
+ end
+ end
+ % add header line at beginning (now we know how many funcs
+ % measured
+ lines = [['# Timings given as avg(stdev) in seconds ' ...
+ 'for each function (see col. headers)'], ...
+ sprintf([fmt_str, ...
+ repmat('\t%-22s', 1, numel(col_names))], ...
+ 'Test Dir.', col_names{:}), ...
+ lines];
+ % write to file
+ save_file = fullfile(save_dir, ...
+ sprintf('%s_profile_%.0f.txt', ...
+ test_names{itest}, do_profile));
+ % writelines(lines, save_file);
+ fid = fopen(save_file, 'w');
+ for line = lines
+ fprintf(fid, [line{1}, '\n']);
+ end
+ fclose(fid);
+ end
+ end
+end
+
diff --git a/+sw_tests/+utilities/is_daaas.m b/+sw_tests/+utilities/is_daaas.m
new file mode 100644
index 000000000..34c943f85
--- /dev/null
+++ b/+sw_tests/+utilities/is_daaas.m
@@ -0,0 +1,10 @@
+function out = is_daaas()
+ if ispc || ismac
+ out = false;
+ else
+ [~, hostname] = system('hostname');
+ % DaaaS systems have hostname of the form 'host-NNN-NNN-NNN-NNN'
+ % where the number is the (internal) IP address
+ out = ~isempty(regexp(hostname, 'host-[0-9\-]*', 'match'));
+ end
+end
\ No newline at end of file
diff --git a/+sw_tests/+utilities/mock_function.m b/+sw_tests/+utilities/mock_function.m
new file mode 100644
index 000000000..f505ba598
--- /dev/null
+++ b/+sw_tests/+utilities/mock_function.m
@@ -0,0 +1,74 @@
+classdef mock_function < handle
+ properties
+ arguments = {}; % Arguments called with
+ n_calls = 0; % Number of times called
+ func = ''; % Name of function
+ filename = ''; % Name of function file
+ end
+ methods
+ function mockobj = mock_function(function_name, return_value)
+ if nargin < 2
+ rv_str = '';
+ return_value = '{[]}';
+ else
+ global mock_ret_val;
+ if isempty(mock_ret_val)
+ mock_ret_val = struct();
+ end
+ if iscell(return_value)
+ mock_ret_val.(function_name) = return_value;
+ else
+ mock_ret_val.(function_name) = {return_value};
+ end
+ rv_str = 'global mock_ret_val;';
+ return_value = ['mock_ret_val.' function_name];
+ end
+ fnstr = [...
+ 'function varargout = %s(varargin)\n' ...
+ ' persistent n_calls;\n' ...
+ ' persistent arguments;\n' ...
+ ' %s\n' ...
+ ' if nargin > 0 && ischar(varargin{1}) && strcmp(varargin{1}, ''check_calls'')\n' ...
+ ' varargout = {n_calls arguments};\n' ...
+ ' return;\n' ...
+ ' end\n' ...
+ ' if isempty(n_calls)\n' ...
+ ' n_calls = 1;\n' ...
+ ' arguments = {varargin};\n' ...
+ ' else\n' ...
+ ' n_calls = n_calls + 1;\n' ...
+ ' arguments = [arguments {varargin}];\n' ...
+ ' end\n' ...
+ ' if nargout > 0\n' ...
+ ' varargout = %s;\n' ...
+ ' end\n' ...
+ 'end\n'];
+ mockobj.func = function_name;
+ mockobj.filename = sprintf('%s.m', function_name);
+ fid = fopen(mockobj.filename, 'w');
+ fprintf(fid, fnstr, function_name, rv_str, return_value);
+ fclose(fid);
+ whichfun = which(function_name);
+ while ~strcmp(whichfun, fullfile(pwd, mockobj.filename))
+ pause(0.1);
+ whichfun = which(function_name);
+ end
+ end
+ function delete(mockobj)
+ delete(mockobj.filename);
+ global mock_ret_val;
+ if isfield(mock_ret_val, mockobj.func)
+ mock_ret_val = rmfield(mock_ret_val, mockobj.func);
+ end
+ end
+ function n_call = get.n_calls(mockobj)
+ [n_call, ~] = feval(mockobj.func, 'check_calls');
+ if isempty(n_call)
+ n_call = 0;
+ end
+ end
+ function arguments = get.arguments(mockobj)
+ [~, arguments] = feval(mockobj.func, 'check_calls');
+ end
+ end
+end
diff --git a/+sw_tests/+utilities/profile_spinwave.m b/+sw_tests/+utilities/profile_spinwave.m
new file mode 100644
index 000000000..83688fb15
--- /dev/null
+++ b/+sw_tests/+utilities/profile_spinwave.m
@@ -0,0 +1,67 @@
+function profile_spinwave(test_name, sw_obj, spinwave_args, egrid_args, ...
+ inst_args, do_profiles)
+
+ if nargin < 6
+ do_profiles = 1;
+ end
+
+ % generate file directory name for results
+ try
+ % get current commit is possible
+ commit = evalc("!git rev-parse --short HEAD");
+ commit = commit(1:end-1); % remove newline
+ catch
+ ver = sw_version();
+ commit = [ver.Name ver.Release];
+ end
+ host_info = [computer(), '_', version('-release')];
+ default_save_dir = fullfile(pwd, "profile_results", commit, ...
+ host_info, test_name);
+ irun = 0;
+ is_dir = true;
+ while is_dir
+ save_dir = fullfile(default_save_dir, num2str(irun, '%02.0f'));
+ is_dir = isfolder(save_dir);
+ irun = irun + 1;
+ end
+ mkdir(save_dir);
+
+ for do_profile = do_profiles
+ % open file for tic/toc timings
+ fid = fopen(fullfile(save_dir, ...
+ sprintf('tictoc_times_profile_%i.txt', do_profile)), 'w');
+ c = onCleanup(@()fclose(fid)); % in case of exception file will close
+ fprintf(fid, "Function\tDuration (s)\n");
+
+ if do_profile
+ % start profiling
+ profile('clear');
+ profile('on', '-memory');
+ else
+ profile('off'); % can be left on if user aborts prematurely
+ end
+ % use supercell k=0 structure
+ start_time = tic;
+ spec = sw_obj.spinwave(spinwave_args{:});
+ fprintf(fid, 'spinwave\t%.4e\n', toc(start_time));
+ if ~isempty(egrid_args)
+ tic;
+ spec = sw_egrid(spec, egrid_args{:});
+ fprintf(fid, 'sw_egrid\t%.4e\n', toc);
+ if ~isempty(inst_args)
+ tic;
+ sw_instrument(spec, inst_args{:});
+ fprintf(fid, 'sw_instrument\t%.4e\n', toc);
+ end
+ end
+
+ if do_profile
+ % save profile results
+ p = profile('info');
+ profsave(p, save_dir); % will mkdir if not exist
+ % save ascii summary
+ sw_tests.utilities.save_profile_results_to_txt(p, save_dir);
+ profile('off');
+ end
+ end
+end
\ No newline at end of file
diff --git a/+sw_tests/+utilities/save_profile_results_to_txt.m b/+sw_tests/+utilities/save_profile_results_to_txt.m
new file mode 100644
index 000000000..224689dc3
--- /dev/null
+++ b/+sw_tests/+utilities/save_profile_results_to_txt.m
@@ -0,0 +1,40 @@
+function save_profile_results_to_txt(results, save_dir, fname_suffix)
+ arguments
+ results struct
+ save_dir string
+ fname_suffix string = string(); % default is empty
+ end
+
+ extract = {'FunctionName' 'NumCalls' 'TotalTime' 'TotalMemAllocated' 'TotalMemFreed' 'PeakMem'};
+
+ ft = results.FunctionTable;
+ if ~isempty(ft)
+ maxTime = max([ft.TotalTime]);
+
+ fn = fieldnames(ft);
+ sd = setdiff(fn, extract);
+ m = rmfield(ft, sd);
+
+ percent = arrayfun(@(x) 100*x.TotalTime/maxTime, m, 'UniformOutput', false);
+ [m.PercentageTime] = percent{:};
+ sp_time = arrayfun(@(x) sum([x.Children.TotalTime]), ft);
+ self_time = arrayfun(@(x,y) x-y, [ft.TotalTime]', sp_time, 'UniformOutput', false);
+ [m.SelfTime] = self_time{:};
+ percent = arrayfun(@(x) 100*x.SelfTime/maxTime, m, 'UniformOutput', false);
+ [m.SelfPercentageTime] = percent{:};
+
+ dataStr = evalc('struct2table(m)');
+
+ % Remove HTML, braces and header
+ dataStr = regexprep(dataStr, '<.*?>', '');
+ dataStr = regexprep(dataStr, '[{}]', ' ');
+ dataStr = dataStr(24:end);
+
+ % make filename using function and mfilename of test
+ filepath = fullfile(save_dir, 'summary.txt');
+ % write profile results to file
+ fh = fopen(filepath, 'w');
+ fwrite(fh, dataStr);
+ fclose(fh);
+ end
+end
\ No newline at end of file
diff --git a/.github/workflows/build_pyspinw.yml b/.github/workflows/build_pyspinw.yml
new file mode 100644
index 000000000..20de0d474
--- /dev/null
+++ b/.github/workflows/build_pyspinw.yml
@@ -0,0 +1,179 @@
+name: pySpinW
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master, development]
+ types: [opened, reopened, synchronize]
+ workflow_dispatch:
+
+jobs:
+ compile_mex:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ matlab_version: [latest]
+ include:
+ - os: macos-latest
+ INSTALL_DEPS: brew install llvm libomp
+ fail-fast: true
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Check out SpinW
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Install Dependencies
+ run: ${{ matrix.INSTALL_DEPS }}
+ - name: Set up MATLAB
+ uses: matlab-actions/setup-matlab@v2
+ with:
+ release: ${{ matrix.matlab_version }}
+
+ - name: Remove old mex # This is due to find not working :-/ # find ${{ github.workspace }} -name "*.mex*" -type f -delete
+ run: |
+ rm external/chol_omp/chol_omp.mexa64
+ rm external/chol_omp/chol_omp.mexmaci64
+ rm external/chol_omp/chol_omp.mexw64
+ rm external/eig_omp/eig_omp.mexa64
+ rm external/eig_omp/eig_omp.mexmaci64
+ rm external/eig_omp/eig_omp.mexw64
+ rm external/mtimesx/sw_mtimesx.mexa64
+ rm external/mtimesx/sw_mtimesx.mexmaci64
+ rm external/mtimesx/sw_mtimesx.mexw64
+ - name: Run MEXing
+ uses: matlab-actions/run-command@v2
+ with:
+ command: "addpath(genpath('swfiles')); addpath(genpath('external')); sw_mex('compile', true, 'test', false, 'swtest', false);"
+ - name: Upload MEX results
+ uses: actions/upload-artifact@v4
+ with:
+ name: mex-${{ matrix.os }}
+ path: ${{ github.workspace }}/external/**/*.mex*
+
+ build_mltbx:
+ runs-on: ubuntu-latest
+ needs: compile_mex
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout SpinW
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Download MEX artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: mex-*
+ path: ${{ github.workspace }}/external
+ - name: Set up MATLAB
+ uses: matlab-actions/setup-matlab@v2
+ with:
+ release: latest
+ - name: Build mltbx
+ uses: matlab-actions/run-command@v2
+ with:
+ command: "cd mltbx; create_mltbx"
+ - name: Upload mltbx
+ uses: actions/upload-artifact@v4
+ with:
+ name: spinw.mltbx
+ path: ${{ github.workspace }}/mltbx/spinw.mltbx
+ - name: Setup tmate
+ if: ${{ failure() }}
+ uses: mxschmitt/action-tmate@v3
+
+ build_ctfs:
+ needs: compile_mex
+ runs-on: self-hosted
+ steps:
+ - name: Check out SpinW
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Download MEX artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: mex-*
+ path: ${{ github.workspace }}/external
+ - name: Build ctf
+ run: |
+ cd python
+ mkdir ctf
+ python mcc_all.py
+ - name: Upload CTF results
+ uses: actions/upload-artifact@v4
+ with:
+ name: ctf-all
+ path: ${{ github.workspace }}/python/ctf/*.ctf
+
+ build_wheel:
+ runs-on: ubuntu-latest
+ needs: build_ctfs
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout SpinW
+ uses: actions/checkout@v4
+ - name: Download CTF artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: ctf-*
+ path: python/ctf
+ - name: Download mltbx artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: spinw.mltbx
+ path: mltbx
+ - name: Set up Python environment
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.8
+ - name: Move files
+ run: |
+ cd python
+ echo "PYSPINW_VERSION=$( cat pyproject.toml | grep "version = \"" | awk -F'"' '$0=$2' | sed 's/ //g' )" >> $GITHUB_ENV
+ mkdir pyspinw/ctfs
+ find ctf/ -name "*.ctf" -exec mv '{}' pyspinw/ctfs \;
+ - name: Set up MATLAB
+ uses: matlab-actions/setup-matlab@v2
+ with:
+ release: R2023a
+ products: MATLAB_Compiler_SDK
+ # Cannot run matlab directly from the setup (gives license error) need to download a runner with the run-command actions
+ - name: Download Matlab command runner
+ uses: matlab-actions/run-command@v2
+ with:
+ command: "ver"
+ - name: Generate wrappers
+ run: |
+ python -m pip install libpymcr
+ wget https://gist.github.com/mducle/9186d062b42f05507d831af3d6677a5d/raw/cd0b0d3ed059f4e13d0364e98312dcddc2690ced/run_gh_matlab.sh
+ chmod 777 run_gh_matlab.sh
+ matlab2python -a swfiles -a external --preamble "import pyspinw; m = pyspinw.Matlab()" --matlabexec `pwd`/run_gh_matlab.sh
+ mv matlab_wrapped python/pyspinw
+ - name: Build Wheel
+ run: |
+ cd ${{ github.workspace }}/python
+ python -m pip wheel --no-deps --wheel-dir wheelhouse .
+ - name: Run python test
+ run: |
+ pip install scipy
+ cd ${{ github.workspace }}/python
+ pip install wheelhouse/*whl
+ cd tests
+ python -m unittest
+ - name: Create wheel artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: pySpinW Wheel
+ path: ${{ github.workspace }}/python/wheelhouse/*.whl
+ - name: Upload release wheels
+ if: ${{ github.event_name == 'release' }}
+ run: |
+ python -m pip install requests
+ python release.py --notest --github --token=${{ secrets.GH_TOKEN }}
+ - name: Setup tmate
+ if: ${{ failure() }}
+ uses: mxschmitt/action-tmate@v3
diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml
new file mode 100644
index 000000000..845324669
--- /dev/null
+++ b/.github/workflows/publish_pypi.yml
@@ -0,0 +1,25 @@
+name: Publish to PyPI
+
+on: workflow_dispatch
+
+jobs:
+ pypi-publish:
+ name: upload release to PyPI
+ runs-on: ubuntu-latest
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v4
+ - name: Download wheels
+ run: |
+ python -m pip install twine requests
+ python release.py --pypi --token=${{ secrets.GH_TOKEN }}
+ - name: Publish package distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: twine_wheelhouse
+ verbose: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..f12364888
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,24 @@
+name: Release SpinW
+
+on:
+ pull_request:
+ branches: [master]
+ types: [closed]
+
+jobs:
+ create_release:
+ name: Creates a SpinW release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v4
+ - name: Create Release
+ if: |
+ contains(github.event.pull_request.title, 'RELEASE') &&
+ github.event.pull_request.merged
+ shell: bash -l {0}
+ run: |
+ python -m pip install requests
+ python release.py --notest --github --create_tag --token=${{ secrets.GH_TOKEN }}
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
new file mode 100644
index 000000000..489117fa1
--- /dev/null
+++ b/.github/workflows/run_tests.yml
@@ -0,0 +1,69 @@
+name: SpinW Tests
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master, development]
+ types: [opened, reopened, synchronize]
+ workflow_dispatch:
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ matlab_version: [latest]
+ include:
+ - os: ubuntu-latest
+ matlab_version: R2021a
+ - os: macos-latest
+ INSTALL_DEPS: brew install llvm libomp
+ fail-fast: false
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Check out SpinW
+ uses: actions/checkout@v4
+ - name: Install Dependencies
+ run: ${{ matrix.INSTALL_DEPS }}
+ - name: Set up MATLAB
+ uses: matlab-actions/setup-matlab@v2
+ with:
+ release: ${{ matrix.matlab_version }}
+ - name: Set GCC version on Linux
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ run: |
+ sudo apt install -y gcc-12 g++-12
+ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 10
+ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 10
+ - name: Run tests
+ uses: matlab-actions/run-command@v2
+ with:
+ command: "run run_tests.m"
+ - uses: codecov/codecov-action@v4
+ if: ${{ always() && matrix.os == 'ubuntu-latest' && matrix.matlab_version == 'latest' }}
+ with:
+ files: coverage*.xml
+ token: ${{ secrets.CODECOV_TOKEN }}
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: Unit test results ${{ matrix.os }}-${{ matrix.matlab_version }}
+ path: junit_report*.xml
+ #- name: Setup tmate
+ # if: ${{ failure() }}
+ # uses: mxschmitt/action-tmate@v3
+ publish-test-results:
+ needs: test
+ runs-on: ubuntu-latest
+ if: success() || failure()
+ steps:
+ - name: Download Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+ - name: Publish test results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ junit_files: artifacts/**/junit_report*.xml
diff --git a/.gitignore b/.gitignore
index 812c96a98..664036ae7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
-*.mat
!icons.mat
.DS_Store
@@ -11,4 +10,41 @@ docs/.sass-cache/
.jekyll-metadata
_pdf
-*.sublime-workspace
\ No newline at end of file
+*.sublime-workspace
+*.tap
+
+*.asv
+
+dev/standalone/Linux/Source/
+dev/standalone/Win/Source/
+dev/standalone/MacOS/Source/
+
+*.swp
+*.swo
+*.xml
+*.rej
+
+**profile_results
+python/ctf
+.idea/
+**/*.pyc
+python/build/
+python/pyspinw/ctfs/
+
+includedSupportPackages.txt
+mccExcludedFiles.log
+requiredMCRProducts.txt
+unresolvedSymbols.txt
+
+coverage*xml
+junit*xml
+coverageReport
+coverage.html
+
+mltbx/mltbx/CITATION.cff
+mltbx/mltbx/license.txt
+mltbx/spinw.mltbx
+
+*.mexa64
+*.mexmaci64
+*.mexw64
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..fa1979aa4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,158 @@
+# [v4.0.0](https://github.com/spinw/spinw/compare/v3.2.0...v4.0.0)
+
+## New Features
+
+- Add a function to output Mantid MDHistogramWorkspaces (`sw_spec2MDHisto`)
+- Add Python plotting of magnetic structure using [vispy](https://vispy.org/)
+- Add mex files to compute main loop in `spinwave()` enabling a 2x - 4x speed up depending on system size
+- Add python wrapper for all Matlab functions so e.g. `sw_plotspec` etc can be called without the `m.` prefix in pyspinw:q
+
+## Improvements
+
+- Replace `spinwavefast()` method with a new `fastmode` option in the main `spinwave()` method to reduce confusion
+- Adds a `neutron_output` option to `spinwave()` to compute only the neutron (`Sperp`) cross-section and not the full spin-spin correlation tensor, saving memory
+
+## Bug Fixes
+
+- Corrects equation for Q-range from `thetaMin` in `sw_instrument` and add `thetaMax` option
+- Fixes `sw_issymspec` to recognise powder spectra
+- Fixes a parsing error in the `spinw.fourier` method if no sublat option given.
+- Fixes several bugs in `sw_plotspec` where it ignores user options in `'auto'` mode, and where it inverts user supplied colormaps.
+- Fixes several bugs in `.fitspec()` for handling twins and where it only outputs the final chi^2 values.
+
+
+# [v3.2.0](https://github.com/spinw/spinw/compare/0.0.1...v3.2.0)
+
+## Initial public beta of PySpinW
+
+This is an initial public beta version of PySpinW released on PyPI.
+
+Please install using:
+
+```bash
+pip install spinw
+```
+
+This will install a module called `pyspinw` (note the `py` at the start).
+
+You can then run SpinW with:
+
+```python
+import numpy as np
+import matplotlib.pyplot as plt
+from pyspinw import Matlab
+m = Matlab()
+swobj = m.spinw()
+swobj.genlattice('lat_const', [3, 3, 6], 'angled', [90, 90, 120], 'sym', 'P 1');
+swobj.addatom('r', [0, 0, 0], 'S', 1/2, 'label', 'MCu2')
+swobj.gencoupling('maxDistance', 5)
+swobj.addmatrix('label', 'J1', 'value', 1.00, 'color', 'g')
+swobj.addcoupling('mat', 'J1', 'bond', 1)
+swobj.genmagstr('mode', 'helical', 'k', [-1/3, -1/3, 0], 'n',[0, 0, 1], 'unit', 'lu', 'S', [[1], [0], [0]])
+spec = swobj.spinwave([[-1/2, 0, 0], [0, 0, 0], [1/2, 1/2, 0], 100], 'hermit', False)
+spec = m.sw_egrid(spec, 'component', 'Sxx+Syy', 'imagChk', False, 'Evect', np.linspace(0, 3, 100))
+ax = plt.imshow(np.real(np.flipud(spec['swConv'])), aspect='auto', vmax=1)
+plt.show()
+```
+
+On Windows and Linux systems, as long as you're running PySpinW locally, Matlab plotting commands like `m.plot(swobj)` will work. This is not the case on MacOS (a known bug) and on remote systems (e.g. via JupyterHub).
+
+# [v0.0.1](https://github.com/spinw/spinw/compare/v3.1.2...0.0.1)
+
+## pySpinW
+
+This is an initial release of pySpinW as a `pip` installable wheel for python >= 3.8 and MATLAB >= R2021a
+
+### Installation
+
+Please install with
+
+```bash
+pip install pyspinw*.whl
+```
+
+This package can now be used in python if you have a version of MATLAB or MCR available on the machine.
+The package will try to automatically detect your installation, however if it is in a non-standard location, the path and version will have to be specified.
+
+```python
+from pyspinw import Matlab
+m = Matlab(matlab_version='R2023a', matlab_path='/usr/local/MATLAB/R2023a/')
+```
+
+### Example
+
+An example would be:
+
+```python
+import numpy as np
+from pyspinw import Matlab
+
+m = Matlab()
+
+# Create a spinw model, in this case a triangular antiferromagnet
+s = m.sw_model('triAF', 1)
+
+# Specify the start and end points of the q grid and the number of points
+q_start = [0, 0, 0]
+q_end = [1, 1, 0]
+pts = 501
+
+# Calculate the spin wave spectrum
+spec = m.spinwave(s, [q_start, q_end, pts])
+```
+
+### Known limitations
+
+At the moment graphics will not work on macOS systems and is disabled.
+
+
+# [v3.1.2](https://github.com/spinw/spinw/compare/v3.1.0...v3.1.2)
+
+## Improvements
+
+- Change to preallocation of output energies in `spinwave` to reduce memory usage and improve calculation speed
+- Use a mex function (if mex is enabled) for matrix multiplication in `spinwave` with `hermit=false` that reduces memory usage and improves calculation speed for large magnetic cells (in an example with 216 magnetic atoms the execution time was reduced by ~65%)
+
+
+## Bug Fixes
+
+- Fix generation of lattice from basis vectors in `genlattice`, see issue [#28](https://github.com/SpinW/spinw/issues/28)
+- `sortMode` in `spinwave` now correctly sorts the spin wave modes within each twin
+- A `spinw` object can now be correctly created from a structure figure
+- `.cif` files with a mixture of tabs and spaces or containing a `?` in the comments can now be read correctly
+- Rotation matrix `rotC` in `addtwin` is now required to be a valid rotation or reflection matrix.
+- Spin of atom in `addatom` must have `S>=0`.
+- Anisotropic g-tensor in `addg` must be physically valid - i.e. :math:`g^\dagger.g` must be a symmetric positive definite matrix.
+- Fix bug in addcoupling that did not allow user to supply 'atom' with numeric array of atom indices (previously only worked for string or cell of strings corresponding to atom labels).
+- Renamed undocumented `gencoupling` parameter `tol` to `tolMaxDist` (see doc string of `gencoupling` for more details).
+- Added validation to `gencoupling` to ensure `maxDistance > dMin`.
+- Fixed uncaught error in `gencoupling` by checking if any bonds have length < `maxSym`
+- A warning will now be emitted if `saveSabp` is requested in `spinwave` for a commensurate structure
+- Fix bug in definition of rotation matrix transforming to spinw coordinate system when left-handed set of basis vectors supplied to `genlattice`, see issue [#57](https://github.com/SpinW/spinw/issues/57)
+- Validation added for `perm` and `origin` arguments supplied to `genlattice` (and warn users that these will be ignored if no symmetry/spacegroup is supplied in the same function call).
+- Deprecated `spgr` argument to `genlattice` (users should use `sym` instead).
+- Fix `MATLAB:nonLogicalConditional` error raised when using multiple k in `genmagstr` with `helical` mode
+- Raise error if invalid shape `S` or `k` is provided to `genmagstr`, previously they would be silently set to zero
+- Raise error if wrong number of spins `S` is provided to `genmagstr` in `helical` mode. Previously the structure would be silently initialised to a random structure.
+- Raise error if a complex spin `S` is provided to `genmagstr` in `helical` mode. Previously this meant it would silently ignore the `n` option, and behave exactly like `fourier` mode.
+- Raise error if `rotate` mode is used without first initialising a magnetic structure
+- Emit deprecation warning if the undocumented `extend` mode is used in `genmagstr`
+- Raise error if the first spin is parallel to `n` and no rotation angle is provided in `rotate` mode in `genmagstr`. Previously this would silently result in `NaN`
+- Raise error if `phi` or `phid` is not real in `rotate` mode in `genmagstr`. This was an undocumented feature which has been removed.
+- Emit warning that the spin amplitude will be moderated if components of `S` are parallel to `n` in `helical` mode in `genmagstr`
+- Emit warning if `nExt` is unnecessarily large compared to `k` in `helical` and `fourier` modes in `genmagstr`
+- Emit warning if arguments that will be ignored are passed to a particular mode in `genmagstr` (e.g. `S` is passed to `random`)
+- Raise error if complex values is provided for `n` in `genmagstr`. 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`.
+- Fix errors when calling `intmatrix` with dipolar bonds and symbolic spinw object with fitmode true and false
+- Ensure biquadratic exchange interactions are isotropic in `addcoupling` (previously checked in `intmatrix`)
+- 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.
+- Warn users that that the results of `spinwave` have not been scientifically validated for supercell structures with an incommensurate modulation.
+- Emit warning if wrong length `xmin`, `xmax` or `x0` is passed to `optmagstr`. Previously they would be silently ignored.
+- No longer require a magnetic structure be initialised with `genmagstr` before using `optmagstr`. If not intialised, a default `nExt` of `[1 1 1]` is used. This has also been clarified in the docstring.
+- Fix bug where powder spectra was not recognised in `sw_plotspec`, introduced by a previous update to provide more helpful error messages.
+- `sw_instrument` now calculates the limits for thetaMax, before it was using the continuation of the thetaMin line to high Q which is incorrect.
+- Fixes a parsing error in the `spinw.fourier` method if no sublat option given.
+
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 000000000..ff3961ad7
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,28 @@
+cff-version: "1.1.0"
+message: "If you have used SpinW in your research, please cite it as below"
+abstract: "SpinW is a library for spin wave calculations"
+authors:
+ - family-names: "Tóth"
+ given-names: "Sándor"
+ orcid: "https://orcid.org/0000-0002-7174-9399"
+ - family-names: "Ward"
+ given-names: "Simon"
+ orcid: "https://orcid.org/0000-0001-7127-5763"
+ - family-names: "Le"
+ given-names: "Manh Duc"
+ orcid: "https://orcid.org/0000-0003-3012-6053"
+ - family-names: "Fair"
+ given-names: "Rebecca L."
+ orcid: "https://orcid.org/0000-0002-0926-2942"
+ - family-names: "Waite"
+ given-names: "Richard"
+title: "libpymcr"
+version: "4.0.0"
+date-released: "2023-06-12"
+license: "GPL-3.0-only"
+repository: "https://github.com/spinw/spinw"
+url: "https://www.spinw.org"
+keywords:
+ - "Python"
+ - "Matlab"
+
diff --git a/Contents.m b/Contents.m
index b6855d7ba..fa5d3eddd 100644
--- a/Contents.m
+++ b/Contents.m
@@ -1,3 +1,3 @@
% SpinW
-% Version 4.0 (unreleased) 29-Nov-2017
+% Version 3.1 (unreleased) 29-Nov-2017
%
diff --git a/README.md b/README.md
index 58b03bfdc..bf596cec5 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,38 @@
-[](https://zenodo.org/badge/latestdoi/33274418) [](https://twitter.com/intent/user?screen_name=spinw4) [](https://github.com/tsdev/spinw/releases)
+[](https://doi.org/10.5281/zenodo.2651100)[](https://twitter.com/intent/user?screen_name=spinw4) [](https://github.com/tsdev/spinw/releases)[](https://github.com/spinw/spinw/releases)
-**SpinW** (*spin-double-u*) is a Matlab library that can optimize magnetic structures using mean field theory and calculate spin wave dispersion and spin-spin correlation function for complex crystal and magnetic structures. For details check http://www.psi.ch/spinw.
+**SpinW** (*spin-double-u*) is a Matlab library that can optimize magnetic structures using mean field theory and calculate spin wave dispersion and spin-spin correlation function for complex crystal and magnetic structures. For details check http://www.spinw.org
-Keep up to date on announcements and more by following [@spinw4](https://twitter.com/intent/user?screen_name=spinw4) on Twitter.
+# Current Status
+We are currently in a period of change. **SpinW will be moving to python/C++ (with a Matlab interface)**. I'm sure you can appreciate that this will be a lot of work as all of the code will be completely re-written and updated. In this period the Matlab version will be stabilized at v3.1.1 with bug fixes and reviewed external pull requests. More details of the new version will follow. [**For Q&A we are testing GitHub Discussions.**](https://github.com/SpinW/spinw/discussions)
# Documentation
* experimental and under construction, the address can change in the future
* documentation of the master branch
* use `swdoc`/`swhelp` instead of the Matlab built-in `doc`/`help` functions to get help on SpinW
-* can be also accessed from the browser: https://tsdev.github.io/spinwdoc/
+* can be also accessed from the browser: https://spinw.github.io/spinwdoc/
+
+# Build Status
+Currently automated testing is on the `ubuntu-latest`, `windows-latest` and `macos-latest` Github actions runners (Ubuntu 20.04, Windows Server 2022, macOS-11 as of 16/11/22) and the latest Matlab version available from the [setup-matlab](https://github.com/matlab-actions/setup-matlab) action (R2022b as of 16/11/22). We also test on Ubuntu 20.04 and MATLAB2020a. It should be noted that MATLAB symbolic calculation changed post R2018a and as such symbolic results may be differ with a relative tolerance of < 0.03%.
+
+Testing can be pulled from the [testing](https://www.github.com/spinw/testing) repository and run with the `runspinwFunctionalityTests` command from the `Testing` directory.
+
diff --git a/dev/sw_release.m b/dev/sw_release.m
index 5e998d78f..b04418be8 100644
--- a/dev/sw_release.m
+++ b/dev/sw_release.m
@@ -130,13 +130,20 @@ function sw_release(verNum, tempDir)
% files with '~' and 'sw_release.m' file
fList = rdir('**/*');
fListZip = {};
+if ispc
+ sp = '\';
+else
+ sp = '/';
+end
+dirname = [pwd sp];
for ii = 1:numel(fList)
if (~any(strfind(fList(ii).name,[filesep '.']))) && (~any(strfind(fList(ii).name,'~'))) ...
&& (~any(strfind(fList(ii).name,[filesep 'dev' filesep]))) ...
&& (~any(strfind(fList(ii).name,[filesep 'docs' filesep]))) ...
&& (~any(strfind(fList(ii).name,[filesep 'test' filesep]))) ...
- && (~any(strfind(fList(ii).name,[filesep 'tutorials' filesep])))
+ && (~any(strfind(fList(ii).name,[filesep 'tutorials' filesep]))) ...
+ && (~any(strfind(fList(ii).folder,'git')))
fListZip{end+1} = fList(ii).name;
end
end
diff --git a/docs/developers.md b/docs/developers.md
new file mode 100644
index 000000000..580ea420c
--- /dev/null
+++ b/docs/developers.md
@@ -0,0 +1,32 @@
+# Release workflow
+
+The release workflow is mostly automated using continuous integration builds,
+but some actions and triggering a release needs to be done manually by the developer.
+
+To create a release:
+
+1. Create a branch and edit the `CHANGELOG.md` and `CITATION.cff` files to update it with a new version number.
+2. Create a new PR from the branch. The PR must have `RELEASE` in the title.
+3. This will trigger a build with multiple versions of python.
+4. Review the branch, check that all tests for all python versions pass and if so merge.
+5. Once merged, the CI should create a github release in "Draft" mode.
+6. Check that the release page is correct (has the wheel and `mltbx` files and the release notes are ok).
+7. Check that the wheel and `mltbx` toolbox can be installed and work.
+8. Then manually trigger the `Publish to PyPI` action to upload the wheel to PyPI.
+
+
+In particular, in step 1:
+
+* in `CHANGELOG.md` the first title line must have the form:
+
+```
+# [](https://github.com/spinw/spinw/compare/...)
+```
+
+* in `CITATION.cff` the `version` field must be updated with a new version
+
+If the version string in these two files do not match, or if the version string matches an existing git tag,
+then the CI build will fail.
+
+Also note that in step 8, after uploading to PyPI the release cannot be changed on PyPI (only deleted).
+If a release is deleted, you have to then create a new release version (PyPI does not allow overwriting previous releases).
diff --git a/docs/docgenerator/docgen.m b/docs/docgenerator/docgen.m
index 589dfab9b..dfc12f195 100644
--- a/docs/docgenerator/docgen.m
+++ b/docs/docgenerator/docgen.m
@@ -1,12 +1,12 @@
%% setup help generator options
swPath = {'swfiles/@spinw' 'swfiles' 'swfiles/+swplot' 'swfiles/@swpref' 'swfiles/+swsym' 'swfiles/+swfunc' 'swfiles/+ndbase'};
-swr = sw_rootdir;
+swr = sw_rootdir();
swPath = cellfun(@(C)[swr C],swPath,'UniformOutput',false);
swver = sw_version;
-outPath = '~/spinwdoc_git';
-docPath = '~/spinw_git/docs';
-upload = true;
+outPath = fullfile(userpath,'Release','docgen');
+docPath = [outPath filesep 'docs'];
+upload = false;
recalc = true;
%% generate help
diff --git a/external/chol_omp/chol_omp.cpp b/external/chol_omp/chol_omp.cpp
index d03d46f4e..db5c4d59d 100644
--- a/external/chol_omp/chol_omp.cpp
+++ b/external/chol_omp/chol_omp.cpp
@@ -32,6 +32,7 @@
#include
#include
#include
+#include
#include "mex.h"
#include "matrix.h"
#include "blas.h"
@@ -46,12 +47,72 @@ void omp_set_num_threads(int nThreads) {};
#include
#endif
+// Define templated gateways to single / double LAPACK functions
+template
+void potrf(const char *uplo, const ptrdiff_t *n, T *a, const ptrdiff_t *lda, ptrdiff_t *info, bool is_complex) {
+ mexErrMsgIdAndTxt("chol_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void potrf(const char *uplo, const ptrdiff_t *n, float *a, const ptrdiff_t *lda, ptrdiff_t *info, bool is_complex) {
+ if(is_complex)
+ return cpotrf(uplo, n, a, lda, info);
+ else
+ return spotrf(uplo, n, a, lda, info);
+}
+template <> void potrf(const char *uplo, const ptrdiff_t *n, double *a, const ptrdiff_t *lda, ptrdiff_t *info, bool is_complex) {
+ if(is_complex)
+ return zpotrf(uplo, n, a, lda, info);
+ else
+ return dpotrf(uplo, n, a, lda, info);
+}
+
+template
+void trtri(const char *uplo, const char *diag, const ptrdiff_t *n, T *a, const ptrdiff_t *lda, ptrdiff_t *info, bool is_complex) {
+ mexErrMsgIdAndTxt("chol_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void trtri(const char *uplo, const char *diag, const ptrdiff_t *n, float *a, const ptrdiff_t *lda, ptrdiff_t *info, bool is_complex) {
+ if(is_complex)
+ return ctrtri(uplo, diag, n, a, lda, info);
+ else
+ return strtri(uplo, diag, n, a, lda, info);
+}
+template <> void trtri(const char *uplo, const char *diag, const ptrdiff_t *n, double *a, const ptrdiff_t *lda, ptrdiff_t *info, bool is_complex) {
+ if(is_complex)
+ return ztrtri(uplo, diag, n, a, lda, info);
+ else
+ return dtrtri(uplo, diag, n, a, lda, info);
+}
+
+template
+void trmm(const char *side, const char *uplo, const char *transa, const char *diag, const ptrdiff_t *m, const ptrdiff_t *n,
+ const T *alpha, const T *a, const ptrdiff_t *lda, T *b, const ptrdiff_t *ldb, bool is_complex) {
+ mexErrMsgIdAndTxt("chol_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void trmm(const char *side, const char *uplo, const char *transa, const char *diag, const ptrdiff_t *m, const ptrdiff_t *n,
+ const float *alpha, const float *a, const ptrdiff_t *lda, float *b, const ptrdiff_t *ldb, bool is_complex) {
+ if(is_complex)
+ return ctrmm(side, uplo, transa, diag, m, n, alpha, a, lda, b, ldb);
+ else
+ return strmm(side, uplo, transa, diag, m, n, alpha, a, lda, b, ldb);
+}
+template <> void trmm(const char *side, const char *uplo, const char *transa, const char *diag, const ptrdiff_t *m, const ptrdiff_t *n,
+ const double *alpha, const double *a, const ptrdiff_t *lda, double *b, const ptrdiff_t *ldb, bool is_complex) {
+ if(is_complex)
+ return ztrmm(side, uplo, transa, diag, m, n, alpha, a, lda, b, ldb);
+ else
+ return dtrmm(side, uplo, transa, diag, m, n, alpha, a, lda, b, ldb);
+}
+
+template
+int do_loop(mxArray *plhs[], const mxArray *prhs[], int nthread, mwSignedIndex m, int nlhs,
+ int *blkid, char uplo, T tol, bool do_Colpa);
+
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
mwSignedIndex m, n, nd;
const size_t *dims;
int ib, nblock, nb, err_code=0;
bool do_Colpa = false;
+ bool is_single = false;
int *blkid;
char uplo = 'U', *parstr;
int nthread = omp_get_max_threads();
@@ -59,8 +120,12 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
// mexPrintf("Number of threads = %d\n",nthread);
// Checks inputs
- if(!mxIsNumeric(prhs[0])) {
- mexErrMsgIdAndTxt("chol_omp:notnumeric","Input matrix must be a numeric array.");
+ if(mxIsDouble(prhs[0])) {
+ is_single = false;
+ } else if(mxIsSingle(prhs[0])) {
+ is_single = true;
+ } else {
+ mexErrMsgIdAndTxt("chol_omp:notfloat","Input matrix must be a float array.");
}
nd = mxGetNumberOfDimensions(prhs[0]);
if(nd<2 || nd>3) {
@@ -109,85 +174,94 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
}
// Creates outputs
- if(mxIsComplex(prhs[0])) {
- if(nd==2)
- plhs[0] = mxCreateDoubleMatrix(m, m, mxCOMPLEX);
- else
- plhs[0] = mxCreateNumericArray(3, dims, mxDOUBLE_CLASS, mxCOMPLEX);
- if(nlhs>1) {
- if(do_Colpa) {
- if(nd==2)
- plhs[1] = mxCreateDoubleMatrix(m, m, mxCOMPLEX);
- else
- plhs[1] = mxCreateNumericArray(3, dims, mxDOUBLE_CLASS, mxCOMPLEX);
- }
- else {
- if(nd==2)
- plhs[1] = mxCreateDoubleMatrix(1, 1, mxREAL);
- else
- plhs[1] = mxCreateDoubleMatrix(1, nblock, mxREAL);
- }
+ mxComplexity complexflag = mxIsComplex(prhs[0]) ? mxCOMPLEX : mxREAL;
+ mxClassID classid = is_single ? mxSINGLE_CLASS : mxDOUBLE_CLASS;
+ if(nd==2)
+ plhs[0] = mxCreateNumericMatrix(m, m, classid, complexflag);
+ else
+ plhs[0] = mxCreateNumericArray(3, dims, classid, complexflag);
+ if(nlhs>1) {
+ if(do_Colpa) {
+ if(nd==2)
+ plhs[1] = mxCreateNumericMatrix(m, m, classid, complexflag);
+ else
+ plhs[1] = mxCreateNumericArray(3, dims, classid, complexflag);
}
- }
- else {
- if(nd==2)
- plhs[0] = mxCreateDoubleMatrix(m, m, mxREAL);
- else
- plhs[0] = mxCreateNumericArray(3, dims, mxDOUBLE_CLASS, mxREAL);
- if(nlhs>1) {
- if(do_Colpa) {
- if(nd==2)
- plhs[1] = mxCreateDoubleMatrix(m, m, mxREAL);
- else
- plhs[1] = mxCreateNumericArray(3, dims, mxDOUBLE_CLASS, mxREAL);
- }
- else {
- if(nd==2)
- plhs[1] = mxCreateDoubleMatrix(1, 1, mxREAL);
- else
- plhs[1] = mxCreateDoubleMatrix(1, nblock, mxREAL);
- }
+ else {
+ if(nd==2)
+ plhs[1] = mxCreateNumericMatrix(1, 1, classid, mxREAL);
+ else
+ plhs[1] = mxCreateNumericMatrix(1, nblock, classid, mxREAL);
}
}
-#pragma omp parallel default(none) shared(plhs,prhs,err_code) \
- firstprivate(nthread, m, nlhs, nd, ib, blkid, uplo, tol, do_Colpa)
+ if(is_single) {
+ float stol = std::max((float)tol, (float)sqrt(FLT_EPSILON));
+ err_code = do_loop(plhs, prhs, nthread, m, nlhs, blkid, uplo, stol, do_Colpa);
+ } else {
+ err_code = do_loop(plhs, prhs, nthread, m, nlhs, blkid, uplo, tol, do_Colpa);
+ }
+
+ delete[]blkid;
+ if(err_code==1)
+ mexErrMsgIdAndTxt("chol_omp:notposdef","The input matrix is not positive definite.");
+ else if(err_code==2)
+ mexErrMsgIdAndTxt("chol_omp:singular","The input matrix is singular.");
+}
+
+template
+int do_loop(mxArray *plhs[], const mxArray *prhs[], int nthread, mwSignedIndex m, int nlhs,
+ int *blkid, char uplo, T tol, bool do_Colpa)
+{
+ int err_nonpos = 0, err_singular = 0;
+ T *lhs0, *ilhs0, *lhs1, *ilhs1;
+ lhs0 = (T*)mxGetData(plhs[0]);
+ ilhs0 = (T*)mxGetImagData(plhs[0]);
+ if (nlhs > 1) {
+ lhs1 = (T*)mxGetData(plhs[1]);
+ ilhs1 = (T*)mxGetImagData(plhs[1]);
+ }
+ T* rhs0 = (T*)mxGetData(prhs[0]);
+ T* irhs0 = (T*)mxGetImagData(prhs[0]);
+ bool is_complex = mxIsComplex(prhs[0]);
+#pragma omp parallel default(none) shared(err_nonpos, err_singular) \
+ firstprivate(nthread, m, nlhs, blkid, uplo, tol, do_Colpa, lhs0, lhs1, rhs0, irhs0, ilhs0, ilhs1, is_complex)
{
#pragma omp for
for(int nt=0; nt0?2:1); kk++) {
// Populate the matrix input array (which will be overwritten by the Lapack function)
- if(mxIsComplex(prhs[0])) {
- memset(M, 0, 2*m*m*sizeof(double));
- ptr_M = mxGetPr(prhs[0]) + ib*m2;
- ptr_Mi = mxGetPi(prhs[0]) + ib*m2;
+ if(is_complex) {
+ memset(M, 0, 2*m*m*sizeof(T));
+ ptr_M = rhs0 + ib*m2;
+ ptr_Mi = irhs0 + ib*m2;
// Interleaves complex matrices - Matlab stores complex matrix as an array of real
// values followed by an array of imaginary values; Fortran (and C++ std::complex)
// and hence Lapack stores it as arrays of pairs of values (real,imaginary).
@@ -200,39 +274,38 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
}
else {
// *potrf overwrites the input array - copy only upper or lower triangle of input.
- M = mxGetPr(plhs[0]) + ib*m2;
- ptr_M = mxGetPr(prhs[0]) + ib*m2;
+ M = lhs0 + ib*m2;
+ ptr_M = rhs0 + ib*m2;
if(uplo=='U')
for(ii=0; ii0) {
if(nlhs<=1 || do_Colpa) {
- #pragma omp critical
- {
- err_code = 1;
- }
+ // Have to use this becase VSC only supports OpenMP 2.0, allowing only {op}=, ++, -- in atomic
+ #pragma omp atomic
+ err_nonpos++;
break;
}
else {
- ptr_I = mxGetPr(plhs[1]) + ib;
- *ptr_I = (double)info;
+ ptr_I = lhs1 + ib;
+ *ptr_I = (T)info;
// Zeros the non positive parts of the factor.
//kk = (mwSignedIndex)info-1;
//if(uplo=='U')
@@ -245,45 +318,43 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
}
if(do_Colpa) {
// Computes the Hermitian K^2 matrix = R*gComm*R';
- memcpy(Mp, M, mxIsComplex(prhs[0]) ? m*m*2*sizeof(double) : m*m*sizeof(double));
+ memcpy(Mp, M, is_complex ? m*m*2*sizeof(T) : m*m*sizeof(T));
// Applies the commutator [1..1,-1..-1] to the cholesky factor transposed
for(ii=m/2; ii1) {
- if(mxIsComplex(prhs[0])) {
- ztrtri(&uplo, &diag, &m, M, &lda, &info);
+ if(is_complex) {
+ trtri(&uplo, &diag, &m, M, &lda, &info, true);
}
else {
- M = mxGetPr(plhs[1]) + ib*m2;
- ptr_M = mxGetPr(plhs[0]) + ib*m2;
+ M = lhs1 + ib*m2;
+ ptr_M = lhs0 + ib*m2;
if(uplo=='U')
for(ii=0; ii0) {
- #pragma omp critical
- {
- err_code = 2;
- }
+ #pragma omp atomic
+ err_singular++;
break;
}
- if(mxIsComplex(prhs[0])) {
- ptr_M = mxGetPr(plhs[1]) + ib*m2;
- ptr_Mi = mxGetPi(plhs[1]) + ib*m2;
+ if(is_complex) {
+ ptr_M = lhs1 + ib*m2;
+ ptr_Mi = ilhs1 + ib*m2;
for(ii=0; ii 0)
break; // One of the threads got a singular or not pos def error - break loop here.
}
// Free memory...
- if(mxIsComplex(prhs[0]))
+ if(is_complex)
delete[]M;
if(do_Colpa) {
delete[]Mp; delete[]alpha;
}
#ifndef _OPENMP
- if(err_code!=0)
+ if((err_nonpos + err_singular) > 0)
break;
#endif
}
}
- delete[]blkid;
- if(err_code==1)
- mexErrMsgIdAndTxt("chol_omp:notposdef","The input matrix is not positive definite.");
- else if(err_code==2)
- mexErrMsgIdAndTxt("chol_omp:singular","The input matrix is singular.");
+ return err_nonpos > 0 ? 1 : (err_singular > 0 ? 2 : 0);
}
diff --git a/external/chol_omp/chol_omp.mexa64 b/external/chol_omp/chol_omp.mexa64
index 912c5d680..1e77d871c 100755
Binary files a/external/chol_omp/chol_omp.mexa64 and b/external/chol_omp/chol_omp.mexa64 differ
diff --git a/external/chol_omp/chol_omp.mexmaci64 b/external/chol_omp/chol_omp.mexmaci64
index 15ca23b0a..5b9d0e71f 100755
Binary files a/external/chol_omp/chol_omp.mexmaci64 and b/external/chol_omp/chol_omp.mexmaci64 differ
diff --git a/external/chol_omp/chol_omp.mexw64 b/external/chol_omp/chol_omp.mexw64
index 8d3c3199a..aa6ac4472 100755
Binary files a/external/chol_omp/chol_omp.mexw64 and b/external/chol_omp/chol_omp.mexw64 differ
diff --git a/external/eig_omp/eig_omp.cpp b/external/eig_omp/eig_omp.cpp
index 545b46b4f..ddf3abd31 100644
--- a/external/eig_omp/eig_omp.cpp
+++ b/external/eig_omp/eig_omp.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include "mex.h"
#include "matrix.h"
#include "lapack.h"
@@ -39,11 +40,95 @@ void omp_set_num_threads(int nThreads) {};
#include
#endif
+// Define LAPACK functions depending on type
+template
+void igeev(const char *jobvl, const char *jobvr, const ptrdiff_t *n, T *a, const ptrdiff_t *lda, T *w, T *vl, const ptrdiff_t *ldvl,
+ T *vr, const ptrdiff_t *ldvr, T *work, const ptrdiff_t *lwork, T *rwork, ptrdiff_t *info) {
+ mexErrMsgIdAndTxt("eig_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void igeev(const char *jobvl, const char *jobvr, const ptrdiff_t *n, float *a, const ptrdiff_t *lda, float *w, float *vl, const ptrdiff_t *ldvl,
+ float *vr, const ptrdiff_t *ldvr, float *work, const ptrdiff_t *lwork, float *rwork, ptrdiff_t *info) {
+ return cgeev(jobvl, jobvr, n, a, lda, w, vl, ldvl, vr, ldvr, work, lwork, rwork, info);
+}
+template <> void igeev(const char *jobvl, const char *jobvr, const ptrdiff_t *n, double *a, const ptrdiff_t *lda, double *w, double *vl, const ptrdiff_t *ldvl,
+ double *vr, const ptrdiff_t *ldvr, double *work, const ptrdiff_t *lwork, double *rwork, ptrdiff_t *info) {
+ return zgeev(jobvl, jobvr, n, a, lda, w, vl, ldvl, vr, ldvr, work, lwork, rwork, info);
+}
+
+template
+void geev(const char *jobvl, const char *jobvr, const ptrdiff_t *n, T *a, const ptrdiff_t *lda, T *wr, T *wi, T *vl,
+ const ptrdiff_t *ldvl, T *vr, const ptrdiff_t *ldvr, T *work, const ptrdiff_t *lwork, ptrdiff_t *info) {
+ mexErrMsgIdAndTxt("eig_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void geev(const char *jobvl, const char *jobvr, const ptrdiff_t *n, float *a, const ptrdiff_t *lda, float *wr, float *wi, float *vl,
+ const ptrdiff_t *ldvl, float *vr, const ptrdiff_t *ldvr, float *work, const ptrdiff_t *lwork, ptrdiff_t *info) {
+ return sgeev(jobvl, jobvr, n, a, lda, wr, wi, vl, ldvl, vr, ldvr, work, lwork, info);
+}
+template <> void geev(const char *jobvl, const char *jobvr, const ptrdiff_t *n, double *a, const ptrdiff_t *lda, double *wr, double *wi, double *vl,
+ const ptrdiff_t *ldvl, double *vr, const ptrdiff_t *ldvr, double *work, const ptrdiff_t *lwork, ptrdiff_t *info) {
+ return dgeev(jobvl, jobvr, n, a, lda, wr, wi, vl, ldvl, vr, ldvr, work, lwork, info);
+}
+
+template
+void ievr(const char *jobz, const char *range, const char *uplo, const ptrdiff_t *n, T *a, const ptrdiff_t *lda,
+ const T *vl, const T *vu, const ptrdiff_t *il, const ptrdiff_t *iu, const T *abstol, ptrdiff_t *m, T *w, T *z,
+ const ptrdiff_t *ldz, ptrdiff_t *isuppz, T *work, const ptrdiff_t *lwork, T *rwork, const ptrdiff_t *lrwork,
+ ptrdiff_t *iwork, const ptrdiff_t *liwork, ptrdiff_t *info) {
+ mexErrMsgIdAndTxt("eig_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void ievr(const char *jobz, const char *range, const char *uplo, const ptrdiff_t *n, float *a, const ptrdiff_t *lda,
+ const float *vl, const float *vu, const ptrdiff_t *il, const ptrdiff_t *iu, const float *abstol, ptrdiff_t *m, float *w, float *z,
+ const ptrdiff_t *ldz, ptrdiff_t *isuppz, float *work, const ptrdiff_t *lwork, float *rwork, const ptrdiff_t *lrwork,
+ ptrdiff_t *iwork, const ptrdiff_t *liwork, ptrdiff_t *info) {
+ return cheevr(jobz, range, uplo, n, a, lda, vl, vu, il, iu, abstol, m, w, z, ldz, isuppz, work, lwork, rwork, lrwork, iwork, liwork, info);
+}
+template <> void ievr(const char *jobz, const char *range, const char *uplo, const ptrdiff_t *n, double *a, const ptrdiff_t *lda,
+ const double *vl, const double *vu, const ptrdiff_t *il, const ptrdiff_t *iu, const double *abstol, ptrdiff_t *m, double *w, double *z,
+ const ptrdiff_t *ldz, ptrdiff_t *isuppz, double *work, const ptrdiff_t *lwork, double *rwork, const ptrdiff_t *lrwork,
+ ptrdiff_t *iwork, const ptrdiff_t *liwork, ptrdiff_t *info) {
+ return zheevr(jobz, range, uplo, n, a, lda, vl, vu, il, iu, abstol, m, w, z, ldz, isuppz, work, lwork, rwork, lrwork, iwork, liwork, info);
+}
+
+template
+void evr(const char *jobz, const char *range, const char *uplo, const ptrdiff_t *n, T *a, const ptrdiff_t *lda,
+ const T *vl, const T *vu, const ptrdiff_t *il, const ptrdiff_t *iu, const T *abstol, ptrdiff_t *m, T *w, T *z,
+ const ptrdiff_t *ldz, ptrdiff_t *isuppz, T *work, const ptrdiff_t *lwork, ptrdiff_t *iwork,
+ const ptrdiff_t *liwork, ptrdiff_t *info) {
+ mexErrMsgIdAndTxt("eig_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void evr(const char *jobz, const char *range, const char *uplo, const ptrdiff_t *n, float *a, const ptrdiff_t *lda,
+ const float *vl, const float *vu, const ptrdiff_t *il, const ptrdiff_t *iu, const float *abstol, ptrdiff_t *m, float *w, float *z,
+ const ptrdiff_t *ldz, ptrdiff_t *isuppz, float *work, const ptrdiff_t *lwork, ptrdiff_t *iwork,
+ const ptrdiff_t *liwork, ptrdiff_t *info) {
+ return ssyevr(jobz, range, uplo, n, a, lda, vl, vu, il, iu, abstol, m, w, z, ldz, isuppz, work, lwork, iwork, liwork, info);
+}
+template <> void evr(const char *jobz, const char *range, const char *uplo, const ptrdiff_t *n, double *a, const ptrdiff_t *lda,
+ const double *vl, const double *vu, const ptrdiff_t *il, const ptrdiff_t *iu, const double *abstol, ptrdiff_t *m, double *w, double *z,
+ const ptrdiff_t *ldz, ptrdiff_t *isuppz, double *work, const ptrdiff_t *lwork, ptrdiff_t *iwork,
+ const ptrdiff_t *liwork, ptrdiff_t *info) {
+ return dsyevr(jobz, range, uplo, n, a, lda, vl, vu, il, iu, abstol, m, w, z, ldz, isuppz, work, lwork, iwork, liwork, info);
+}
+
+template
+void gesvd(const char *jobu, const char *jobvt, const ptrdiff_t *m, const ptrdiff_t *n, T *a, const ptrdiff_t *lda, T *s,
+ T *u, const ptrdiff_t *ldu, T *vt, const ptrdiff_t *ldvt, T *work, const ptrdiff_t *lwork, T *rwork, ptrdiff_t *info) {
+ mexErrMsgIdAndTxt("eig_omp:wrongtype","This function is only defined for single and double floats.");
+}
+template <> void gesvd(const char *jobu, const char *jobvt, const ptrdiff_t *m, const ptrdiff_t *n, float *a, const ptrdiff_t *lda, float *s,
+ float *u, const ptrdiff_t *ldu, float *vt, const ptrdiff_t *ldvt, float *work, const ptrdiff_t *lwork, float *rwork, ptrdiff_t *info) {
+ return cgesvd(jobu, jobvt, m, n, a, lda, s, u, ldu, vt, ldvt, work, lwork, rwork, info);
+}
+template <> void gesvd(const char *jobu, const char *jobvt, const ptrdiff_t *m, const ptrdiff_t *n, double *a, const ptrdiff_t *lda, double *s,
+ double *u, const ptrdiff_t *ldu, double *vt, const ptrdiff_t *ldvt, double *work, const ptrdiff_t *lwork, double *rwork, ptrdiff_t *info) {
+ return zgesvd(jobu, jobvt, m, n, a, lda, s, u, ldu, vt, ldvt, work, lwork, rwork, info);
+}
+
// Flips a (column-major) matrix by columns, like the matlab function.
-void fliplr(double *M, mwSignedIndex m, mwSignedIndex n, double *vec, bool isreal)
+template
+void fliplr(T *M, mwSignedIndex m, mwSignedIndex n, T *vec, bool isreal)
{
int ii;
- double val, *p0r, *p1r, *p0i, *p1i;
+ T val, *p0r, *p1r, *p0i, *p1i;
// Just a row vector, reverse order of values.
if(m==1) {
if(isreal) {
@@ -66,7 +151,7 @@ void fliplr(double *M, mwSignedIndex m, mwSignedIndex n, double *vec, bool isrea
// Actual matrix - assume column major
else {
if(isreal) {
- size_t msz = m*sizeof(double);
+ size_t msz = m*sizeof(T);
for(ii=0; ii<(n/2); ii++) {
memcpy(vec, M+(n-ii-1)*n, msz);
memcpy(M+(n-ii-1)*n, M+ii*n, msz);
@@ -75,7 +160,7 @@ void fliplr(double *M, mwSignedIndex m, mwSignedIndex n, double *vec, bool isrea
}
else {
mwSignedIndex n2 = n*2;
- size_t m2sz = 2*m*sizeof(double);
+ size_t m2sz = 2*m*sizeof(T);
for(ii=0; ii<(n/2); ii++) {
memcpy(vec, M+(n-ii-1)*n2, m2sz);
memcpy(M+(n-ii-1)*n2, M+ii*n2, m2sz);
@@ -87,9 +172,10 @@ void fliplr(double *M, mwSignedIndex m, mwSignedIndex n, double *vec, bool isrea
// Quicksort modified from public domain implementation by Darel Rex Finley.
// http://alienryderflex.com/quicksort/
-void quicksort(int *id, double *val, mwSignedIndex elements)
+template
+void quicksort(int *id, T *val, mwSignedIndex elements)
{
- double piv;
+ T piv;
int i=0, L, R, C, swap;
int beg[300], end[300];
beg[0]=0; end[0]=(int)elements;
@@ -104,6 +190,9 @@ void quicksort(int *id, double *val, mwSignedIndex elements)
while (val[id[L]]<=piv && L 300) {
+ mexErrMsgIdAndTxt("eig_omp:qsortoverflow", "Quicksort ran out of memory");
+ }
if (end[i]-beg[i]>end[i-1]-beg[i-1])
{
swap=beg[i]; beg[i]=beg[i-1]; beg[i-1]=swap;
@@ -115,10 +204,12 @@ void quicksort(int *id, double *val, mwSignedIndex elements)
}
// This is called for real general matrices - complex conjugate eigenvectors stored in consecutive columns
-void sort(mwSignedIndex m, double *Dr, double *Di, double *V, double *work, int sort_type)
+template
+void sort(mwSignedIndex m, T *Dr, T *Di, T *V, T *work, int sort_type)
{
int *id, ii, jj;
- double *val, *pD, *pDi, *pV, abstol = sqrt(DBL_EPSILON);
+ T *val, *pD, *pDi, *pV;
+ T abstol = sqrt(std::numeric_limits::epsilon());
size_t msz;
// Assume(!) that workspace is max[3*m+601,m*(m+3)] large.
@@ -142,11 +233,11 @@ void sort(mwSignedIndex m, double *Dr, double *Di, double *V, double *work, int
// Now permute the eigenvalues and eigenvectors
// this is horendously bloated... maybe try Fich et al. 1995
// http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.29.2256
- msz = m*sizeof(double);
+ msz = m*sizeof(T);
pD = work+m; memcpy(pD, Dr, msz);
pDi = work+2*m; memcpy(pDi, Di, msz);
- if(V!=0)
- pV = work+3*m; memcpy(pV, V, m*m*sizeof(double));
+ if(V != NULL) {
+ pV = work+3*m; memcpy(pV, V, m*m*sizeof(T)); }
for(ii=0; ii<(int)m; ii++) {
// Take care to preserve the order of the eigenvectors (real parts first)
if((ii+1)<(int)m)
@@ -157,20 +248,21 @@ void sort(mwSignedIndex m, double *Dr, double *Di, double *V, double *work, int
}
pD[ii] = Dr[id[ii]];
pDi[ii] = Di[id[ii]];
- if(V!=0)
- memcpy(&pV[ii*m], &V[id[ii]*m], msz);
+ if(V != NULL) {
+ memcpy(&pV[ii*m], &V[id[ii]*m], msz); }
}
memcpy(Dr, pD, msz);
memcpy(Di, pDi, msz);
- if(V!=0)
- memcpy(V, pV, m*m*sizeof(double));
+ if(V != NULL) {
+ memcpy(V, pV, m*m*sizeof(T)); }
}
// This is called for complex general matrices - both eigenvalues and eigenvectors are actually complex
-void sort(mwSignedIndex m, double *D, double *V, double *work, int sort_type)
+template
+void sort(mwSignedIndex m, T *D, T *V, T *work, int sort_type)
{
int *id, ii;
- double *val, *pD, *pV;
+ T *val, *pD, *pV;
size_t msz;
// Assume(!) that workspace is max[3*m+601,2*m*(m+3)] large.
@@ -193,35 +285,36 @@ void sort(mwSignedIndex m, double *D, double *V, double *work, int sort_type)
quicksort(id, val, (int)m);
// Now permute the eigenvalues and eigenvectors - in future look at using Fich et al. 1995
- msz = 2*m*sizeof(double);
+ msz = 2*m*sizeof(T);
pD = work+m; memcpy(pD, D, msz);
- if(V!=0)
- pV = work+3*m; memcpy(pV, V, 2*m*m*sizeof(double));
+ if(V != NULL) {
+ pV = work+3*m; memcpy(pV, V, 2*m*m*sizeof(T)); }
for(ii=0; ii<(int)m; ii++) {
pD[ii*2] = D[id[ii]*2];
pD[ii*2+1] = D[id[ii]*2+1];
- if(V!=0)
- memcpy(&pV[ii*2*m], &V[id[ii]*2*m], msz);
+ if(V != NULL) {
+ memcpy(&pV[ii*2*m], &V[id[ii]*2*m], msz); }
}
memcpy(D, pD, msz);
- if(V!=0)
- memcpy(V, pV, 2*m*m*sizeof(double));
+ if(V != NULL) {
+ memcpy(V, pV, 2*m*m*sizeof(T)); }
}
// This is called for real general matrices - complex conjugate eigenvectors stored in consecutive columns
-int orth(mwSignedIndex m, double *Dr, double *Di, double *Vr, double *Vi, double *work, bool isreal)
+template
+int orth(mwSignedIndex m, T *Dr, T *Di, T *Vr, T *Vi, T *work, bool isreal)
{
int *id, ii, jj, kk, nn;
mwSignedIndex n, info;
- double *pVz, *pS;
- double abstol = sqrt(DBL_EPSILON);
+ T *pVz, *pS;
+ T abstol = sqrt(std::numeric_limits::epsilon());
char jobu = 'O';
char jobvt = 'N';
mwSignedIndex lwork = 3*m;
- double *zwork = work + (2*m*(m+7)) - 2*3*m;
- double *rwork = zwork - 5*m;
+ T *zwork = work + (2*m*(m+7)) - 2*3*m;
+ T *rwork = zwork - 5*m;
- // Assume workspace is size [m*(m+7)]*sizeof(complexdouble)
+ // Assume workspace is size [m*(m+7)]*sizeof(complex)
id = (int*)work;
for(ii=0; ii<(int)m; ii++)
id[ii] = ii;
@@ -270,7 +363,7 @@ int orth(mwSignedIndex m, double *Dr, double *Di, double *Vr, double *Vi, double
}
// Does the singular value decomposition to get the orthogonal basis and singular values
pS = work + 2*(n+1)*m;
- zgesvd(&jobu, &jobvt, &m, &n, pVz, &m, pS, pVz, &m, pVz, &m, zwork, &lwork, rwork, &info);
+ gesvd(&jobu, &jobvt, &m, &n, pVz, &m, pS, pVz, &m, pVz, &m, zwork, &lwork, rwork, &info);
// Checks that number of singular values == n
for(nn=0; nn
+int orth(mwSignedIndex m, T *D, T *V, T *work, bool isreal)
{
int *id, ii, jj, kk, nn;
mwSignedIndex n, info;
- double *Dr, *pVz, *pS;
- double abstol = sqrt(DBL_EPSILON);
+ T *Dr, *pVz, *pS;
+ T abstol = sqrt(std::numeric_limits::epsilon());
char jobu = 'O';
char jobvt = 'N';
mwSignedIndex lwork = 3*m;
- double *zwork = work + (2*m*(m+7)) - 2*3*m;
- double *rwork = zwork - 5*m;
- size_t msz = 2*m*sizeof(double);
+ T *zwork = work + (2*m*(m+10)) - 2*3*m;
+ T *rwork = zwork - 2*5*m;
+ size_t msz = 2*m*sizeof(T);
- // Assume workspace is size [m*(m+7)]*sizeof(complexdouble)
+ // Assume workspace is size [m*(m+10)]*sizeof(complex)
id = (int*)work;
Dr = work + m;
for(ii=0; ii<(int)m; ii++) {
@@ -330,8 +424,8 @@ int orth(mwSignedIndex m, double *D, double *V, double *work, bool isreal)
for(jj=ii; jj
+void check_sym(bool *issym, const mxArray *mat, T tolsymm, mwSignedIndex m, int nthread, int *blkid, bool is_complex)
+{
+#pragma omp parallel default(none) shared(issym, mat) firstprivate(nthread, m, tolsymm, blkid, is_complex)
+ if(is_complex) {
+ T *A = (T*)mxGetData(mat);
+ T *Ai = (T*)mxGetImagData(mat);
+#pragma omp for
+ for(int nt=0; nt tolsymm
+ ||fabs( *(Ai+ii+jj*m) + *(Ai+jj+ii*m) ) > tolsymm) {
+ issym[ib] = false;
+ break;
+ }
+ }
+ if(!issym[ib])
+ break;
+ }
+ A += m*m;
+ Ai += m*m;
+ }
+ }
+ }
+ else {
+ T *A = (T*)mxGetData(mat);
+#pragma omp for
+ for(int nt=0; nt tolsymm) {
+ issym[ib] = false;
+ break;
+ }
+ }
+ if(!issym[ib])
+ break;
+ }
+ A += m*m;
+ }
+ }
+ }
+}
+
+template
+int do_loop(T *mat, T *mat_i, mxArray *plhs[], int nthread, mwSignedIndex m, int nlhs, mwSignedIndex nd,
+ const int *blkid, char jobz, bool anynonsym, const bool *issym, bool do_orth, int do_sort, bool is_complex);
+
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
mwSignedIndex m, n, nd;
@@ -356,18 +502,21 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
int *blkid, valint;
char jobz, *parstr, *valstr;
bool *issym, anynonsym=false, do_orth=false, do_Colpa=false;
- // Tolerance on whether matrix is symmetric/hermitian
- double tolsymm = sqrt(DBL_EPSILON);
+ bool is_single;
int nthread = omp_get_max_threads();
- int err_code;
+ int err_code = 0;
// mexPrintf("Number of threads = %d\n",nthread);
// Checks inputs
-// if(nrhs!=1) {
-// mexErrMsgIdAndTxt("eig_omp:nargin","Number of input argument must be 1.");
-// }
- if(!mxIsNumeric(prhs[0])) {
- mexErrMsgIdAndTxt("eig_omp:notnumeric","Input matrix must be a numeric array.");
+ if(nrhs<1) {
+ mexErrMsgIdAndTxt("eig_omp:nargin","Number of input argument must be at least 1.");
+ }
+ if(mxIsDouble(prhs[0])) {
+ is_single = false;
+ } else if(mxIsSingle(prhs[0])) {
+ is_single = true;
+ } else {
+ mexErrMsgIdAndTxt("eig_omp:notfloat","Input matrix must be a float array.");
}
nd = mxGetNumberOfDimensions(prhs[0]);
if(nd<2 || nd>3) {
@@ -449,7 +598,7 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
}
}
}
-// mexPrintf("do_orth = %d; do_sort = %d\n",do_orth,do_sort);
+ //mexPrintf("do_orth = %d; do_sort = %d\n",do_orth,do_sort);
// More efficient to group blocks together to run in a single thread than to spawn one thread per matrix.
if(nblock tolsymm
- ||fabs( *(Ai+ii+jj*m) + *(Ai+jj+ii*m) ) > tolsymm) {
- issym[ib] = false;
- break;
- }
- }
- if(!issym[ib])
- break;
- }
- A += m*m;
- Ai += m*m;
- }
- }
- }
- else {
- double *A = mxGetPr(prhs[0]);
-#pragma omp for
- for(int nt=0; nt tolsymm) {
- issym[ib] = false;
- break;
- }
- }
- if(!issym[ib])
- break;
- }
- A += m*m;
- }
- }
+
+ mxClassID classid;
+ bool is_complex = mxIsComplex(prhs[0]);
+ if(is_single) {
+ // Tolerance on whether matrix is symmetric/hermitian
+ check_sym(issym, prhs[0], (float)(FLT_EPSILON * 10.0), m, nthread, blkid, is_complex);
+ classid = mxSINGLE_CLASS;
+ } else {
+ check_sym(issym, prhs[0], (double)(DBL_EPSILON * 10.0), m, nthread, blkid, is_complex);
+ classid = mxDOUBLE_CLASS;
}
- for(ib=0; ib1) {
- if(nd==2)
- plhs[0] = mxCreateDoubleMatrix(m, m, mxCOMPLEX);
- else
- plhs[0] = mxCreateNumericArray(3, dims, mxDOUBLE_CLASS, mxCOMPLEX);
- }
+ if(nd==2)
+ plhs[1] = mxCreateNumericMatrix(m, m, classid, complexflag);
+ else
+ plhs[1] = mxCreateNumericMatrix(m, nblock, classid, complexflag);
+
+ // If some matrices are not symmetric, will get complex conjugate eigenpairs
+ complexflag = (mxIsComplex(prhs[0]) || anynonsym) ? mxCOMPLEX : mxREAL;
+ if(nd==2)
+ plhs[0] = mxCreateNumericMatrix(m, m, classid, complexflag);
+ else
+ plhs[0] = mxCreateNumericArray(3, dims, classid, complexflag);
}
- else {
- if(nlhs>1) {
- if(nd==2)
- plhs[0] = mxCreateDoubleMatrix(m, m, mxREAL);
- else
- plhs[0] = mxCreateNumericArray(3, dims, mxDOUBLE_CLASS, mxREAL);
- }
+ //mexPrintf("IsComplex=%d, anynonsym=%d, nlhs=%d\n", mxIsComplex(prhs[0]), anynonsym, nlhs);
+ //mexEvalString("drawnow;");
+
+ void *i_part = is_complex ? mxGetImagData(prhs[0]) : NULL;
+ if(is_single) {
+ err_code = do_loop((float *)mxGetData(prhs[0]), (float *)i_part, plhs, nthread, m, nlhs, nd,
+ blkid, jobz, anynonsym, issym, do_orth, do_sort, is_complex);
+ } else {
+ err_code = do_loop((double *)mxGetData(prhs[0]), (double *)i_part, plhs, nthread, m, nlhs, nd,
+ blkid, jobz, anynonsym, issym, do_orth, do_sort, is_complex);
}
-#pragma omp parallel default(none) shared(plhs,prhs,err_code) \
- firstprivate(nthread, m, nlhs, nd, ib, blkid, jobz, anynonsym, issym, do_orth, do_sort)
+ delete[]blkid; delete[]issym;
+ if(err_code > 0)
+ mexErrMsgIdAndTxt("eig_omp:defectivematrix","Eigenvectors of defective eigenvalues cannot be orthogonalised.");
+}
+
+template
+int do_loop(T *mat, T *mat_i, mxArray *plhs[], int nthread, mwSignedIndex m, int nlhs, mwSignedIndex nd,
+ const int *blkid, char jobz, bool anynonsym, const bool *issym, bool do_orth, int do_sort, bool is_complex)
+{
+ int err_code = 0;
+ T *lhs0, *ilhs0, *lhs1, *ilhs1;
+ lhs0 = (T*)mxGetData(plhs[0]);
+ ilhs0 = (T*)mxGetImagData(plhs[0]);
+ if (nlhs > 1) {
+ lhs1 = (T*)mxGetData(plhs[1]);
+ ilhs1 = (T*)mxGetImagData(plhs[1]);
+ }
+#pragma omp parallel default(none) shared(mat, mat_i, err_code, blkid, issym) \
+ firstprivate(nthread, m, nlhs, nd, jobz, anynonsym, do_orth, do_sort, is_complex, lhs0, lhs1, ilhs0, ilhs1)
{
#pragma omp for
for(int nt=0; nt::max();
+ T vl = -vu;
mwSignedIndex il = 0, iu;
- double abstol = sqrt(DBL_EPSILON);
+ T abstol = sqrt(std::numeric_limits::epsilon());
mwSignedIndex lda, ldz, numfind;
mwSignedIndex info, lwork, liwork, lzwork;
mwSignedIndex *isuppz, *iwork;
- double *work, *zwork;
+ T *work, *zwork;
int ii, jj;
lda = m;
ldz = m;
iu = m;
m2 = m*m;
m22 = 2*m2;
- msz = m2*sizeof(double);
- lwork = do_orth ? ( (26*m>(2*m*(m+7))) ? 26*m : (2*m*(m+7)) )
+ msz = m2*sizeof(T);
+ lwork = do_orth ? ( (26*m>(2*m*(m+10))) ? 26*m : (2*m*(m+10)) )
: ( (26*m>(2*m*(m+3))) ? 26*m : (2*m*(m+3)) );
liwork = 10*m;
isuppz = new mwSignedIndex[2*m];
- work = new double[lwork];
+ work = new T[lwork];
iwork = new mwSignedIndex[liwork];
- if(mxIsComplex(prhs[0])) {
+ if(is_complex) {
lzwork = 4*m;
- M = new double[m22];
- zwork = new double[lzwork*2];
+ M = new T[m22];
+ zwork = new T[lzwork*2];
if(nlhs>1)
- V = new double[m22];
+ V = new T[m22];
}
else
- M = new double[m2];
+ M = new T[m2];
// The output of the _evr Lapack routines gives eigenvalues as vectors
// If we want it as a diagonal matrix, need to use a temporary array...
- if(mxIsComplex(prhs[0]) && anynonsym) {
- D = new double[2*m];
+ if(is_complex && anynonsym) {
+ D = new T[2*m];
}
else if(nlhs>1 && nd==2) {
- D = new double[m];
+ D = new T[m];
if(anynonsym)
- Di = new double[m];
+ Di = new T[m];
}
// Actual loop over individual matrices start here
- for(ib=blkid[nt]; ib1)
sort(m, D, V, work, do_sort);
else
- sort(m, D, 0, work, do_sort);
- if(do_orth)
+ sort(m, D, (T*)NULL, work, do_sort);
+ if(nlhs>1 && do_orth)
if(orth(m, D, V, work, 0)==1) {
- #pragma omp critical
- {
- err_code = 1;
- }
+ #pragma omp atomic
+ err_code++;
break;
}
}
else {
- zheevr(&jobz, &range, &uplo, &m, M, &lda, &vl, &vu, &il, &iu, &abstol, &numfind,
+ ievr(&jobz, &range, &uplo, &m, M, &lda, &vl, &vu, &il, &iu, &abstol, &numfind,
D, V, &ldz, isuppz, zwork, &lzwork, work, &lwork, iwork, &liwork, &info);
// ZHEEVR outputs eigenvectors in ascending order by default.
if(do_sort==-1) {
@@ -666,8 +787,8 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
}
}
if(nlhs>1) {
- ptr_V = mxGetPr(plhs[0]) + ib*m2;
- ptr_Vi = mxGetPi(plhs[0]) + ib*m2;
+ ptr_V = lhs0 + ib*m2;
+ ptr_Vi = ilhs0 + ib*m2;
for(ii=0; ii1)
sort(m, D, Di, V, work, do_sort);
else
- sort(m, D, Di, 0, work, do_sort);
+ sort(m, D, Di, (T*)NULL, work, do_sort);
if(nlhs>1 && do_orth)
- if(orth(m, D, Di, V, mxGetPi(plhs[0])+ib*m2, work, 1)==1) {
- #pragma omp critical
- {
- err_code = 1;
- }
+ if(orth(m, D, Di, V, lhs0+ib*m2, work, 1)==1) {
+ #pragma omp atomic
+ err_code++;
break;
}
}
else {
- dsyevr(&jobz, &range, &uplo, &m, M, &lda, &vl, &vu, &il, &iu, &abstol, &numfind,
+ evr(&jobz, &range, &uplo, &m, M, &lda, &vl, &vu, &il, &iu, &abstol, &numfind,
D, V, &ldz, isuppz, work, &lwork, iwork, &liwork, &info);
// DSYEVR outputs eigenvectors in ascending order by default.
if(do_sort==-1) {
@@ -719,8 +838,8 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
// Need to account for complex conjugate eigenvalue/vector pairs (imaginary parts
// of eigenvectors stored in consecutive columns)
if(nlhs>1 && !issym[ib] && !do_orth) {
- ptr_V = mxGetPr(plhs[0]) + ib*m2;
- ptr_Vi = mxGetPi(plhs[0]) + ib*m2;
+ ptr_V = lhs0 + ib*m2;
+ ptr_Vi = ilhs0 + ib*m2;
// Complex conjugate pairs of eigenvalues appear consecutively with the eigenvalue
// having the positive imaginary part first.
for(ii=0; ii1 && nd==2) {
- if(mxIsComplex(prhs[0]) && anynonsym) {
- E = mxGetPr(plhs[1]) + ib*m2;
+ if(is_complex && anynonsym) {
+ E = lhs1 + ib*m2;
for(ii=0; ii1)
delete[]V;
}
- if(nlhs>1 && nd==2)
+ if(is_complex && anynonsym) {
+ delete[]D;
+ }
+ else if(nlhs>1 && nd==2) {
delete[]D;
+ if(anynonsym)
+ delete[]Di;
+ }
#ifndef _OPENMP
if(err_code!=0)
break;
#endif
}
}
- delete[]blkid; delete[]issym;
- if(err_code==1)
- mexErrMsgIdAndTxt("eig_omp:defectivematrix","Eigenvectors of defective eigenvalues cannot be orthogonalised.");
+ return err_code;
}
diff --git a/external/eig_omp/eig_omp.mexa64 b/external/eig_omp/eig_omp.mexa64
index 3b349e7b1..a70fc5f74 100755
Binary files a/external/eig_omp/eig_omp.mexa64 and b/external/eig_omp/eig_omp.mexa64 differ
diff --git a/external/eig_omp/eig_omp.mexmaci64 b/external/eig_omp/eig_omp.mexmaci64
index 2a97af42d..45595aa75 100755
Binary files a/external/eig_omp/eig_omp.mexmaci64 and b/external/eig_omp/eig_omp.mexmaci64 differ
diff --git a/external/eig_omp/eig_omp.mexw64 b/external/eig_omp/eig_omp.mexw64
index fa6fe02d8..99bbaab30 100755
Binary files a/external/eig_omp/eig_omp.mexw64 and b/external/eig_omp/eig_omp.mexw64 differ
diff --git a/external/eigorth.m b/external/eigorth.m
index 798e3e7eb..c945727db 100644
--- a/external/eigorth.m
+++ b/external/eigorth.m
@@ -45,9 +45,9 @@
nStack = size(M,3);
% Use OpenMP parallelised mex file if it exists
-if nStack>1 && useMex
+if nStack>1 && any(useMex)
% eigenvalues are already orthogonalised by eig_omp
- [V, D] = eig_omp(M,'orth');
+ [V, D] = eig_omp(M,'orth','sort','descend');
return
end
diff --git a/external/mmat.m b/external/mmat.m
index b69ef2d8d..93eed500a 100644
--- a/external/mmat.m
+++ b/external/mmat.m
@@ -38,6 +38,7 @@
if (nargin < 3)
dim = [1 2];
end
+isdefaultdim = all(dim == [1 2]);
if numel(dim)~=2
error('mmat:WrongInput','dim has to be a two element array!');
@@ -51,9 +52,49 @@
nDB = ndims(B);
nD = max(nDA,nDB);
+if nD == 2 && isdefaultdim
+ C = A * B;
+ return
+end
+
nA = [size(A),ones(1,nD-nDA)]; nA = nA(dim);
nB = [size(B),ones(1,nD-nDB)]; nB = nB(dim);
+% Array is double float which is 8 bytes per element, but this needs to be doubled
+% (16 bytes/element) as sum / bsxfun do not operate in-place and need a temp matrix
+neededMem = (numel(A)*nB(2) + numel(B)*nA(1)) * 16;
+if sw_freemem < neededMem && isdefaultdim
+ % Not enough memory to expand matrix to use bsxfun; use slow loop instead
+ szA = size(A); if numel(szA) > 3, A = reshape(A, [szA(1:2) prod(szA(3:end))]); end
+ szB = size(B); if numel(szB) > 3, B = reshape(B, [szB(1:2) prod(szB(3:end))]); end
+ if numel(szA) == 2 && numel(szB) > 2
+ output_shape = [szA(1) szB(2:end)];
+ C = zeros([szA(1) szB(2) size(B,3)]);
+ for ii = 1:size(B,3)
+ C(:,:,ii) = A * B(:,:,ii);
+ end
+ elseif numel(szA) > 2 && numel(szB) == 2
+ output_shape = [szA(1) szB(2) szA(3:end)];
+ C = zeros([szA(1) szB(2) size(A,3)]);
+ for ii = 1:size(A,3)
+ C(:,:,ii) = A(:,:,ii) * B;
+ end
+ elseif size(A, 3) == size(B, 3)
+ output_shape = [szA(1) szB(2:end)];
+ C = zeros([szA(1) szB(2) size(A,3)]);
+ for ii = 1:size(A,3)
+ C(:,:,ii) = A(:,:,ii) * B(:,:,ii);
+ end
+ else
+ error('Extra dimensions do not agree');
+ end
+ szC = size(C);
+ if numel(szC) ~= numel(output_shape) || ~all(szC == output_shape)
+ C = reshape(C, output_shape);
+ end
+ return
+end
+
% form A matrix
% (nA1) x (nA2) x nB2
A = repmat(A,[ones(1,nD) nB(2)]);
@@ -73,4 +114,4 @@
% permute back the final result to the right size
C = permute(C,idx2);
-end
\ No newline at end of file
+end
diff --git a/external/mtimesx/mtimesx.mexmaci64 b/external/mtimesx/mtimesx.mexmaci64
deleted file mode 100755
index 20ebb8164..000000000
Binary files a/external/mtimesx/mtimesx.mexmaci64 and /dev/null differ
diff --git a/external/mtimesx/mtimesx.mexw64 b/external/mtimesx/mtimesx.mexw64
deleted file mode 100755
index d6bc3b3a2..000000000
Binary files a/external/mtimesx/mtimesx.mexw64 and /dev/null differ
diff --git a/external/mtimesx/mtimesx.c b/external/mtimesx/sw_mtimesx.c
similarity index 100%
rename from external/mtimesx/mtimesx.c
rename to external/mtimesx/sw_mtimesx.c
diff --git a/external/mtimesx/mtimesx.m b/external/mtimesx/sw_mtimesx.m
similarity index 98%
rename from external/mtimesx/mtimesx.m
rename to external/mtimesx/sw_mtimesx.m
index 2f02f3c1a..f060fc845 100644
--- a/external/mtimesx/mtimesx.m
+++ b/external/mtimesx/sw_mtimesx.m
@@ -110,8 +110,8 @@
% exception is a sparse scalar times an nD full array. In that special case,
% mtimesx will treat the sparse scalar as a full scalar and return a full nD result.
%
-% Note: The ‘N’, ‘T’, and ‘C’ have the same meanings as the direct inputs to the BLAS
-% routines. The ‘G’ input has no direct BLAS counterpart, but was relatively easy to
+% Note: The N, T, and C have the same meanings as the direct inputs to the BLAS
+% routines. The G input has no direct BLAS counterpart, but was relatively easy to
% implement in mtimesx and saves time (as opposed to computing conj(A) or conj(B)
% explicitly before calling mtimesx).
%
@@ -262,6 +262,7 @@
%
% ---------------------------------------------------------------------------------------------------------------------------------
+%{
function varargout = mtimesx(varargin)
%\
@@ -277,3 +278,4 @@
[varargout{1:nargout}] = mtimesx(varargin{:});
end
+%}
diff --git a/external/mtimesx/mtimesx.mexa64 b/external/mtimesx/sw_mtimesx.mexa64
similarity index 100%
rename from external/mtimesx/mtimesx.mexa64
rename to external/mtimesx/sw_mtimesx.mexa64
diff --git a/external/mtimesx/sw_mtimesx.mexmaci64 b/external/mtimesx/sw_mtimesx.mexmaci64
new file mode 100755
index 000000000..ca3b95f88
Binary files /dev/null and b/external/mtimesx/sw_mtimesx.mexmaci64 differ
diff --git a/external/mtimesx/sw_mtimesx.mexw64 b/external/mtimesx/sw_mtimesx.mexw64
new file mode 100755
index 000000000..04c45748c
Binary files /dev/null and b/external/mtimesx/sw_mtimesx.mexw64 differ
diff --git a/external/sw_qconv/sw_qconv.cpp b/external/sw_qconv/sw_qconv.cpp
new file mode 100644
index 000000000..7d2e3e91b
--- /dev/null
+++ b/external/sw_qconv/sw_qconv.cpp
@@ -0,0 +1,85 @@
+#include "mex.h"
+#include
+#include
+#include
+#include
+
+template
+void loop(T *swOut, const mwSize *d1, const double stdG, const double *Qconv, const T *swConv, size_t i0, size_t i1) {
+ for (size_t ii=i0; ii fG(d1[1], 0.0);
+ for (size_t jj=0; jj
+void do_calc(T *swOut, const mwSize *d1, const double stdG, const double *Qconv, const T *swConv, size_t nThread) {
+ if (d1[1] > 10*nThread) {
+ size_t nBlock = d1[1] / nThread;
+ size_t i0 = 0, i1 = nBlock;
+ std::vector swv(nThread);
+ std::vector threads;
+ for (size_t ii=0; ii, std::ref(swv[ii]), std::ref(d1), stdG, std::ref(Qconv), std::ref(swConv), i0, i1)
+ );
+ i0 = i1;
+ i1 += nBlock;
+ if (i1 > d1[1] || ii == (nThread - 2)) {
+ i1 = d1[1]; }
+ }
+ for (size_t ii=0; ii(swOut, d1, stdG, Qconv, swConv, 0, d1[1]);
+ }
+}
+
+void mexFunction( int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] )
+{
+ if (nrhs < 3) {
+ throw std::runtime_error("sw_qconv: Requires 3 arguments");
+ }
+ size_t nThreads = 8;
+ if (nrhs == 4) {
+ nThreads = (size_t)(*mxGetDoubles(prhs[3]));
+ }
+ if (mxIsComplex(prhs[1])) { throw std::runtime_error("Arg 2 is complex\n"); }
+ if (mxIsComplex(prhs[2])) { throw std::runtime_error("Arg 3 is complex\n"); }
+ const mwSize *d1 = mxGetDimensions(prhs[0]);
+ const mwSize *d2 = mxGetDimensions(prhs[1]);
+ if (d1[1] != d2[1]) { throw std::runtime_error("Arg 1 and 2 size mismatch\n"); }
+ if (mxGetNumberOfElements(prhs[2]) > 1) { throw std::runtime_error("Arg 3 should be scalar\n"); }
+ double *Qconv = mxGetDoubles(prhs[1]);
+ double stdG = *(mxGetDoubles(prhs[2]));
+ if (mxIsComplex(prhs[0])) {
+ std::complex *swConv = reinterpret_cast*>(mxGetComplexDoubles(prhs[0]));
+ plhs[0] = mxCreateDoubleMatrix(d1[0], d1[1], mxCOMPLEX);
+ std::complex *swOut = reinterpret_cast*>(mxGetComplexDoubles(plhs[0]));
+ do_calc(swOut, d1, stdG, Qconv, swConv, nThreads);
+ } else {
+ double *swConv = mxGetDoubles(prhs[0]);
+ plhs[0] = mxCreateDoubleMatrix(d1[0], d1[1], mxREAL);
+ double *swOut = mxGetDoubles(plhs[0]);
+ do_calc(swOut, d1, stdG, Qconv, swConv, nThreads);
+ }
+}
diff --git a/external/swloop/swloop.cpp b/external/swloop/swloop.cpp
new file mode 100644
index 000000000..28d4e9c9c
--- /dev/null
+++ b/external/swloop/swloop.cpp
@@ -0,0 +1,618 @@
+#include
+#include "mex.h"
+#include "Eigen/Core"
+#include "Eigen/Cholesky"
+#include "Eigen/Eigenvalues"
+#include
+#include
+#include
+#include
+#include
+
+typedef Eigen::Matrix RowMatrixXd;
+typedef std::complex cd_t;
+typedef std::tuple, Eigen::VectorXcd> ev_tuple_t;
+
+struct pars {
+ bool hermit;
+ bool formfact;
+ bool incomm;
+ bool helical;
+ bool bq;
+ bool field;
+ double omega_tol;
+ size_t nMagExt;
+ size_t nTwin;
+ size_t nHkl;
+ size_t nBond;
+ size_t nBqBond;
+ bool fastmode;
+ bool neutron_output;
+ size_t nformula;
+ double nCell;
+};
+
+// In terms of the Toth & Lake paper ( https://arxiv.org/pdf/1402.6069.pdf ),
+// ABCD = [A B; B' A'] in eq (25) and (26)
+// ham_diag = [C 0; 0 C] in eq (25) and (26)
+// zed = u^alpha in eq (9)
+
+struct swinputs {
+ const double *hklExt; // 3*nQ array of Q-points
+ const std::complex *ABCD; // 3*nBond flattened array of non-q-dep part of H
+ const double *idxAll; // indices into ABCD to compute H using accumarray
+ const double *ham_diag; // the diagonal non-q-dep part of H
+ const double *dR; // 3*nBond array of bond vectors
+ const double *RR; // 3*nAtom array of magnetic ion coordinates
+ const double *S0; // nAtom vector of magnetic ion spin lengths
+ const std::complex *zed; // 3*nAtom array of moment direction vectors
+ const double *FF; // nQ*nAtom array of magnetic form factor values
+ const double *bqdR; // 3*nBond_bq array of biquadratic bond vectors
+ const double *bqABCD; // 3*nBond_bq array of non-q-dep part biquadratic H
+ const double *idxBq; // indices into bqABCD to compute H with accumarray
+ const double *bq_ham_diag; // diagonal part of the biquadratic Hamiltonian
+ std::vector< const double *> ham_MF_v; // The full Zeeman hamiltonian.
+ const int *idx0; // Indices into hklExt for twined Q
+ const double *n; // normal vector defining the rotating frame (IC calcs)
+ const double *rotc; // 3*3*nTwin twin rotation matrices
+ const double *hklA; // 3*nQ array of Q-points in Cartesian A^-1 units
+};
+
+struct swoutputs {
+ std::complex *omega;
+ std::complex *Sab;
+ bool warn_posdef;
+ bool warn_orth;
+};
+
+template
+using MatrixXT = Eigen::Matrix;
+
+template
+MatrixXT accumarray(Eigen::MatrixXd ind,
+ Eigen::Array data,
+ size_t sz1, size_t sz2=0) {
+ // Note this version of accumarray is explicitly 2D
+ if (sz2 == 0)
+ sz2 = sz1;
+ MatrixXT res = MatrixXT::Zero(sz1, sz2);
+ for (int ii=0; ii(ind(ii, 0)) - 1, static_cast(ind(ii, 1)) - 1) += data(ii);
+ }
+ return res;
+}
+
+std::atomic_int err_flag(0);
+std::vector errmsgs = {
+ std::string("all good"),
+ std::string("swloop:notposdef: The input matrix is not positive definite."),
+ std::string("swloop:cantdiag: Eigensolver could not converge")
+};
+
+void swcalc_fun(size_t i0, size_t i1, struct pars ¶ms, struct swinputs &inputs, struct swoutputs &outputs) {
+ // Setup
+ size_t nHam = 2 * params.nMagExt;
+ size_t nHam_sel = params.fastmode ? params.nMagExt : nHam;
+ size_t len_abcd = 3 * params.nBond;
+ // Assuming all data is column-major(!), so can be mapped directly to Eigen matrices
+ // We're also assuming that the memory blocks are actually as big as defined here!
+ // (E.g. the code which calls this needs to do some bounds checking)
+ Eigen::Map hklExt(inputs.hklExt, 3, params.nHkl);
+ Eigen::Map ABCD (inputs.ABCD, len_abcd);
+ Eigen::Map idxAll (inputs.idxAll, len_abcd, 2);
+ Eigen::Map ham_diag (inputs.ham_diag, nHam);
+ Eigen::Map dR (inputs.dR, params.nBond, 3);
+ Eigen::Map RR (inputs.RR, 3, params.nMagExt);
+ Eigen::VectorXd sqrtS0 = Eigen::VectorXd(params.nMagExt);
+ for (int ii=0; ii zed (inputs.zed, 3, params.nMagExt);
+ Eigen::Map FF (inputs.FF, params.nMagExt, params.nHkl);
+ Eigen::Map bqdR (inputs.bqdR, params.nBqBond, 3);
+ Eigen::Map bqABCD (inputs.bqABCD, 3 * params.nBqBond);
+ Eigen::Map idxBq (inputs.idxBq, 3 * params.nBqBond, 2);
+ Eigen::Map bq_ham_diag (inputs.bq_ham_diag, nHam);
+ std::vector< Eigen::Map > ham_MF;
+ if (params.field) {
+ for (size_t ii=0; ii(inputs.ham_MF_v[ii], nHam, nHam));
+ }
+ }
+
+ Eigen::MatrixXd gComm = Eigen::MatrixXd::Identity(nHam, nHam);
+ for (size_t ii=params.nMagExt; ii idx0(inputs.idx0, params.nHkl);
+ size_t nHklT = params.nHkl / params.nTwin;
+
+ std::complex *omega = outputs.omega;
+ std::complex *Sab_ptr, *Sperp_ptr;
+ if (params.neutron_output) {
+ Sab_ptr = new std::complex[9];
+ Sperp_ptr = outputs.Sab;
+ } else {
+ Sab_ptr = outputs.Sab;
+ }
+
+ size_t nHklI = params.nHkl / params.nTwin / 3;
+ Eigen::Matrix3cd K1, K2, cK1, nx, K, qPerp;
+ Eigen::Vector3d n;
+ Eigen::Matrix3cd m1 = Eigen::Matrix3cd::Identity(3, 3);
+ if (params.incomm) {
+ // Defines the Rodrigues rotation matrix to rotate Sab back
+ n << inputs.n[0], inputs.n[1], inputs.n[2];
+ nx << 0, -n(2), n(1),
+ n(2), 0, -n(0),
+ -n(1), n(0), 0;
+ K2 = n * n.adjoint().eval();
+ K1 = 0.5 * (m1 - K2 - nx * std::complex(0., 1.));
+ cK1 = K1.conjugate().eval();
+ }
+
+ // This is the main loop
+ for (size_t jj=i0; jj 0) {
+ return; }
+ }
+ Eigen::MatrixXcd V(nHam, nHam_sel);
+ Eigen::VectorXcd ExpF = exp((dR * hklExt(Eigen::all, jj)).array() * std::complex(0., 1.));
+ Eigen::MatrixXcd ham = accumarray(idxAll, ABCD.array() * ExpF.replicate(3,1).array(), nHam);
+ ham += ham_diag.asDiagonal();
+ if (params.bq) {
+ Eigen::VectorXcd bqExp = exp((bqdR * hklExt(Eigen::all, jj)).array() * std::complex(0., 1.));
+ Eigen::MatrixXcd bqham = accumarray(idxBq, bqABCD.array() * bqExp.replicate(3,1).array(), nHam);
+ ham += bqham;
+ ham += bq_ham_diag.asDiagonal();
+ }
+ if (params.field) {
+ ham += ham_MF[jj / nHklT];
+ }
+ ham = (ham + ham.adjoint().eval()) / 2.;
+ if (params.hermit) {
+ Eigen::LLT chol(ham);
+ if (chol.info() == Eigen::NumericalIssue) {
+ Eigen::VectorXcd eigvals = ham.eigenvalues();
+ double tol0 = abs(eigvals.real().minCoeff()) * sqrt(nHam) * 4.;
+ if (tol0 > params.omega_tol) {
+ err_flag.store(1, std::memory_order_relaxed);
+ return;
+ }
+ Eigen::MatrixXcd hamtol = ham + (Eigen::MatrixXcd::Identity(nHam, nHam) * tol0);
+ chol.compute(hamtol);
+ if (chol.info() == Eigen::NumericalIssue) {
+ chol.compute(ham + (Eigen::MatrixXcd::Identity(nHam, nHam) * params.omega_tol));
+ if (chol.info() == Eigen::NumericalIssue) {
+ err_flag.store(1, std::memory_order_relaxed);
+ return;
+ }
+ }
+ outputs.warn_posdef = true;
+ }
+ Eigen::MatrixXcd K = chol.matrixU();
+ Eigen::MatrixXcd K2 = K * gComm * K.adjoint().eval();
+ K2 = (K2 + K2.adjoint().eval()) / 2;
+ Eigen::SelfAdjointEigenSolver eig(K2);
+ if (eig.info() != Eigen::Success) {
+ err_flag.store(2, std::memory_order_relaxed);
+ return;
+ }
+ std::vector evs;
+ for (size_t ii=0; ii bool {
+ return std::real(std::get<0>(a)) > std::real(std::get<0>(b)); });
+ Eigen::MatrixXcd U(nHam, nHam_sel);
+ for (size_t ii=0; ii(evs[ii]);
+ U.col(ii) = std::get<1>(evs[ii]) * sqrt(gComm(ii,ii) * std::get<0>(evs[ii]));
+ }
+ V = K.inverse() * U;
+ } else {
+ Eigen::MatrixXcd gham = (gComm * ham).eval();
+ // Add a small amount to the diagonal to ensure there are no degenerate levels,
+ // so that the eigenvectors would be (quasi)orthogonal (instead of using eigorth()).
+ double vd = 0.5 - (double)params.nMagExt;
+ for (size_t ii=0; ii eig(gham);
+ if (eig.info() != Eigen::Success) {
+ err_flag.store(2, std::memory_order_relaxed);
+ return;
+ }
+ std::vector evs;
+ for (size_t ii=0; ii bool {
+ return std::real(std::get<0>(a)) > std::real(std::get<0>(b)); });
+ for (size_t ii=0; ii(evs[ii]); // Note this pointer arithmetic assumes column-major ordering
+ Eigen::ArrayXcd U = std::get<1>(evs[ii]);
+ std::complex vsum = (gComm.diagonal().array() * U.conjugate() * U).sum();
+ V.col(ii) = U * sqrt(1.0 / vsum);
+ }
+ }
+ Eigen::MatrixXcd zedExpF(nHam, 3);
+ for (size_t j1=0; j1 expF = exp(std::complex(0., -ph)) * sqrtS0(j1);
+ if (params.formfact) {
+ expF *= FF(j1, jj); }
+ for (size_t i2=0; i2<3; i2++) {
+ // Don't know why we have to use the reverse of the Matlab code here and in
+ // V.tranpose() below instead of V.adjoint() - but otherwise get wrong intensities...
+ zedExpF(j1, i2) = std::conj(zed(i2, j1)) * expF;
+ zedExpF(j1 + params.nMagExt, i2) = zed(i2, j1) * expF;
+ }
+ }
+ V = V.transpose().eval();
+ Eigen::MatrixXcd VExp(3, nHam_sel);
+ for (size_t j1=0; j1<3; j1++) {
+ VExp(j1, Eigen::all) = V * zedExpF(Eigen::all, j1);
+ }
+ if (params.incomm) {
+ size_t kk = jj % nHklT;
+ if (kk < nHklI) {
+ K = K1;
+ } else if (kk < 2*nHklI) {
+ K = K2;
+ } else {
+ K = cK1;
+ }
+ }
+ if (params.neutron_output) {
+ size_t iHklA = jj * 3;
+ Eigen::Vector3d hklAN;
+ hklAN << inputs.hklA[iHklA], inputs.hklA[iHklA + 1], inputs.hklA[iHklA + 2];
+ if (std::isnan(hklAN[0]) || std::isnan(hklAN[1]) || std::isnan(hklAN[2])) {
+ if (jj < (params.nHkl - 1)) {
+ hklAN << inputs.hklA[iHklA + 3], inputs.hklA[iHklA + 4], inputs.hklA[iHklA + 5];
+ } else {
+ hklAN << 1., 0., 0.;
+ }
+ }
+ qPerp = m1 - (hklAN * hklAN.transpose().eval());
+ }
+ for (int j1=0; j1 Sab(Sab_ptr);
+ Sab = VExp(Eigen::all, j1) * VExp(Eigen::all, j1).adjoint();
+ if (params.incomm) {
+ if (params.helical) {
+ // integrating out the arbitrary initial phase of the helix
+ Eigen::Matrix3cd tmp = (nx * Sab * nx) - ((K2 - m1) * Sab * K2) - (K2 * Sab * (2*K2 - m1));
+ Sab = 0.5 * (Sab - tmp);
+ }
+ Sab = Sab * K;
+ }
+ if (params.nTwin > 1) {
+ // Rotates the correlation function by the twin rotation matrix
+ size_t iT = jj / nHklT;
+ Eigen::Map rotC(inputs.rotc + iT*9, 3, 3);
+ Sab = rotC * Sab * rotC.transpose().eval();
+ }
+ Sab /= params.nCell;
+ if (params.neutron_output) {
+ if (params.nformula > 0) {
+ Sab /= params.nformula;
+ }
+ Sab = (Sab + Sab.transpose().eval()) / 2;
+ *(Sperp_ptr++) = (qPerp.cwiseProduct(Sab)).sum();
+ } else {
+ Sab_ptr += 9; // This trick only works for column-major data layouts!
+ }
+ }
+ }
+ if (params.neutron_output) {
+ delete[](Sab_ptr); }
+}
+
+template T getVal(const mxArray *d) {
+ mxClassID cat = mxGetClassID(d);
+ switch (cat) {
+ case mxDOUBLE_CLASS: return (T)*(mxGetDoubles(d)); break;
+ case mxSINGLE_CLASS: return (T)*(mxGetSingles(d)); break;
+ case mxINT32_CLASS: return (T)*(mxGetInt32s(d)); break;
+ case mxUINT32_CLASS: return (T)*(mxGetUint32s(d)); break;
+ case mxINT64_CLASS: return (T)*(mxGetInt64s(d)); break;
+ case mxUINT64_CLASS: return (T)*(mxGetUint64s(d)); break;
+ case mxINT16_CLASS: return (T)*(mxGetInt16s(d)); break;
+ case mxUINT16_CLASS: return (T)*(mxGetUint16s(d)); break;
+ case mxINT8_CLASS: return (T)*(mxGetInt8s(d)); break;
+ case mxUINT8_CLASS: return (T)*(mxGetUint8s(d)); break;
+ default:
+ throw std::runtime_error("Unknown mxArray class");
+ }
+}
+template <> bool getVal(const mxArray *d) { return (bool)*((mxLogical*)mxGetData(d)); }
+template
+T getField(const mxArray *data_ptr, size_t idx, const char *fieldname, T default_val) {
+ mxArray *field = mxGetField(data_ptr, idx, fieldname);
+ if (field == nullptr) {
+ return default_val;
+ }
+ return getVal(field);
+}
+
+void checkDims(std::string varname, const mxArray *data, size_t dim1, size_t dim2) {
+ const mwSize *dims = mxGetDimensions(data);
+ if ((size_t)dims[0] != dim1 || (size_t)dims[1] != dim2) {
+ throw std::runtime_error("Input " + varname + " has the incorrect size");
+ }
+}
+
+void mexFunction( int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] )
+{
+ if (nrhs < 15) {
+ throw std::runtime_error("swloop: Requires 15 arguments");
+ }
+ err_flag.store(0, std::memory_order_release);
+ size_t nThreads;
+ int nT = getField(prhs[0], 0, "nThreads", -1);
+ if (nT < 0) {
+ // Gets number of cores / hyperthreads
+ nThreads = std::thread::hardware_concurrency() / 2;
+ if (nThreads == 0) {
+ nThreads = 1; // Defaults to single thread if not known
+ }
+ } else {
+ nThreads = static_cast(nT);
+ }
+ // Initialises the parameters structure
+ struct pars params;
+ if (!mxIsStruct(prhs[0])) {
+ throw std::runtime_error("swloop: Error first argument must be a param struct");
+ }
+ params.hermit = getField(prhs[0], 0, "hermit", false);
+ params.formfact = getField(prhs[0], 0, "formfact", false);
+ params.incomm = getField(prhs[0], 0, "incomm", false);
+ params.helical = getField(prhs[0], 0, "helical", false);
+ params.bq = getField(prhs[0], 0, "bq", false);
+ params.field = getField(prhs[0], 0, "field", false);
+ params.fastmode = getField(prhs[0], 0, "fastmode", false);
+ params.neutron_output = getField(prhs[0], 0, "neutron_output", false);
+ params.omega_tol = getField(prhs[0], 0, "omega_tol", 1.0e-5);
+ params.nCell = getField(prhs[0], 0, "nCell", 1.0);
+ params.nformula = getField(prhs[0], 0, "nformula", 1);
+ params.nTwin = getField(prhs[0], 0, "nTwin", 1);
+ params.nHkl = (size_t)mxGetDimensions(prhs[1])[1]; // hklExt
+ params.nBond = (size_t)mxGetDimensions(prhs[5])[1]; // dR
+ params.nMagExt = (size_t)mxGetDimensions(prhs[6])[1]; // RR
+ params.nBqBond = (size_t)mxGetDimensions(prhs[10])[1]; // bqdR
+ // Checks all inputs have the correct dimensions
+ size_t len_abcd = 3 * params.nBond;
+ size_t nHam = 2 * params.nMagExt;
+ checkDims("hklExt", prhs[1], 3, params.nHkl);
+ checkDims("ABCD", prhs[2], 1, len_abcd);
+ checkDims("idxAll", prhs[3], len_abcd, 2);
+ checkDims("ham_diag", prhs[4], nHam, 1);
+ checkDims("dR", prhs[5], 3, params.nBond);
+ checkDims("RR", prhs[6], 3, params.nMagExt);
+ checkDims("S0", prhs[7], 1, params.nMagExt);
+ checkDims("zed", prhs[8], 3, params.nMagExt);
+ if (params.formfact) {
+ checkDims("FF", prhs[9], params.nMagExt, params.nHkl);
+ }
+ if (params.bq) {
+ checkDims("bqdR", prhs[10], 3, params.nBqBond);
+ checkDims("bqABCD", prhs[11], 1, 3 * params.nBqBond);
+ checkDims("idxBq", prhs[12], 3 * params.nBqBond, 2);
+ checkDims("bq_ham_d", prhs[13], nHam, 1);
+ }
+ // Process other inputs
+ struct swinputs inputs;
+ if (!mxIsComplex(prhs[8])) {
+ throw std::runtime_error("swloop: zed (arg 9) must be complex"); }
+ for (size_t ii=1; ii < 15; ii++) {
+ if (ii != 2 && ii != 8 && mxIsComplex(prhs[ii])) {
+ throw std::runtime_error("swloop: Error an input was found to be complex when it "
+ "is expected to be real"); }
+ }
+ std::complex *ABCDmem;
+ if (mxIsComplex(prhs[2])) {
+ ABCDmem = reinterpret_cast*>(mxGetComplexDoubles(prhs[2]));
+ } else {
+ ABCDmem = new std::complex[len_abcd];
+ double *abcdtmp = mxGetDoubles(prhs[2]);
+ for (size_t ii=0; ii(abcdtmp[ii], 0.0); }
+ }
+ inputs.hklExt = mxGetDoubles(prhs[1]);
+ inputs.ABCD = ABCDmem;
+ inputs.idxAll = mxGetDoubles(prhs[3]);
+ inputs.ham_diag = mxGetDoubles(prhs[4]);
+ inputs.dR = mxGetDoubles(prhs[5]);
+ inputs.RR = mxGetDoubles(prhs[6]);
+ inputs.S0 = mxGetDoubles(prhs[7]);
+ inputs.zed = reinterpret_cast(mxGetComplexDoubles(prhs[8]));
+ inputs.FF = mxGetDoubles(prhs[9]);
+ inputs.bqdR = mxGetDoubles(prhs[10]);
+ inputs.bqABCD = mxGetDoubles(prhs[11]);
+ inputs.idxBq = mxGetDoubles(prhs[12]);
+ inputs.bq_ham_diag = mxGetDoubles(prhs[13]);
+ if (params.field) {
+ for (size_t ii=0; ii(t0 + nHkl0 + kk);
+ }
+ }
+ }
+
+ } else {
+ for (size_t ii=0; ii(ii); }
+ }
+ inputs.idx0 = idx0;
+
+ // Creates outputs
+ size_t nHam_sel = params.fastmode ? nHam / 2.0 : nHam;
+ size_t nn = nHam_sel, nHkl0 = params.nHkl;
+ if (params.incomm) {
+ nn *= 3;
+ nHkl0 = params.nHkl / 3;
+ }
+ const mwSize dSab[] = {3, 3, nn, nHkl0};
+ plhs[0] = mxCreateDoubleMatrix(nn, nHkl0, mxCOMPLEX);
+ size_t Sab_sz;
+ if (params.neutron_output) {
+ plhs[1] = mxCreateDoubleMatrix(nn, nHkl0, mxCOMPLEX);
+ Sab_sz = 1;
+ } else {
+ plhs[1] = mxCreateNumericArray(4, dSab, mxDOUBLE_CLASS, mxCOMPLEX);
+ Sab_sz = 9;
+ }
+ plhs[2] = mxCreateDoubleMatrix(1, 1, mxREAL);
+ plhs[3] = mxCreateDoubleMatrix(1, 1, mxREAL);
+ double *warn1 = mxGetDoubles(plhs[2]); *warn1 = 0.;
+ double *orthwarn = mxGetDoubles(plhs[3]); *orthwarn = 0.;
+ struct swoutputs outputs;
+
+ if (params.nHkl < 10 * nThreads || nThreads == 1) {
+ // Too few nHkl to run in parallel
+ if (params.incomm) {
+ outputs.omega = new std::complex[nHam_sel * params.nHkl];
+ outputs.Sab = new std::complex[Sab_sz * nHam_sel * params.nHkl];
+ } else {
+ outputs.omega = reinterpret_cast(mxGetComplexDoubles(plhs[0]));
+ outputs.Sab = reinterpret_cast(mxGetComplexDoubles(plhs[1]));
+ }
+ swcalc_fun(0, params.nHkl, std::ref(params), std::ref(inputs), std::ref(outputs));
+ int errcode = err_flag.load(std::memory_order_relaxed);
+ if (errcode > 0) {
+ delete[]idx0;
+ if (!mxIsComplex(prhs[2])) {
+ delete[]ABCDmem; }
+ if (params.incomm) {
+ delete [](outputs.omega);
+ delete [](outputs.Sab);
+ }
+ mexErrMsgIdAndTxt("swloop:error", errmsgs[errcode].c_str());
+ }
+ if (outputs.warn_posdef) *warn1 = 1.;
+ if (params.incomm) {
+ // Re-arranges incommensurate modes into [Q-km, Q, Q+km] modes
+ std::complex *dest_om = reinterpret_cast(mxGetComplexDoubles(plhs[0]));
+ std::complex *dest_Sab = reinterpret_cast(mxGetComplexDoubles(plhs[1]));
+ size_t nHams = nHam_sel * Sab_sz;
+ size_t blkSz = nHam_sel * sizeof(std::complex), blkSzs = blkSz * Sab_sz;
+ size_t nHklI = params.nHkl / params.nTwin / 3;
+ size_t nHklI1 = nHklI * nHam_sel, nHklS1 = nHklI1 * Sab_sz;
+ size_t nHklI2 = 2 * nHklI * nHam_sel, nHklS2 = nHklI2 * Sab_sz;
+ for (size_t ii=0; ii outputs_v(nThreads);
+ std::vector threads;
+ size_t nBlock = params.nHkl / nThreads;
+ size_t i0 = 0, i1 = nBlock;
+ for (size_t ii=0; ii[nHam_sel * (i1 - i0)];
+ outputs_v[ii].Sab = new std::complex[Sab_sz * nHam_sel * (i1 - i0)];
+ outputs_v[ii].warn_posdef = false;
+ outputs_v[ii].warn_orth = false;
+ threads.push_back(
+ std::thread(swcalc_fun, i0, i1, std::ref(params), std::ref(inputs), std::ref(outputs_v[ii]))
+ );
+ i0 = i1;
+ i1 += nBlock;
+ if (i1 > params.nHkl || ii == (nThreads - 2)) i1 = params.nHkl;
+ }
+ i0 = 0; i1 = nBlock;
+ std::complex *omega_ptr = reinterpret_cast(mxGetComplexDoubles(plhs[0]));
+ std::complex *Sab_ptr = reinterpret_cast(mxGetComplexDoubles(plhs[1]));
+
+ size_t nHams, blkSz, blkSzs, nHklI;
+ if (params.incomm) {
+ nHams = nHam_sel * Sab_sz;
+ blkSz = nHam_sel * sizeof(std::complex);
+ blkSzs = blkSz * Sab_sz;
+ nHklI = params.nHkl / params.nTwin / 3;
+ }
+ for (size_t ii=0; ii 0) {
+ delete[]idx0;
+ if (!mxIsComplex(prhs[2])) {
+ delete[]ABCDmem; }
+ for (size_t ii=0; ii));
+ memcpy(Sab_ptr, outputs_v[ii].Sab, Sab_sz * msz * sizeof(std::complex));
+ omega_ptr += msz;
+ Sab_ptr += Sab_sz * msz;
+ }
+ i0 = i1;
+ i1 += nBlock;
+ if (i1 > params.nHkl || ii == (nThreads - 2)) i1 = params.nHkl;
+ if (outputs_v[ii].warn_posdef) *warn1 = 1.;
+ delete [](outputs_v[ii].omega);
+ delete [](outputs_v[ii].Sab);
+ }
+ }
+
+ // Clean up
+ delete[]idx0;
+ if (!mxIsComplex(prhs[2])) {
+ delete[]ABCDmem; }
+}
diff --git a/external/swloop/swloop.m b/external/swloop/swloop.m
new file mode 100644
index 000000000..552f75f89
--- /dev/null
+++ b/external/swloop/swloop.m
@@ -0,0 +1,176 @@
+% Calculates the inner loop of spinw/spinwave.m in parallel
+%
+% ### Syntax
+%
+% [omega, Sab, warn1, orthWarn0] = swloop(param, hklExt, ...
+% ABCD, idxAll, ham_diag, dR, RR, S0, zed, FF, ...
+% bqdR, bqABCD, idxBq, bq_ham_d, ham_MF)
+%
+% This is a MEX-file for MATLAB.
+%
+% ### Description
+%
+% This code uses the Eigen matrix library for linear algebra
+% C++ threads to calculate the inner loop of spinwave.m
+% As this is different to the LAPACK/BLAS used by the Matlab
+% code there will be numerical differences in the output
+% You should double check that the Matlab and Mex code gives
+% consistent results before running a long calculation.
+%
+% ### Input Arguments
+%
+% `params`
+% : a struct with parameters:
+% hermit (bool) - use Hermitian algorithm or not
+% omega_tol (double) - max val to add to Hamiltonian to make +def
+% formfact (bool) - whether to calculate formfactor
+% incomm, helical (bool) - whether system is incommensurate/helical
+% nTwin (int) - number of twins
+% bq (bool) - whether there are any biquadratic interactions
+% field (bool) - whether a magnetic field is applied
+% nThreads (int) - number of threads to use
+% n (3-vector) - normal vector defining the rotation frame
+% rotc (3x3xN matrix) - twin rotation matrices
+%
+% `hklExt` : 3 x nQ array of Q-point in the extended unit cell
+% `ABCD` : 3*nBond flattened array of the non-q-dependent part of
+% the Hamiltonian [A B; B' A'] in eq 25 of Toth & Lake
+% `idxAll` : indices into `ABCD` to compute the Q-dependent part
+% of the Hamiltonian using accumarray
+% `ham_diag` : diagonal part of the Hamiltonian (C in eq 26)
+% `dR` : 3*nBond array of bond vectors
+% `RR` : 3*nAtom array of magnetic ion coordinates
+% `S0` : nAtom vector of magnetic ion spin lengths
+% `zed` : 3*nAtom array of the (complex) moment direction vector
+% (denoted u^alpha in eq 9 of Toth & Lake)
+% `FF` : nQ x nAtom array of magnetic form factor values
+% (not referenced if params.formfact = false)
+% `bqdR`, `bqABCD`, `idxBq`, `bq_ham_d` : equivalent inputs
+% to `dR`, `ABCD`, `idxAll` and `ham_diag` for biquadratic
+% interactions (not referenced if params.bq = false)
+% `ham_MF` : the full Zeeman Hamiltonian
+% (not referenced if params.field = false)
+%
+% Original Author: M. D. Le [duc.le@stfc.ac.uk]
+
+% Equivalent Matlab code to C++ code follows:
+%
+% function [omega, Sab, warn1, orthWarn0] = swavel3(param, hklExt, ...
+% ABCD, idxAll, ham_diag, dR, RR, S0, zed, FF, bqdR, bqABCD, idxBq, bq_ham_d, ham_MF)
+%
+% nHkl = size(hklExt,2);
+% nMagExt = param.nMagExt;
+%
+% % Empty omega dispersion of all spin wave modes, size: 2*nMagExt x nHkl.
+% omega = zeros(2*nMagExt, nHkl);
+%
+% % empty Sab
+% Sab = zeros(3,3,2*nMagExt,nHkl);
+%
+% orthWarn0 = false;
+% warn1 = false;
+%
+% % Could replace the code below with a generator if Matlab has one...
+% idx0 = 1:nHkl;
+% nHklT = nHkl / param.nTwin;
+% if param.incomm
+% nHkl0 = nHkl / 3 / param.nTwin;
+% for tt = 1:param.nTwin
+% t0 = (tt-1)*nHklT + 1;
+% idx0(t0:(t0+nHklT-1)) = repmat((t0+nHkl0):(t0+2*nHkl0-1), [1 3]);
+% end
+% end
+%
+% % diagonal of the boson commutator matrix
+% gCommd = [ones(nMagExt,1); -ones(nMagExt,1)];
+% % boson commutator matrix
+% gComm = diag(gCommd);
+%
+% sqrtS0 = sqrt(S0 / 2);
+%
+% for jj = 1:nHkl
+% ExpF = exp(1i*(dR' * hklExt(:,jj)))';
+% ham = accumarray(idxAll, ABCD.*repmat(ExpF, [1 3]), [1 1]*2*nMagExt)' + diag(ham_diag);
+%
+% if param.bq
+% ExpF = exp(1i*(bqdR' * hklExt(:,jj)))';
+% h_bq = accumarray(idxBq, bqABCD.*repmat(ExpF, [1 3]), [1 1]*2*nMagExt)' + diag(bq_ham_d);
+% ham = ham + h_bq;
+% end
+% if param.field
+% ham = ham + ham_MF{ceil(jj / nHklT)};
+% end
+%
+% ham = (ham + ham') / 2;
+%
+% if param.hermit
+% [K, posDef] = chol(ham);
+% if posDef > 0
+% try
+% % get tolerance from smallest negative eigenvalue
+% tol0 = eig(ham);
+% tol0 = sort(real(tol0));
+% tol0 = abs(tol0(1));
+% % TODO determine the right tolerance value
+% tol0 = tol0*sqrt(nMagExt*2)*4;
+% if tol0>param.omega_tol
+% error('spinw:spinwave:NonPosDefHamiltonian','Very baaaad!');
+% end
+% try
+% K = chol(ham+eye(2*nMagExt)*tol0);
+% catch
+% K = chol(ham+eye(2*nMagExt)*param.omega_tol);
+% end
+% warn1 = true;
+% catch PD
+% error('spinw:spinwave:NonPosDefHamiltonian',...
+% ['Hamiltonian matrix is not positive definite, probably'...
+% ' the magnetic structure is wrong! For approximate'...
+% ' diagonalization try the param.hermit=false option']);
+% end
+% end
+%
+% K2 = K*gComm*K';
+% K2 = 1/2*(K2+K2');
+% % Hermitian K2 will give orthogonal eigenvectors
+% [U, D] = eig(K2);
+% D = diag(D);
+%
+% % sort modes accordign to the real part of the energy
+% [~, idx] = sort(real(D),'descend');
+% U = U(:,idx);
+% % omega dispersion
+% omega(:,jj) = D(idx);
+%
+% % the inverse of the para-unitary transformation V
+% V = inv(K)*U*diag(sqrt(gCommd.*omega(:,jj))); %#ok
+% else
+% gham = gComm * ham;
+% gham = gham + diag([(-nMagExt+0.5):nMagExt].*1e-12);
+% [V, D, orthWarn] = eigorth(gham,param.omega_tol);
+% orthWarn0 = orthWarn || orthWarn0;
+% % multiplication with g removed to get negative and positive energies as well
+% omega(:,jj) = D;
+% M = diag(gComm * V' * gComm * V);
+% V = V * diag(sqrt(1./M));
+% end
+%
+% % TODO saveV / saveH
+%
+% % Calculates correlation functions.
+% ExpF = exp(-1i * sum(repmat(hklExt(:,idx0(jj)),[1 nMagExt 1]) .* RR)) .* sqrtS0;
+% if param.formfact
+% ExpF = ExpF .* FF(:,jj)';
+% end
+% zedExpF = zeros(2*nMagExt, 3);
+% for i1 = 1:3
+% zedExpF(:, i1) = transpose([zed(i1,:) .* ExpF, conj(zed(i1,:)) .* ExpF]);
+% end
+% VExp = zeros(3, 2*nMagExt, 1);
+% for i1 = 1:3
+% VExp(i1,:,:) = V' * zedExpF(:, i1);
+% end
+% for i1 = 1:(2*nMagExt)
+% Sab(:,:,i1,jj) = VExp(:,i1) * VExp(:,i1)';
+% end
+% end
diff --git a/external/uiopen.m b/external/uiopen.m
index 325900402..d6c8e90e5 100644
--- a/external/uiopen.m
+++ b/external/uiopen.m
@@ -57,6 +57,15 @@ function uiopen(type,direct)
%
% See also UIGETFILE, UIPUTFILE, OPEN, UIIMPORT.
+persistent uiroot
+if isempty(uiroot)
+ uiroot = [matlabroot filesep 'toolbox' filesep 'matlab' filesep 'uitools'];
+ vervec = cellfun(@(x) sscanf(x, '%d'), split(version, '.'));
+ if (vervec(1) * 10 + vervec(2)) >= 242
+ uiroot = [uiroot filesep 'uitools'];
+ end
+end
+
if numel(type)>3 && strcmpi(type(end+(-3:0)),'.cif') && direct
model = spinw(type);
if ~isempty(model.matom.S)
@@ -81,7 +90,7 @@ function uiopen(type,direct)
%fprintf('The imported image is stored in the ''img'' variable.\n');
else
pwd0 = pwd;
- cd([matlabroot filesep 'toolbox' filesep 'matlab' filesep 'uitools'])
+ cd(uiroot)
feval('uiopen',type,direct);
cd(pwd0)
end
\ No newline at end of file
diff --git a/install_spinw.m b/install_spinw.m
index 235437b53..2f8d79406 100644
--- a/install_spinw.m
+++ b/install_spinw.m
@@ -1,4 +1,4 @@
-function install_spinw()
+function install_spinw(varargin)
% installs SpinW
%
% INSTALL_SPINW()
@@ -13,6 +13,15 @@ function install_spinw()
% return
% end
+silent = false;
+if nargin == 2
+ if strcmpi(varargin{1},'silent')
+ if isa(varargin{2},'logical')
+ silent = varargin{2};
+ end
+ end
+end
+
newline = char(10); %#ok
% remove old SpinW installation from path
@@ -91,8 +100,12 @@ function install_spinw()
% create new startup.m file
if isempty(sfLoc)
+ if ~silent
answer = getinput(sprintf(['You don''t have a Matlab startup.m file,\n'...
'do you want it to be created at %s? (y/n)'],esc(uPath)),'yn');
+ else
+ answer = 'n';
+ end
if answer == 'y'
fclose(fopen(uPath,'w'));
sfLoc = uPath;
@@ -100,11 +113,13 @@ function install_spinw()
end
if ~isempty(sfLoc)
-
+ if ~silent
answer = getinput(['Would you like to add the following line:' newline...
'addpath(genpath(''' esc(folName) '''));' newline 'to the end of '...
'your Matlab startup file (' esc(sfLoc) ')? (y/n)'],'yn');
-
+ else
+ answer = 'n';
+ end
if answer == 'y'
fid = fopen(sfLoc,'a');
fprintf(fid,['\n%%###SW_UPDATE\n%% Path to the SpinW installation\n'...
@@ -113,6 +128,7 @@ function install_spinw()
end
end
+if ~silent
answer = getinput(...
['\nIn order to refresh the internal class definitions of Matlab (to\n'...
'access the new SpinW version), issuing the "clear classes" command\n'...
@@ -120,7 +136,9 @@ function install_spinw()
'in the Matlab internal memory. Would you like the updater to issue\n'...
'the command now, otherwise you can do it manually later.\n'...
'Do you want to issue the command "clear classes" now? (y/n)'],'yn');
-
+else
+ answer = 'n';
+end
if answer == 'y'
clear('classes'); %#ok
disp('Matlab class memory is refreshed!')
diff --git a/mltbx/create_mltbx.m b/mltbx/create_mltbx.m
new file mode 100644
index 000000000..b0632e664
--- /dev/null
+++ b/mltbx/create_mltbx.m
@@ -0,0 +1,13 @@
+currdir = fileparts(mfilename('fullpath'));
+mltbx_dir = fullfile(currdir, 'mltbx');
+if exist(mltbx_dir)
+ rmdir(mltbx_dir, 's');
+end
+mkdir(fullfile(currdir, 'mltbx'));
+copyfile(fullfile(currdir, '..', 'CITATION.cff'), fullfile(currdir, 'mltbx'));
+copyfile(fullfile(currdir, '..', 'license.txt'), fullfile(currdir, 'mltbx'));
+copyfile(fullfile(currdir, '..', 'swfiles'), fullfile(currdir, 'mltbx', 'swfiles'));
+copyfile(fullfile(currdir, '..', 'external'), fullfile(currdir, 'mltbx', 'external'));
+copyfile(fullfile(currdir, '..', 'dat_files'), fullfile(currdir, 'mltbx', 'dat_files'));
+copyfile(fullfile(currdir, 'spinw_on.m'), fullfile(currdir, 'mltbx'));
+matlab.addons.toolbox.packageToolbox("spinw.prj", "spinw.mltbx");
diff --git a/mltbx/spinw.prj b/mltbx/spinw.prj
new file mode 100644
index 000000000..918c6f5b9
--- /dev/null
+++ b/mltbx/spinw.prj
@@ -0,0 +1,111 @@
+
+
+ SpinW
+ Duc Le
+ duc.le@stfc.ac.uk
+
+ A library for spin wave calculation
+ A library for spin wave calculation
+
+ 4.0
+ ${PROJECT_ROOT}/spinW.mltbx
+
+
+
+
+ 859ef126-ff2e-4f45-8bff-60126ff3ff61
+
+ true
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ false
+ true
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${PROJECT_ROOT}/mltbx
+
+
+
+
+
+
+ spinW.mltbx
+
+
+
+ D:/MATLAB/R2024a
+
+
+
+ false
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ 10.0
+ false
+ true
+ win64
+ true
+
+
+
diff --git a/mltbx/spinw_on.m b/mltbx/spinw_on.m
new file mode 100644
index 000000000..3b2fe4778
--- /dev/null
+++ b/mltbx/spinw_on.m
@@ -0,0 +1,6 @@
+if exist('spinw', 'class') ~= 8
+ currdir = fileparts(mfilename('fullpath'));
+ addpath(genpath(fullfile(currdir, 'swfiles')));
+ addpath(genpath(fullfile(currdir, 'external')));
+ addpath(genpath(fullfile(currdir, 'dat_files')));
+end
diff --git a/python/README.md b/python/README.md
new file mode 100644
index 000000000..821f19952
--- /dev/null
+++ b/python/README.md
@@ -0,0 +1,44 @@
+# pySpinW
+
+This is an intial release of pySpinW as a `pip` installable wheel for python >= 3.8 and MATLAB >= R2021a
+
+## Installation
+
+Please install with
+
+```bash
+pip install pyspinw*.whl
+```
+
+This package can now be used in python if you have a version of MATLAB or MCR available on the machine.
+The package will try to automatically detect your installation, however if it is in a non-standard location, the path and version will have to be specified.
+
+```python
+m = Matlab(matlab_version='R2023a', matlab_path='/usr/local/MATLAB/R2023a/')
+```
+
+## Example
+
+An example would be:
+
+```python
+import numpy as np
+from pyspinw import Matlab
+
+m = Matlab()
+
+# Create a spinw model, in this case a triangular antiferromagnet
+s = m.sw_model('triAF', 1)
+
+# Specify the start and end points of the q grid and the number of points
+q_start = [0, 0, 0]
+q_end = [1, 1, 0]
+pts = 501
+
+# Calculate the spin wave spectrum
+spec = m.spinwave(s, [q_start, q_end, pts])
+```
+
+## Known limitations
+
+At the moment graphics will not work on macOS systems and is disabled.
diff --git a/python/build_ctf.m b/python/build_ctf.m
new file mode 100644
index 000000000..dfc63f10b
--- /dev/null
+++ b/python/build_ctf.m
@@ -0,0 +1,11 @@
+out_dir = 'ctf';
+VERSION = version('-release');
+package_name = ['SpinW_', VERSION];
+full_package = ['SpinW_', VERSION, '.ctf'];
+
+mcc('-U', '-W', ['CTF:' package_name], ...
+ '-d', out_dir, ...
+ 'matlab/call.m', ...
+ '-a', '../swfiles', ...
+ '-a', '../external', ...
+ '-a', '../dat_files')
diff --git a/python/matlab/call.m b/python/matlab/call.m
new file mode 100644
index 000000000..8646c99bd
--- /dev/null
+++ b/python/matlab/call.m
@@ -0,0 +1,164 @@
+function [varargout] = call(name, varargin)
+ if strcmp(name, '_call_python')
+ varargout = call_python_m(varargin{:});
+ return
+ end
+ resultsize = nargout;
+ try
+ maxresultsize = nargout(name);
+ if maxresultsize == -1
+ maxresultsize = resultsize;
+ end
+ catch
+ maxresultsize = resultsize;
+ end
+ if resultsize > maxresultsize
+ resultsize = maxresultsize;
+ end
+ if nargin == 1
+ args = {};
+ else
+ args = varargin;
+ end
+ for ir = 1:numel(args)
+ args{ir} = unwrap(args{ir});
+ end
+ if resultsize > 0
+ % call the function with the given number of
+ % output arguments:
+ varargout = cell(resultsize, 1);
+ try
+ [varargout{:}] = feval(name, args{:});
+ catch err
+ if (strcmp(err.identifier,'MATLAB:unassignedOutputs'))
+ varargout = eval_ans(name, args);
+ else
+ rethrow(err);
+ end
+ end
+ else
+ varargout = eval_ans(name, args);
+ end
+ for ir = 1:numel(varargout)
+ varargout{ir} = wrap(varargout{ir});
+ end
+end
+
+function out = unwrap(in_obj)
+ out = in_obj;
+ if isstruct(in_obj) && isfield(in_obj, 'func_ptr') && isfield(in_obj, 'converter')
+ out = @(varargin) call('_call_python', [in_obj.func_ptr, in_obj.converter], varargin{:});
+ elseif isa(in_obj, 'containers.Map') && in_obj.isKey('wrapped_oldstyle_class')
+ out = in_obj('wrapped_oldstyle_class');
+ elseif iscell(in_obj)
+ for ii = 1:numel(in_obj)
+ out{ii} = unwrap(in_obj{ii});
+ end
+ end
+end
+
+function out = wrap(obj)
+ out = obj;
+ if isobject(obj) && (isempty(metaclass(obj)) && ~isjava(obj)) || has_thin_members(obj)
+ out = containers.Map({'wrapped_oldstyle_class'}, {obj});
+ elseif iscell(obj)
+ for ii = 1:numel(obj)
+ out{ii} = wrap(obj{ii});
+ end
+ end
+end
+
+function out = has_thin_members(obj)
+% Checks whether any member of a class or struct is an old-style class
+% or is already a wrapped instance of such a class
+ out = false;
+ if isobject(obj) || isstruct(obj)
+ try
+ fn = fieldnames(obj);
+ catch
+ return;
+ end
+ for ifn = 1:numel(fn)
+ try
+ mem = subsref(obj, struct('type', '.', 'subs', fn{ifn}));
+ catch
+ continue;
+ end
+ if (isempty(metaclass(mem)) && ~isjava(mem))
+ out = true;
+ break;
+ end
+ end
+ end
+end
+
+function results = eval_ans(name, args)
+ % try to get output from ans:
+ clear('ans');
+ feval(name, args{:});
+ try
+ results = {ans};
+ catch err
+ results = {[]};
+ end
+end
+
+function [n, undetermined] = getArgOut(name, parent)
+ undertermined = false;
+ if isstring(name)
+ fun = str2func(name);
+ try
+ n = nargout(fun);
+ catch % nargout fails if fun is a method:
+ try
+ n = nargout(name);
+ catch
+ n = 1;
+ undetermined = true;
+ end
+ end
+ else
+ n = 1;
+ undetermined = true;
+ end
+end
+
+function out = call_python_m(varargin)
+ % Convert row vectors to column vectors for better conversion to numpy
+ for ii = 1:numel(varargin)
+ if size(varargin{ii}, 1) == 1
+ varargin{ii} = varargin{ii}';
+ end
+ end
+ fun_name = varargin{1};
+ [kw_args, remaining_args] = get_kw_args(varargin(2:end));
+ if ~isempty(kw_args)
+ remaining_args = [remaining_args {struct('pyHorace_pyKwArgs', 1, kw_args{:})}];
+ end
+ out = call_python(fun_name, remaining_args{:});
+ if ~iscell(out)
+ out = {out};
+ end
+end
+
+function [kw_args, remaining_args] = get_kw_args(args)
+ % Finds the keyword arguments (string, val) pairs, assuming that they always at the end (last 2n items)
+ first_kwarg_id = numel(args) + 1;
+ for ii = (numel(args)-1):-2:1
+ if ischar(args{ii}); args{ii} = string(args{ii}(:)'); end
+ if isstring(args{ii}) && ...
+ strcmp(regexp(args{ii}, '^[A-Za-z_][A-Za-z0-9_]*', 'match'), args{ii})
+ % Python identifiers must start with a letter or _ and can contain charaters, numbers or _
+ first_kwarg_id = ii;
+ else
+ break;
+ end
+ end
+ if first_kwarg_id < numel(args)
+ kw_args = args(first_kwarg_id:end);
+ remaining_args = args(1:(first_kwarg_id-1));
+ else
+ kw_args = {};
+ remaining_args = args;
+ end
+end
diff --git a/python/mcc_all.py b/python/mcc_all.py
new file mode 100644
index 000000000..477941960
--- /dev/null
+++ b/python/mcc_all.py
@@ -0,0 +1,11 @@
+from libpymcr.utils import checkPath
+import os
+import subprocess
+
+for v in ['R2021a', 'R2021b', 'R2022a', 'R2022b', 'R2023a', 'R2023b', 'R2024a']:
+ print(f'Compiling for {v}')
+ mlPath = checkPath(v)
+ rv = subprocess.run([os.path.join(mlPath, 'bin', 'matlab'), '-batch', '"build_ctf; exit"'], capture_output=True)
+ if rv.returncode != 0:
+ print(rv.stdout.decode())
+ print(rv.stderr.decode())
diff --git a/python/pyproject.toml b/python/pyproject.toml
new file mode 100644
index 000000000..62afb8603
--- /dev/null
+++ b/python/pyproject.toml
@@ -0,0 +1,100 @@
+[build-system]
+requires = [
+ "poetry-core",
+ "poetry-dynamic-versioning",
+]
+build-backend = "poetry_dynamic_versioning.backend"
+
+[tool.black]
+line-length = 88
+target-version = ['py38']
+include = '\.pyi?$'
+extend-exclude = '''
+# A regex preceded with ^/ will apply only to files and directories
+# in the root of the project.
+^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults)
+'''
+
+[tool.coverage.run]
+source = ['pyspinw']
+
+[tool.github.info]
+organization = 'spinw'
+repo = 'spinw'
+
+[tool.poetry-dynamic-versioning]
+enable = true
+vcs = "git"
+style = "semver"
+
+[tool.poetry]
+name = "spinw"
+version = "0.0.0"
+description = "Python library for spin wave calculations"
+license = "BSD-3-Clause"
+authors = ["Sándor Tóth", "Duc Le", "Simon Ward", "Becky Fair", "Richard Waite"]
+readme = "README.md"
+homepage = "https://spinw.org"
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Topic :: Scientific/Engineering :: Physics",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python :: 3 :: Only",
+]
+packages = [ { include = "pyspinw" } ]
+include = [ { path = "pyspinw/ctfs/*" } ]
+
+[tool.poetry.dependencies]
+python = ">=3.8,<=3.12"
+libpymcr = ">=0.1.7"
+numpy = "^1.21.4"
+pyqt5 = "~5.15"
+vispy = ">=0.14.1"
+scipy = ">=1.0.0"
+
+# Optional dependencies
+pytest = {version = ">=7.0.0", optional = true}
+pytest-cov = {version = ">=3,<5", optional = true}
+codecov = {version = ">=2.1.11", optional = true}
+flake8 = {version = ">=5.0", optional = true}
+tox = {version = ">=3.0", optional = true}
+tox-gh-actions = {version = ">=2.11,<4.0", optional = true}
+
+[tool.poetry.extras]
+test = ['pytest', 'pytest-cov', 'codecov', 'flake8', 'tox', 'tox-gh-actions']
+
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+isolated_build = True
+envlist = py{38,39,310,311,312}
+[gh-actions]
+python =
+ 3.8: py38
+ 3.9: py39
+ 3.10: py310
+ 3.11: py311
+ 3.12: py312
+[gh-actions:env]
+PLATFORM =
+ ubuntu-latest: linux
+ macos-latest: macos
+ windows-latest: windows
+[testenv]
+passenv =
+ CI
+ GITHUB_ACTIONS
+ GITHUB_ACTION
+ GITHUB_REF
+ GITHUB_REPOSITORY
+ GITHUB_HEAD_REF
+ GITHUB_RUN_ID
+ GITHUB_SHA
+ COVERAGE_FILE
+deps = coverage
+whitelist_externals = poetry
+commands =
+ pip install '.[test]'
+ pytest --cov --cov-report=xml
+"""
diff --git a/python/pyspinw/__init__.py b/python/pyspinw/__init__.py
new file mode 100644
index 000000000..7d98e5932
--- /dev/null
+++ b/python/pyspinw/__init__.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+__author__ = "github.com/wardsimon"
+__version__ = "0.0.0"
+
+import os
+import libpymcr
+from . import plotting
+
+
+# Generate a list of all the MATLAB versions available
+_VERSION_DIR = os.path.join(os.path.dirname(__file__), 'ctfs')
+_VERSIONS = []
+for file in os.scandir(_VERSION_DIR):
+ if file.is_file() and file.name.endswith('.ctf'):
+ _VERSIONS.append({'file': os.path.join(_VERSION_DIR, file.name),
+ 'version': 'R' + file.name.split('.')[0].split('SpinW_')[1]
+ })
+
+VERSION = ''
+INITIALIZED = False
+
+
+class Matlab(libpymcr.Matlab):
+ def __init__(self, matlab_path: Optional[str] = None, matlab_version: Optional[str] = None):
+ """
+ Create a MATLAB instance with the correct compiled library for the MATLAB version specified. If no version is
+ specified, the first version found will be used. If no MATLAB versions are found, a RuntimeError will be
+ raised. If a version is specified, but not found, a RuntimeError will be raised.
+
+ :param matlab_path: Path to the root directory of the MATLAB installation or MCR installation.
+ :param matlab_version: Used to specify the version of MATLAB if the matlab_path is given or if there is more
+ than 1 MATLAB installation.
+ """
+
+ global INITIALIZED
+ global VERSION
+ if INITIALIZED:
+ super().__init__(VERSION, mlPath=matlab_path)
+ elif matlab_version is None:
+ for version in _VERSIONS:
+ if INITIALIZED:
+ break
+ try:
+ print(f"Trying MATLAB version: {version['version']} ({version['file']}))")
+ super().__init__(version['file'], mlPath=matlab_path)
+ INITIALIZED = True
+ VERSION = version['version']
+ except RuntimeError:
+ continue
+ else:
+ ctf = [version for version in _VERSIONS if version['version'].lower() == matlab_version.lower()]
+ if len(ctf) == 0:
+ raise RuntimeError(
+ f"Compiled library for MATLAB version {matlab_version} not found. Please use: [{', '.join([version['version'] for version in _VERSIONS])}]\n ")
+ else:
+ ctf = ctf[0]
+ try:
+ super().__init__(ctf['file'], mlPath=matlab_path)
+ INITIALIZED = True
+ VERSION = ctf['version']
+ except RuntimeError:
+ pass
+ if not INITIALIZED:
+ raise RuntimeError(
+ f"No supported MATLAB versions [{', '.join([version['version'] for version in _VERSIONS])}] found.\n "
+ f"If installed, please specify the root directory (`matlab_path` and `matlab_version`) of the MATLAB "
+ f"installation.")
+
+from .matlab_wrapped import *
+from .matlab_plotting import *
diff --git a/python/pyspinw/matlab_plotting.py b/python/pyspinw/matlab_plotting.py
new file mode 100644
index 000000000..2e71a9d27
--- /dev/null
+++ b/python/pyspinw/matlab_plotting.py
@@ -0,0 +1,97 @@
+"""
+A set of wrappers for comman Matlab plotting commands so you don't have to use the m. prefix
+"""
+
+import pyspinw
+m = pyspinw.Matlab()
+
+def plot(*args, **kwargs):
+ """
+ Wrapper around Matlab plot() function
+ """
+ return m.plot(*args, **kwargs)
+
+def subplot(*args, **kwargs):
+ """
+ Wrapper around Matlab subplot() function
+ """
+ return m.subplot(*args, **kwargs)
+
+def xlim(*args, **kwargs):
+ """
+ Wrapper around Matlab xlim() function
+ """
+ if args and isinstance(args[0], str):
+ return m.xlim(*args, **kwargs)
+ else:
+ m.xlim(*args, **kwargs)
+
+def ylim(*args, **kwargs):
+ """
+ Wrapper around Matlab ylim() function
+ """
+ if args and isinstance(args[0], str):
+ return m.ylim(*args, **kwargs)
+ else:
+ m.ylim(*args, **kwargs)
+
+def xlabel(*args, **kwargs):
+ """
+ Wrapper around Matlab xlabel() function
+ """
+ return m.xlabel(*args, **kwargs)
+
+def ylabel(*args, **kwargs):
+ """
+ Wrapper around Matlab ylabel() function
+ """
+ return m.ylabel(*args, **kwargs)
+
+def set(*args, **kwargs):
+ """
+ Wrapper around Matlab set() function
+ """
+ m.set(*args, **kwargs)
+
+def get(*args, **kwargs):
+ """
+ Wrapper around Matlab get() function
+ """
+ return m.get(*args, **kwargs)
+
+def gca(*args, **kwargs):
+ """
+ Wrapper around Matlab gca() function
+ """
+ return m.gca(*args, **kwargs)
+
+def gcf(*args, **kwargs):
+ """
+ Wrapper around Matlab gcf() function
+ """
+ return m.gcf(*args, **kwargs)
+
+def legend(*args, **kwargs):
+ """
+ Wrapper around Matlab legend() function
+ """
+ return m.legend(*args, **kwargs)
+
+def hold(*args, **kwargs):
+ """
+ Wrapper around Matlab hold() function
+ """
+ m.hold(*args, **kwargs)
+
+def pcolor(*args, **kwargs):
+ """
+ Wrapper around Matlab pcolor() function
+ """
+ return m.pcolor(*args, **kwargs)
+
+def contour(*args, **kwargs):
+ """
+ Wrapper around Matlab contour() function
+ """
+ return m.contour(*args, **kwargs)
+
diff --git a/python/pyspinw/plotting.py b/python/pyspinw/plotting.py
new file mode 100644
index 000000000..830f197c2
--- /dev/null
+++ b/python/pyspinw/plotting.py
@@ -0,0 +1,521 @@
+import numpy as np
+from vispy import scene
+from vispy.color import color_array
+from itertools import chain
+from vispy.visuals.filters import ShadingFilter, WireframeFilter
+from vispy.geometry import create_sphere
+import copy
+from scipy.spatial.transform import Rotation
+from scipy.spatial import ConvexHull
+from dataclasses import dataclass
+import warnings
+
+@dataclass
+class PolyhedronMesh:
+ vertices: np.ndarray
+ faces: np.ndarray
+
+class PolyhedraArgs:
+ def __init__(self, atom1_idx, atom2_idx, color, n_nearest=6):
+ self.atom1_idx = np.array(atom1_idx).reshape(-1) # makes an array even if single number passed
+ self.atom2_idx = np.array(atom2_idx).reshape(-1) # makes an array even if single number passed
+ self.n_nearest=n_nearest
+ self.color = color
+
+class SuperCell:
+ def __init__(self, matlab_caller, swobj, extent=(1,1,1), plot_mag=True, plot_bonds=False, plot_atoms=True,
+ plot_labels=True, plot_cell=True, plot_axes=True, plot_plane=True, ion_type=None, polyhedra_args=None):
+ """
+ :param swobj: spinw object to plot
+ :param extent: Tuple of supercell dimensions default is (1,1,1) - a single unit cell
+ :param plot_mag: If True the magneitc moments (in rotating frame representation) will be
+ plotted if a magnetic structure has been set on swobj
+ :param plot_bonds: If True the bonds in swobj.coupling will be plotted
+ :param plot_atoms: If True the atoms will be plotted
+ :param plot_labels: If True atom labels will be plotted on atom markers
+ :param plot_cell: If True the unit cell boundaries will be plotted
+ :param plot_axes: If True the arrows for the unit cell vectors will be plotted near the origin
+ :param plot_plane: If True the rotation plane will be plotted
+ :param ion_type: If not None ion_type can be one of 'aniso' or 'g' and the corresponding
+ single-ion ellipsoid will be plotted
+ :param polyhedra_args: If not None then instance of PolyhedraArgs that stores atom indices and
+ nearest neighbours. These will be used to plot polyhedra.
+ """
+ # init with sw obj - could get NExt from object if not explicitly provide (i.e. make default None)
+ self.do_plot_mag=plot_mag
+ self.do_plot_bonds=plot_bonds
+ self.do_plot_atoms=plot_atoms
+ self.do_plot_labels=plot_labels
+ self.do_plot_cell=plot_cell
+ self.do_plot_axes=plot_axes
+ self.do_plot_plane=plot_plane
+ self.do_plot_ion = ion_type is not None
+ self.ion_type = ion_type # "aniso" or "g"
+ self.polyhedra_args = polyhedra_args
+ # magnetic structure
+ self.mj = None
+ self.n = None
+
+ # get properties from swobj
+ self.unit_cell = UnitCell()
+ # add atoms
+ _, single_ion = swobj.intmatrix(plotmode= True, extend=False, sortDM=False, zeroC=False, nExt=[1, 1, 1])
+ aniso_mats = single_ion['aniso'].reshape(3,3,-1) # make 3D array even if only one atom
+ g_mats = single_ion['aniso'].reshape(3,3,-1) # make 3D array even if only one atom
+ imat = -1 # index of aniso and g matrices
+ atoms_mag = np.array(swobj.atom()['mag']).reshape(-1) # handles case when only 1 atom and swobj.atom()['mag'] is bool not list
+ for iatom, atom_idx in enumerate(swobj.atom()['idx'].flatten().astype(int)):
+ # get spin magnitude if magnetic
+ if atoms_mag[iatom]:
+ # get S
+ imatom = np.argmax(swobj.matom()['idx'].flatten().astype(int)== atom_idx)
+ # get single-ion matrices
+ imat += 1
+ g_mat = g_mats[:,:,imat]
+ aniso_mat = aniso_mats[:,:,imat]
+ else:
+ g_mat = None
+ aniso_mat = None
+ color = swobj.unit_cell['color'][:,atom_idx-1]/255
+ label = swobj.unit_cell['label'][atom_idx-1]
+ size = matlab_caller.sw_atomdata(label, 'radius')[0,0]
+ self.unit_cell.add_atom(Atom(atom_idx, swobj.atom()['r'][:,iatom], is_mag=atoms_mag[iatom], size=size, color=color, label=label,
+ gtensor_mat=g_mat, aniso_mat=aniso_mat))
+
+ # add bonds - only plot bonds for which there is a mat_idx
+ bond_idx = np.squeeze(swobj.coupling['idx'])
+ bond_matrices = swobj.matrix['mat'].reshape(3,3,-1)
+ for ibond in np.unique(bond_idx[np.any(swobj.coupling['mat_idx'], axis=0)]):
+ i_dl = np.squeeze(bond_idx==ibond)
+ for mat_idx in swobj.coupling['mat_idx'][:, np.argmax(i_dl)]:
+ if mat_idx > 0:
+ self.unit_cell.add_bond_vertices(name=f"bond{ibond}_mat{mat_idx}",
+ atom1_idx=np.squeeze(swobj.coupling['atom1'])[i_dl]-1,
+ atom2_idx=np.squeeze(swobj.coupling['atom2'])[i_dl]-1,
+ dl=swobj.coupling['dl'].T[i_dl],
+ mat=bond_matrices[:,:,mat_idx-1],
+ color=swobj.matrix['color'][:,mat_idx-1]/255)
+
+ # dimensions of supercell (pad by 1 for plotting)
+ self.extent = np.asarray(extent)
+ self.int_extent = np.ceil(self.extent).astype(int) + 1 # to plot additional unit cell along each dimension to get atoms on cell boundary
+ self.ncells = np.prod(self.int_extent)
+
+ # get magnetic structure for all spins in supercell
+ self.set_magnetic_structure(swobj)
+
+ # transforms
+ self.basis_vec = swobj.basisvector().T
+ self.inv_basis_vec = np.linalg.inv(self.basis_vec)
+
+ # scale factors
+ self.abc = np.sqrt(np.sum(self.basis_vec**2, axis=1))
+ self.cell_scale_abc_to_xyz = min(self.abc)
+ self.supercell_scale_abc_to_xyz = min(self.abc*self.extent)
+ # visual properties
+ self.bond_width = 5
+ self.spin_scale = 1
+ self.arrow_width = 8
+ self.arrow_head_size = 5
+ self.font_size = 20
+ self.axes_font_size = 50
+ self.atom_alpha = 0.75
+ self.mesh_alpha = 0.25
+ self.rotation_plane_radius = 0.3*self.cell_scale_abc_to_xyz
+ self.ion_radius = 0.3*self.cell_scale_abc_to_xyz
+ self.dm_arrow_scale = 0.2*self.cell_scale_abc_to_xyz
+
+ def transform_points_abc_to_xyz(self, points):
+ return points @ self.basis_vec
+
+ def transform_points_xyz_to_abc(self, points):
+ return points @ self.inv_basis_vec
+
+ def set_magnetic_structure(self, swobj):
+ magstr = swobj.magstr(NExt=[int(ext) for ext in self.int_extent])
+ if not np.any(magstr['S']):
+ warnings.warn('No magnetic structure defined')
+ self.do_plot_mag = False
+ self.do_plot_plane = False
+ return
+ self.mj = magstr['S'].T
+ self.n = np.asarray(magstr['n']) # plane of rotation of moment
+
+
+ def plot(self):
+ canvas = scene.SceneCanvas(bgcolor='white', show=True)
+ view = canvas.central_widget.add_view()
+ view.camera = scene.cameras.TurntableCamera()
+
+ pos, is_matom, colors, sizes, labels, iremove, iremove_mag = self.get_atomic_properties_in_supercell()
+ # delete spin vectors outside extent
+ if self.mj is not None:
+ mj = np.delete(self.mj, iremove_mag, axis=0)
+
+ if self.do_plot_cell:
+ self.plot_unit_cell_box(view.scene) # plot gridlines for unit cell boundaries
+ if self.do_plot_mag:
+ self.plot_magnetic_structure(view.scene, mj, pos[is_matom], colors[is_matom])
+ if self.do_plot_atoms:
+ self.plot_atoms(view.scene, pos, colors, sizes, labels)
+ if self.do_plot_bonds:
+ self.plot_bonds(view.scene)
+ if self.do_plot_axes:
+ self.plot_cartesian_axes(view.scene)
+ if self.do_plot_plane:
+ self.plot_rotation_plane(view.scene, pos[is_matom], colors[is_matom])
+ if self.do_plot_ion:
+ self.plot_ion_ellipsoids(view.scene)
+ if self.polyhedra_args is not None:
+ self.plot_polyhedra(view.scene)
+ view.camera.set_range() # centers camera on middle of data and auto-scales extent
+ canvas.app.run()
+ return canvas, view.scene
+
+ def plot_cartesian_axes(self, canvas_scene):
+ pos = np.array([[0., 0., 0.], [1., 0., 0.],
+ [0., 0., 0.], [0., 1., 0.],
+ [0., 0., 0.], [0., 0., 1.],
+ ])*0.5
+ pos = pos - 0.5*np.ones(3)
+ pos = self.transform_points_abc_to_xyz(pos)
+ arrows = np.c_[pos[0::2], pos[1::2]]
+
+ line_color = ['red', 'red', 'green', 'green', 'blue', 'blue']
+ arrow_color = ['red', 'green', 'blue']
+
+ scene.visuals.Arrow(pos=pos, parent=canvas_scene, connect='segments',
+ arrows=arrows, arrow_type='angle_60', arrow_size=3.,
+ width=3., antialias=False, arrow_color=arrow_color,
+ color=line_color)
+ scene.visuals.Text(pos=self.transform_points_abc_to_xyz(0.7*np.eye(3)-0.5), parent=canvas_scene, text=["a", "b", "c"], color=arrow_color,
+ font_size=self.axes_font_size*self.supercell_scale_abc_to_xyz)
+
+
+ def plot_unit_cell_box(self, canvas_scene):
+ for zcen in range(self.int_extent[2]):
+ for ycen in range(self.int_extent[1]):
+ scene.visuals.Line(pos = self.transform_points_abc_to_xyz(np.array([[0, ycen, zcen], [np.ceil(self.extent[0]), ycen, zcen]])),
+ parent=canvas_scene, color=color_array.Color(color="k", alpha=0.25)) # , method="gl")
+ for xcen in range(self.int_extent[0]):
+ for ycen in range(self.int_extent[1]):
+ scene.visuals.Line(pos = self.transform_points_abc_to_xyz(np.array([[xcen, ycen, 0], [xcen, ycen, np.ceil(self.extent[2])]])),
+ parent=canvas_scene, color=color_array.Color(color="k", alpha=0.25)) # , method="gl")
+ for xcen in range(self.int_extent[0]):
+ for zcen in range(self.int_extent[2]):
+ scene.visuals.Line(pos = self.transform_points_abc_to_xyz(np.array([[xcen, 0, zcen], [xcen, np.ceil(self.extent[1]), zcen]])),
+ parent=canvas_scene, color=color_array.Color(color="k", alpha=0.25)) # , method="gl")
+
+ def get_atomic_properties_in_supercell(self):
+ atoms_pos_unit_cell = np.array([atom.pos for atom in self.unit_cell.atoms])
+ natoms = atoms_pos_unit_cell.shape[0]
+ atoms_pos_supercell = np.zeros((self.ncells*natoms, 3))
+ icell = 0
+ # loop over unit cells in same order as in MATLAB
+ for zcen in range(self.int_extent[2]):
+ for ycen in range(self.int_extent[1]):
+ for xcen in range(self.int_extent[0]):
+ atoms_pos_supercell[icell*natoms:(icell+1)*natoms,:] = atoms_pos_unit_cell + np.array([xcen, ycen, zcen])
+ icell += 1
+ is_matom = np.tile([atom.is_mag for atom in self.unit_cell.atoms], self.ncells)
+ sizes = np.tile([atom.size for atom in self.unit_cell.atoms], self.ncells)
+ colors = np.tile(np.array([atom.color for atom in self.unit_cell.atoms]).reshape(-1,3), (self.ncells, 1))
+ # remove points beyond extent of supercell
+ atoms_pos_supercell, iremove = self._remove_points_outside_extent(atoms_pos_supercell)
+ sizes = np.delete(sizes, iremove)
+ colors = np.delete(colors, iremove, axis=0)
+ # get indices of magnetic atoms outside extents
+ iremove_mag = [np.sum(is_matom[:irem]) for irem in iremove if is_matom[irem]]
+ is_matom = np.delete(is_matom, iremove)
+ # transfrom to xyz
+ atoms_pos_supercell = self.transform_points_abc_to_xyz(atoms_pos_supercell)
+ # get atomic labels
+ labels = np.tile([atom.label for atom in self.unit_cell.atoms], self.ncells)
+ labels = np.delete(labels, iremove).tolist()
+ return atoms_pos_supercell, is_matom, colors, sizes, labels, iremove, iremove_mag
+
+ def plot_magnetic_structure(self, canvas_scene, mj, pos, colors):
+ verts = np.c_[pos, pos + self.spin_scale*mj] # natom x 6
+ scene.visuals.Arrow(pos=verts.reshape(-1,3), parent=canvas_scene, connect='segments',
+ arrows=verts, arrow_size=self.arrow_head_size,
+ width=self.arrow_width, antialias=True,
+ arrow_type='triangle_60',
+ color=np.repeat(colors, 2, axis=0).tolist(),
+ arrow_color= colors.tolist())
+
+ def plot_rotation_plane(self, canvas_scene, pos, colors, npts=15):
+ # generate vertices of disc with normal // [0,0,1]
+ theta = np.linspace(0, 2*np.pi,npts)[:-1] # exclude 2pi
+ disc_verts = np.zeros((npts, 3))
+ disc_verts[1:,0] = self.rotation_plane_radius*np.cos(theta)
+ disc_verts[1:,1] = self.rotation_plane_radius*np.sin(theta)
+ # rotate given normal
+ rot_mat = get_rotation_matrix(self.n)
+ disc_verts = rot_mat.dot(disc_verts.T).T
+ disc_faces = self._label_2D_mesh_faces(disc_verts)
+ # for each row (atom) in pos to add to shift to verts (use np boradcasting)
+ disc_verts = (disc_verts + pos[:,None]).reshape(-1,3)
+ # increment faces indices to match larger verts array (use np boradcasting)
+ disc_faces = (disc_faces + np.repeat(npts*np.arange(pos.shape[0]), 3).reshape(-1,1,3)).reshape(-1,3)
+ # repeat colors
+ face_colors = np.tile(colors, (npts-1, 1))
+ face_colors = np.c_[face_colors, np.full((face_colors.shape[0], 1), self.mesh_alpha)] # add transparency
+ scene.visuals.Mesh(vertices=disc_verts, faces=disc_faces, face_colors=face_colors, parent=canvas_scene)
+
+ def plot_ion_ellipsoids(self, canvas_scene, npts=7):
+ matoms = [atom for atom in self.unit_cell.atoms if atom.is_mag and np.any(atom.get_transform(tensor=self.ion_type))]
+ if len(matoms) > 0:
+ # get mesh for a sphere
+ meshdata = create_sphere(radius=self.ion_radius, rows=npts, cols=npts)
+ sphere_verts = meshdata.get_vertices()
+ sphere_faces = meshdata.get_faces()
+ # loop over ions and get mesh verts and faces
+ ion_verts = np.zeros((len(matoms) * self.ncells * sphere_verts.shape[0], 3))
+ ion_faces = np.zeros((len(matoms) * self.ncells * sphere_faces.shape[0], 3))
+ face_colors = np.full((ion_faces.shape[0], 4), self.mesh_alpha)
+ irow_verts, irow_faces = 0, 0
+ imesh = 0
+ for atom in matoms:
+ ellip_verts = sphere_verts @ atom.get_transform(tensor=self.ion_type)
+ for zcen in range(self.int_extent[2]):
+ for ycen in range(self.int_extent[1]):
+ for xcen in range(self.int_extent[0]):
+ centre = (atom.pos + np.array([xcen, ycen, zcen])).reshape(1,-1) # i.e. make 2D array
+ centre, _ = self._remove_points_outside_extent(centre)
+ if centre.size > 0:
+ # atom in extents
+ centre = self.transform_points_abc_to_xyz(centre)
+ ion_verts[irow_verts:irow_verts+sphere_verts.shape[0]] = ellip_verts + centre
+ ion_faces[irow_faces:irow_faces+sphere_faces.shape[0]] = sphere_faces + sphere_verts.shape[0] * imesh
+ face_colors[irow_faces:irow_faces+sphere_faces.shape[0], :3] = atom.color # np broadcasting allows this
+ irow_verts = irow_verts+sphere_verts.shape[0]
+ irow_faces = irow_faces+sphere_faces.shape[0]
+ imesh += 1
+ mesh = scene.visuals.Mesh(vertices=ion_verts[:irow_verts,:], faces=ion_faces[:irow_faces,:].astype(int), face_colors=face_colors[:irow_faces,:], parent=canvas_scene)
+ wireframe_filter = WireframeFilter(color=3*[0.7])
+ mesh.attach(wireframe_filter)
+
+ def plot_atoms(self, canvas_scene, pos, colors, sizes, labels):
+ scene.visuals.Markers(
+ pos=pos,
+ size=sizes,
+ antialias=0,
+ face_color= colors,
+ edge_color='white',
+ edge_width=0,
+ scaling=True,
+ spherical=True,
+ alpha=self.atom_alpha,
+ parent=canvas_scene)
+ # labels
+ if self.do_plot_labels:
+ scene.visuals.Text(pos=pos, parent=canvas_scene, text=labels, color="white", font_size=self.font_size*self.cell_scale_abc_to_xyz)
+
+ def plot_bonds(self, canvas_scene):
+ max_dm_norm = self.unit_cell.get_max_DM_vec_norm()
+ for bond_name in self.unit_cell.bonds:
+ color = self.unit_cell.get_bond_color(bond_name)
+ verts = self._get_supercell_bond_verts(bond_name)
+ if self.unit_cell.is_bond_symmetric(bond_name):
+ scene.visuals.Line(pos=verts, parent=canvas_scene, connect='segments',
+ width=self.bond_width, color=color)
+ else:
+ # DM bond
+ # generate verts of DM arrows at bond mid-points (note DM vector in xyz)
+ mid_points = verts.reshape(-1,2,3).sum(axis=1)/2
+ dm_vec = self.dm_arrow_scale*self.unit_cell.get_bond_DM_vec(bond_name)/max_dm_norm
+ dm_verts = np.c_[mid_points, mid_points + dm_vec]
+ arrow_verts = np.r_[np.c_[verts[::2], mid_points], dm_verts] # draw arrow at mid-point of line as well as DM vec
+ line_verts = np.r_[verts, dm_verts.reshape(-1,3)]
+ scene.visuals.Arrow(pos=line_verts, parent=canvas_scene, connect='segments',
+ arrows=arrow_verts, arrow_size=self.arrow_head_size,
+ width=self.arrow_width, antialias=True,
+ arrow_type='triangle_60',
+ color=color,
+ arrow_color=color)
+
+ def _get_supercell_bond_verts(self, bond_name):
+ bond = self.unit_cell.bonds[bond_name]
+ bond_verts_unit_cell = self.unit_cell.get_bond_vertices(bond_name)
+ nverts_per_cell = bond_verts_unit_cell.shape[0]
+ bond_verts_supercell = np.zeros((self.ncells*nverts_per_cell, 3))
+ icell = 0
+ for zcen in range(self.int_extent[2]):
+ for ycen in range(self.int_extent[1]):
+ for xcen in range(self.int_extent[0]):
+ lvec = np.array([xcen, ycen, zcen])
+ bond_verts_supercell[icell*nverts_per_cell:(icell+1)*nverts_per_cell,:] = bond_verts_unit_cell + lvec
+ icell += 1
+ bond_verts_supercell, _ = self._remove_vertices_outside_extent(bond_verts_supercell)
+ bond_verts_supercell = self.transform_points_abc_to_xyz(bond_verts_supercell)
+ return bond_verts_supercell
+
+
+ def plot_polyhedra(self, canvas_scene):
+ polyhedra = self._calc_convex_polyhedra_mesh()
+ # loop over all unit cells and add origin to mesh vertices
+ npoly = self.ncells*len(polyhedra)
+ nverts_per_poly = polyhedra[0].vertices.shape[0]
+ verts = np.zeros((npoly*nverts_per_poly, 3))
+ nfaces_per_poly = polyhedra[0].faces.shape[0]
+ faces = np.zeros((npoly*nfaces_per_poly, 3))
+ irow_verts, irow_faces = 0, 0
+ ipoly = 0
+ for zcen in range(self.int_extent[2]):
+ for ycen in range(self.int_extent[1]):
+ for xcen in range(self.int_extent[0]):
+ lvec = self.transform_points_abc_to_xyz(np.array([xcen, ycen, zcen]))
+ for poly in polyhedra:
+ this_verts = poly.vertices + lvec
+ _, irem = self._remove_points_outside_extent(self.transform_points_xyz_to_abc(this_verts))
+ if len(irem) < self.polyhedra_args.n_nearest:
+ # polyhedron has at least 1 vertex inside extent
+ verts[irow_verts:irow_verts+nverts_per_poly, : ] = this_verts
+ faces[irow_faces:irow_faces+nfaces_per_poly, : ] = poly.faces + nverts_per_poly * ipoly
+ irow_verts = irow_verts+nverts_per_poly
+ irow_faces = irow_faces+nfaces_per_poly
+ ipoly += 1
+ mesh = scene.visuals.Mesh(vertices=verts[:irow_verts,:], faces=faces[:irow_faces,:].astype(int),
+ color=color_array.Color(color=self.polyhedra_args.color, alpha=self.mesh_alpha), parent=canvas_scene)
+ wireframe_filter = WireframeFilter(color=3*[0.7])
+ mesh.attach(wireframe_filter)
+
+ def _calc_convex_polyhedra_mesh(self):
+ atom2_pos_xyz = self.transform_points_abc_to_xyz(np.array([atom.pos for atom in self.unit_cell.atoms if atom.wyckoff_index in self.polyhedra_args.atom2_idx]))
+ natom2 = atom2_pos_xyz.shape[0]
+ polyhedra = []
+ for atom1_pos_rlu in np.array([atom.pos for atom in self.unit_cell.atoms if atom.wyckoff_index in self.polyhedra_args.atom1_idx]):
+ # find vector bewteen atom1 in unit cells +/- 1 in each direction to atom2 in first unit cell
+ dr = np.zeros((27*natom2, 3))
+ icell = 0
+ for dz in range(-1,2):
+ for dy in range(-1,2):
+ for dx in range(-1,2):
+ atom1_pos_xyz = self.transform_points_abc_to_xyz(atom1_pos_rlu + np.array([dx, dy, dz]))
+ dr[icell*natom2:(icell+1)*natom2,:] = -atom2_pos_xyz + atom1_pos_xyz # ordered like this due to np broadcasting
+ icell += 1
+ # keep unique within some tolerance (1e-3)
+ _, unique_idx = np.unique(np.round(dr,3), axis=0, return_index=True)
+ dr = dr[unique_idx]
+ # sort and get n shortest
+ isort = np.argsort(np.linalg.norm(dr, axis=1))
+ dr = dr[isort[:self.polyhedra_args.n_nearest]]
+ atom1_pos_xyz = self.transform_points_abc_to_xyz(atom1_pos_rlu)
+ verts_xyz = dr + atom1_pos_xyz
+ rank = np.linalg.matrix_rank(dr)
+ if rank == 3:
+ hull = ConvexHull(verts_xyz)
+ polyhedra.append(PolyhedronMesh(vertices=verts_xyz[hull.vertices], faces=hull.simplices))
+ elif rank == 2:
+ # transform to basis of polygon plane
+ *_, evecs_inv = np.linalg.svd(verts_xyz - verts_xyz[0]) # sorted in decreasing order of singular value
+ verts_2d = (evecs_inv @ verts_xyz.T).T
+ hull = ConvexHull(verts_2d[:, :-1]) # exclude last col (out of polygon plane - all have same value)
+ verts_xyz = np.vstack((atom1_pos_xyz, verts_xyz[hull.vertices], )) # include central atom1 position as vertex
+ faces = self._label_2D_mesh_faces(verts_xyz)
+ polyhedra.append(PolyhedronMesh(vertices=verts_xyz, faces=faces))
+ else:
+ warnings.warn('Polyhedron vertices must not be a line or point')
+ return polyhedra
+
+ def _label_2D_mesh_faces(self, verts):
+ # assume centre point has index 0
+ nverts = verts.shape[0]
+ faces = np.zeros((nverts-1, 3), dtype=int)
+ faces[:,1] = np.arange(1,nverts)
+ faces[:,2] = np.arange(2,nverts + 1)
+ faces[-1,2] = 1 # close the shape by returning to first non-central vertex
+ return faces
+
+ def _remove_vertices_outside_extent(self, verts):
+ # DO THIS BEFORE CONVERTING TO XYZ
+ # remove pairs of verts that correpsond to a line outside the extent
+ _, iremove = self._remove_points_outside_extent(verts)
+ iatom2 = (iremove % 2).astype(bool) # end point of pair of vertices
+ # for atom2 type vertex we need to remove previous row (atom1 vertex)
+ # for atom1 type vertex we need to remove the subsequent row (atom2 vertex)
+ iremove = np.hstack((iremove, iremove[iatom2]-1, iremove[~iatom2]+1))
+ return np.delete(verts, iremove, axis=0), iremove
+
+ def _remove_points_outside_extent(self, points, tol=1e-10):
+ # DO THIS BEFORE CONVERTING TO XYZ
+ iremove = np.flatnonzero(np.logical_or(np.any(points < -tol, axis=1), np.any((points - self.extent)>tol, axis=1)))
+ return np.delete(points, iremove, axis=0), iremove
+
+class UnitCell:
+ def __init__(self, atoms_list=None, bonds=None):
+ self.atoms = atoms_list if atoms_list is not None else []
+ self.bonds = bonds if bonds is not None else {}
+
+ def add_atom(self, atom):
+ self.atoms.append(atom)
+
+ def add_bond_vertices(self, name, atom1_idx, atom2_idx, dl, mat, color):
+ # get type of interaction from matrix
+ self.bonds[name] = {'verts': np.array([(self.atoms[atom1_idx[ibond]].pos,
+ self.atoms[atom2_idx[ibond]].pos + dl) for ibond, dl in enumerate(np.asarray(dl))]).reshape(-1,3)}
+ self.bonds[name]['is_sym'] = np.allclose(mat, mat.T)
+ self.bonds[name]['DM_vec'] = np.array([mat[1,2], mat[2,0], mat[0,1]]) if not self.bonds[name]['is_sym'] else None
+ self.bonds[name]['color'] = color
+
+ def get_bond_vertices(self, bond_name):
+ return self.bonds[bond_name]['verts']
+
+ def get_bond_DM_vec(self, bond_name):
+ return self.bonds[bond_name]['DM_vec']
+
+ def get_bond_color(self, bond_name):
+ return self.bonds[bond_name]['color']
+
+ def is_bond_symmetric(self, bond_name):
+ return self.bonds[bond_name]['is_sym']
+
+ def get_max_DM_vec_norm(self):
+ max_norm = 0
+ for bond_name in self.bonds:
+ if self.bonds[bond_name]['DM_vec'] is not None:
+ max_norm = max(max_norm, np.linalg.norm(self.bonds[bond_name]['DM_vec']))
+ return max_norm
+
+
+class Atom:
+ def __init__(self, index, position, is_mag=False, moment=np.zeros(3), size=0.2, gtensor_mat=None, aniso_mat=None, label='atom', color="blue"):
+ self.pos = np.asarray(position)
+ self.is_mag = is_mag
+ self.moment=moment
+ self.gtensor = gtensor_mat
+ self.aniso = aniso_mat
+ self.size = size
+ self.color = color
+ self.label = label
+ self.spin_scale = 0.3
+ self.wyckoff_index = index
+
+ def get_transform(self, tensor='aniso'):
+ if tensor=="aniso":
+ mat = self.aniso
+ else:
+ mat = self.gtensor
+ # diagonalise so can normalise eigenvalues
+ evals, evecs = np.linalg.eig(mat)
+ if not evals.all():
+ warnings.warn(f"Singular {tensor} matrix on atom {self.label}")
+ return np.zeros(mat.shape) # transform will be ignored
+ else:
+ if tensor=="aniso":
+ # take inverse of eigenvals as large number should produce a small axis
+ evals = 1/evals
+ # scale such that max eigval is 1
+ evals = evals/np.max(abs(evals))
+ return evecs @ np.diag(evals) @ np.linalg.inv(evecs)
+
+
+def get_rotation_matrix(vec2, vec1=np.array([0,0,1])):
+ vec1 = vec1/np.linalg.norm(vec1) # unit vectors
+ vec2 = vec2/np.linalg.norm(vec2)
+ if np.arccos(np.clip(np.dot(vec1.flat, vec2.flat), -1.0, 1.0)) > 1e-5:
+ r = Rotation.align_vectors(vec2.reshape(1,-1), vec1.reshape(1,-1)) # matrix to rotate vec1 onto vec2
+ return r[0].as_matrix()
+ else:
+ # too small a difference for above algorithm, just return identity
+ return np.eye(3)
\ No newline at end of file
diff --git a/python/tests/systemtest_spinwave.py b/python/tests/systemtest_spinwave.py
new file mode 100644
index 000000000..1216fd4ed
--- /dev/null
+++ b/python/tests/systemtest_spinwave.py
@@ -0,0 +1,164 @@
+from pyspinw import Matlab
+from libpymcr.MatlabProxyObject import wrap
+import numpy as np
+import scipy.io
+import unittest
+import copy
+import os
+
+m = Matlab()
+
+class SystemTest_Spinwave(unittest.TestCase):
+ """
+ Port to Python of Matlab `sw_tests.system_tests.systemtest_spinwave` classes
+ without code to generate reference files (will use Matlab generated files)
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ try:
+ import matplotlib.pyplot
+ cls.plt = matplotlib.pyplot
+ except ImportError:
+ cls.plt = None
+ curdir = os.path.dirname(__file__)
+ cls.ref_data_dir = os.path.abspath(os.path.join(curdir, '..', '..', 'test_data', 'system_tests'))
+ cls.relToll = 0.01
+ cls.absToll = 1.0e-6
+ cls.bigVal = 1.0e8
+ cls.tolSab = 0.05
+ m.swpref().fid = 0 # Suppress printing to make test output less verbose
+
+
+ @classmethod
+ def get_hash(cls, obj):
+ # Uses Matlab's (undocumented) hash to be consistent with generated reference data
+ engine = wrap(m._interface.call('java.security.MessageDigest.getInstance', ['MD5']), m._interface)
+ engine.update(m.getByteStreamFromArray(obj))
+ return m.typecast(engine.digest(), 'uint8')
+
+
+ @classmethod
+ def harmonize(cls, inp):
+ # Harmonizes saved mat file and new output data
+ # Removes singleton dimensions and nested single structures, converts some types to match
+ if isinstance(inp, np.ndarray):
+ if len(inp.dtype) > 1 and all([inp.dtype[ii]=='O' for ii in range(len(inp.dtype))]):
+ # Convert from dtype array to dict if array is all objects
+ return {ky:cls.harmonize(inp[ky]) for ky in inp.dtype.names}
+ if len(inp.shape) > 0 and inp.size == 1:
+ return cls.harmonize(inp[0])
+ if len(inp.shape) > 1 and any([s==1 for s in inp.shape]):
+ return cls.harmonize(np.squeeze(inp))
+ if inp.dtype == 'O' and inp.size > 1:
+ # Convert object array to list
+ return [inp[ii] for ii in range(inp.size)]
+ elif isinstance(inp, dict):
+ return {ky:cls.harmonize(vl) for ky, vl in inp.items()}
+ elif isinstance(inp, np.str_):
+ return str(inp)
+ elif isinstance(inp, np.uint8):
+ return np.float64(inp)
+ elif isinstance(inp, np.int16):
+ return np.float64(inp)
+ return inp
+
+
+ def load_ref_dat(self, filename):
+ self.reference_data = self.harmonize(scipy.io.loadmat(os.path.join(self.ref_data_dir, filename)))
+
+
+ def get_fieldname(self, pars):
+ if not pars:
+ pars = 'data'
+ if isinstance(pars, str):
+ return pars
+ return f'd{m.dec2hex(self.get_hash(pars))}'
+
+
+ def sanitize(self, indat):
+ out = copy.deepcopy(indat)
+ out[np.where(np.abs(out) > self.bigVal)] = 0.0
+ return out
+
+
+ def verifyIsEqual(self, test_data, ref_data, key=''):
+ # Equivalent to Matlab recursive `verifyThat(a, IsEqual(b))` with absolute and relative bounds
+ self.assertIs(type(test_data), type(ref_data), msg=f'Item {key} type mismatch')
+ if isinstance(test_data, dict):
+ self.assertEqual(test_data.keys(), ref_data.keys())
+ for ky in test_data.keys():
+ self.verifyIsEqual(test_data[ky], ref_data[ky], ky if not key else f'{key}.{ky}')
+ elif isinstance(test_data, list):
+ for ii in range(len(test_data)):
+ self.verifyIsEqual(test_data[ii], ref_data[ii], f'[{ii}]' if not key else f'{key}[{ii}]')
+ elif isinstance(test_data, np.ndarray):
+ np.testing.assert_allclose(self.sanitize(test_data), self.sanitize(ref_data),
+ rtol=self.relToll, atol=self.absToll, equal_nan=True,
+ err_msg=f'Item {key} are not close', verbose=True)
+ else:
+ self.assertEqual(test_data, ref_data)
+
+
+ def approxMatrix(self, actual, expected, frac_not_match):
+ # Checks if two arrays are approximately the same with most entries equal but a fraction not
+ if isinstance(actual, list):
+ return [self.approxMatrix(xx, yy, frac_not_match) for xx, yy in zip(actual, expected)]
+ diff = np.abs(actual - expected)
+ rel_diff = np.divide(diff, expected, out=np.zeros_like(expected), where=expected!=0)
+ # Possible alternative:
+ #return np.where((diff > self.absToll) & (rel_diff > self.relToll), expected, actual)
+ frac = np.where((diff > self.absToll) & (rel_diff > self.relToll))[0].size / diff.size
+ return expected if frac < frac_not_match else actual
+
+
+ def verify_eigval_sort(self, actual, expected, nested=0):
+ # Checks if eigenvalues match, if not try different sort permutations
+ if isinstance(actual, list):
+ vv = (self.verify_eigval_sort(xx, yy, nested) for xx, yy in zip(actual, expected))
+ return (sum(zz, start=zz[0]) for zz in zip(*vv))
+ if not np.allclose(actual, expected, rtol=self.relToll, atol=self.absToll, equal_nan=True):
+ sort_ax = np.where(np.array(actual.shape) > 1)[0][0]
+ if nested > 1:
+ actual = np.sort(np.abs(actual), axis=sort_ax)
+ expected = np.sort(np.abs(expected), axis=sort_ax)
+ else:
+ actual = np.sort(np.real(actual), axis=sort_ax)
+ expected = np.sort(np.real(expected), axis=sort_ax)
+ if nested < 2:
+ actual, expected = self.verify_eigval_sort(actual, expected, nested + 1)
+ return actual, expected
+
+
+ def verify(self, spec, pars, extrafields=None, approxSab=False):
+ # This is the Matlab `generate_or_verify` method without the reference data generation code
+ ref_data = self.reference_data[self.get_fieldname(pars)]
+ # There are some type-mismatch (strings/np.str_ and ints/floats) in the input data, ignore for now
+ ref_data.pop('input')
+ #test_data = {'input': m.struct(self.swobj)}
+ test_data = {}
+ omega, ref_data['spec'][0] = self.verify_eigval_sort(spec['omega'], ref_data['spec'][0])
+ test_data['spec'] = [omega, spec['Sab']]
+ if 'swConv' in spec:
+ test_data['spec'].append(spec['swConv'])
+ if 'swInt' in spec and spec['swInt']:
+ # Matlab code is not explicit that if 'swInt' in included, 'swConv' is also, but it is implicit
+ test_data['spec'].append(spec['swInt'])
+ else:
+ assert 'swInt' not in spec
+ if extrafields is not None:
+ test_data.update(extrafields)
+ if approxSab:
+ tolSab = approxSab if isinstance(approxSab, float) else self.tolSab
+ test_data['spec'][1] = self.approxMatrix(spec['Sab'], ref_data['spec'][1], tolSab)
+ if len(test_data) == 4:
+ test_data['spec'][3] = self.approxMatrix(spec['swInt'], ref_data['spec'][3], tolSab)
+ if 'Sabp' in test_data:
+ test_data['Sabp'] = self.approxMatrix(test_data['Sabp'], ref_data['Sabp'], tolSab)
+ if 'V' in test_data:
+ test_data['V'] = self.approxMatrix(test_data['V'], ref_data['V'], tolSab)
+ self.verifyIsEqual(self.harmonize(test_data), ref_data)
+
+
+ def verify_generic(self, data, fieldname):
+ return self.verifyIsEqual(data, self.reference_data[fieldname])
diff --git a/python/tests/test_spinw.py b/python/tests/test_spinw.py
new file mode 100644
index 000000000..afd4c56d5
--- /dev/null
+++ b/python/tests/test_spinw.py
@@ -0,0 +1,42 @@
+__author__ = 'github.com/wardsimon'
+__version__ = '0.0.1'
+
+import numpy as np
+from pyspinw import Matlab
+
+try:
+ from matplotlib import pyplot as plt
+except ImportError:
+ plt = None
+
+m = Matlab()
+# An example of specifying the MATLAB version and path
+# m = Matlab(matlab_version='R2023a', matlab_path='/usr/local/MATLAB/R2023a/')
+
+# Suppress output to make it less verbose in CI output
+m.swpref().fid = 0
+
+# Create a spinw model, in this case a triangular antiferromagnet
+s = m.sw_model('triAF', 1)
+print(s)
+
+# Specify the start and end points of the q grid and the number of points
+q_start = [0, 0, 0]
+q_end = [1, 1, 0]
+pts = 501
+
+# Calculate the spin wave spectrum, apply an energy grid and convolute with a Gaussian
+spec = m.sw_egrid(m.spinwave(s, [q_start, q_end, pts]))
+spec2 = m.sw_instrument(spec, dE=0.3)
+
+# Plot the result if matplotlib is available
+if plt is not None:
+ ax = plt.imshow(np.flipud(spec2['swConv']),
+ aspect='auto',
+ extent=[q_start[-1], q_end[0], spec2["Evect"][0][0], spec2["Evect"][0][-1]])
+ ax.set_clim(0, 0.15)
+ plt.xlabel('Q [q, q, 0] (r.l.u)')
+ plt.ylabel('Energy (meV)')
+ plt.title('Spectra of a triangular antiferromagnet')
+ plt.savefig('pyspinw.png')
+ plt.show()
diff --git a/python/tests/test_spinwave_af33kagome.py b/python/tests/test_spinwave_af33kagome.py
new file mode 100644
index 000000000..0fd36c0b3
--- /dev/null
+++ b/python/tests/test_spinwave_af33kagome.py
@@ -0,0 +1,53 @@
+import numpy as np
+import unittest
+import os, sys
+
+sys.path.append(os.path.abspath(os.path.dirname(__file__)))
+from systemtest_spinwave import SystemTest_Spinwave, m
+
+class AF33kagomeTest(SystemTest_Spinwave):
+
+ @classmethod
+ def setUpClass(cls):
+ super(AF33kagomeTest, cls).setUpClass()
+ af33kagome = m.spinw();
+ af33kagome.genlattice('lat_const',[6, 6, 40],'angled',[90, 90, 120],'sym','P -3');
+ af33kagome.addatom('r',[1/2, 0, 0],'S', 1,'label','MCu1','color','r');
+ af33kagome.gencoupling('maxDistance',7);
+ af33kagome.addmatrix('label','J1','value',1.00,'color','g');
+ af33kagome.addcoupling('mat','J1','bond',1);
+ cls.swobj = af33kagome
+ cls.relToll = 0.027
+ cls.absToll = 1.2e-5
+
+
+ def test_spinwave_calc(self):
+ self.load_ref_dat('systemstest_spinwave_af33kagome.mat')
+ af33kagome = self.swobj
+ # Syntax for S0/evect below needs libpymcr 0.1.3 or newer; else must make S0 a 2D np array and evect a list
+ S0 = [[0, 0, -1], [1, 1, -1], [0, 0, 0]]
+ af33kagome.genmagstr('mode','helical','k',[-1/3, -1/3, 0],'n',[0, 0, 1],'unit','lu','S',S0,'nExt',[1, 1, 1])
+ kag33spec = af33kagome.spinwave([[-1/2, 0, 0], [0, 0, 0], [1/2, 1/2, 0], 100],'hermit',False,'saveSabp',True)
+ evect = np.linspace(0, 3, 100)
+ kag33spec = m.sw_egrid(kag33spec,'component','Sxx+Syy','imagChk',False, 'Evect', evect)
+ # Reduce values of S(q,w) so it falls within tolerance (rather than change tolerance for all values)
+ kag33spec['swConv'] = kag33spec['swConv'] / 2e5
+ # Ignores swInt in this case
+ kag33spec['swInt'] = 0
+
+ if self.plt is not None:
+ ax = plt.imshow(np.real(np.flipud(kag33spec['swConv'])),
+ aspect='auto')
+ # ax.set_clim(0, 1e-6)
+ plt.xlabel('Q [q, q, 0] (r.l.u)')
+ plt.ylabel('Energy (meV)')
+ plt.title('Spectra of a triangular antiferromagnet')
+ plt.savefig('pyspinw.png')
+ plt.show()
+
+ self.verify(kag33spec, [], {'energy': af33kagome.energy(), 'Sabp': kag33spec['Sabp']}, approxSab=0.5)
+
+
+if __name__ == "__main__":
+ print('################# RUNNING TESTS ###################')
+ unittest.main()
diff --git a/release.py b/release.py
new file mode 100644
index 000000000..d6bc147c5
--- /dev/null
+++ b/release.py
@@ -0,0 +1,190 @@
+import argparse
+import json
+import os
+import re
+import sys
+import subprocess
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--github', action='store_true', help='Release on Github')
+ parser.add_argument('--create_tag', action='store_true', help='Create git tag if needed')
+ parser.add_argument('--pypi', action='store_true', help='Release on PyPI')
+ parser.add_argument('--notest', action='store_true', help='Actually send/upload')
+ parser.add_argument('--token', action='store', help='Github token to access repo')
+ parser.add_argument('--version_check', action='store_true', help='Check version strings')
+ args = parser.parse_args()
+
+ if args.version_check:
+ file_ver, _ = _version_check()
+ print(f'Version string "{file_ver}" in files match')
+
+ token = args.token
+ if token is None and 'GITHUB_TOKEN' in os.environ:
+ token = os.environ['GITHUB_TOKEN']
+
+ test = not args.notest
+ if args.github:
+ release_github(test, args.create_tag, token)
+
+ if args.pypi:
+ release_pypi(test, token)
+
+
+def release_github(test=True, create_tag=False, token=None):
+ rv = subprocess.run(['git', 'describe', '--tags', '--always'],
+ capture_output=True)
+ if rv.returncode != 0:
+ raise Exception(f'During git describe, got this error: {rv.stderr}')
+ git_ver = rv.stdout.decode().strip()
+ file_ver, changelog = _version_check()
+ if 'g' in git_ver and create_tag:
+ # Not in a release, create a new tag
+ rv = subprocess.run(['git', 'tag', file_ver], capture_output=True)
+ if rv.returncode != 0:
+ raise Exception(f'During tag, git returned this error: {rv.stderr}')
+ git_ver = file_ver
+ elif git_ver != file_ver:
+ raise Exception(f'version mismatch! __version__: {git_ver}; files: {file_ver}')
+
+ desc = re.search('# \[v[0-9\.]*\]\(http.*?\)\n(.*?)# \[v[0-9\.]*\]', changelog,
+ re.DOTALL | re.MULTILINE).groups()[0].strip()
+ payload = {
+ "tag_name": git_ver,
+ "target_commitish": "master",
+ "name": git_ver,
+ "body": desc,
+ "draft": False,
+ "prerelease": True
+ }
+ if test:
+ print(payload)
+ else:
+ upload_url = release_exists(git_ver, retval='upload_url', token=token)
+ if not upload_url:
+ upload_url = _create_gh_release(payload, token)
+ else:
+ upload_url = re.search('^(.*)\{\?', upload_url).groups()[0]
+ _upload_assets(upload_url, token)
+
+
+def release_pypi(test=True, token=None):
+ # Downloads wheels from github and upload to PyPI
+ import requests
+ response = requests.get(
+ 'https://api.github.com/repos/spinw/spinw/releases')
+ # Get the latest release
+ releases = response.json()
+ ids = [r['id'] for r in releases]
+ latest = [r for r in releases if r['id'] == max(ids)][0]
+ # Creates a custom wheelhouse folder
+ try:
+ os.mkdir('twine_wheelhouse')
+ except FileExistsError:
+ pass
+ # Loops through assets and downloads all the wheels
+ headers = {"Accept":"application/octet-stream"}
+ for asset in latest['assets']:
+ if asset['name'].endswith('whl'):
+ print('Downloading %s' % (asset['name']))
+ localfile = os.path.join('twine_wheelhouse', asset['name'])
+ download_github(asset['url'], localfile, token)
+ if not test:
+ rv = subprocess.run(['twine', 'upload', 'twine_wheelhouse/*'], capture_output=True)
+ if rv.returncode != 0:
+ raise Exception(f'During upload, twine returned this error: {rv.stderr}')
+
+
+def release_exists(tag_name, retval='upload_url', token=None):
+ import requests
+ headers = {}
+ if token is not None:
+ headers = {"Authorization": "token " + token}
+ response = requests.get(
+ 'https://api.github.com/repos/spinw/spinw/releases',
+ headers=headers)
+ if response.status_code != 200:
+ raise RuntimeError('Could not query Github if release exists')
+ response = json.loads(response.text)
+ desired_release = [v for v in response if v['tag_name'] == tag_name]
+ if desired_release:
+ return desired_release[0][retval]
+ else:
+ return False
+
+
+def download_github(url, local_filename=None, token=None):
+ import requests
+ headers = {"Accept":"application/octet-stream"}
+ if token is not None:
+ headers["Authorization"] = "token " + token
+ if not local_filename:
+ local_filename = url.split('/')[-1]
+ with requests.get(url, stream=True, headers=headers) as r:
+ with open(local_filename, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ f.write(chunk)
+ return local_filename
+
+
+def _version_check():
+ with open('CHANGELOG.md') as f:
+ changelog = f.read()
+ with open('CITATION.cff') as f:
+ citation = f.read()
+ cl_ver = re.findall('# \[(.*)\]\(http', changelog)[0]
+ cit_ver = 'v' + re.findall('\nversion: "(.*)"', citation)[0]
+ if cl_ver != cit_ver:
+ raise Exception(f'version mismatch! CHANGELOG.md: {cl_ver}; CITATION.cff: {cit_ver}')
+ return cl_ver, changelog
+
+
+def _create_gh_release(payload, token):
+ assert token is not None, 'Need token for this action'
+ import requests
+ response = requests.post(
+ 'https://api.github.com/repos/spinw/spinw/releases',
+ data=json.dumps(payload),
+ headers={"Authorization": "token " + token})
+ print(response.text)
+ if response.status_code != 201:
+ raise RuntimeError('Could not create release')
+ upload_url = re.search('^(.*)\{\?', json.loads(response.text)['upload_url']).groups()[0]
+ return upload_url
+
+
+def _upload_assets(upload_url, token):
+ assert token is not None, 'Need token for this action'
+ import requests
+ wheelpaths = None
+ wheelhouse = os.path.join('python', 'wheelhouse')
+ if os.path.exists(wheelhouse):
+ wheelpaths = [os.path.join(wheelhouse, ff) for ff in os.listdir(wheelhouse)]
+ if wheelpaths is not None:
+ for wheelpath in wheelpaths:
+ wheelfile = os.path.basename(wheelpath)
+ print(f'Uploading wheel {wheelpath}')
+ with open(wheelpath, 'rb') as f:
+ upload_response = requests.post(
+ f"{upload_url}?name={wheelfile}",
+ headers={"Authorization": "token " + token,
+ "Content-type": "application/octet-stream"},
+ data=f.read())
+ print(upload_response.text)
+ mltbx = os.path.join('mltbx', 'spinw.mltbx')
+ if os.path.exists(mltbx):
+ print('Uploading mltbx')
+ with open(mltbx, 'rb') as f:
+ upload_response = requests.post(
+ f"{upload_url}?name={mltbx}",
+ headers={"Authorization": "token " + token,
+ "Content-type": "application/octet-stream"},
+ data=f.read())
+ print(upload_response.text)
+ elif wheelpaths is None:
+ raise RuntimeError('No wheels or matlab-toolboxes found in folder. Cannot upload anything')
+ return None
+
+
+if __name__ == '__main__':
+ main()
diff --git a/run_performance_tests.m b/run_performance_tests.m
new file mode 100644
index 000000000..9ef029b6e
--- /dev/null
+++ b/run_performance_tests.m
@@ -0,0 +1,27 @@
+function run_performance_tests(nruns)
+ if nargin == 0
+ nruns = 1;
+ end
+ disp(version);
+ if ~exist('spinw', 'class')
+ if exist('swfiles', 'dir') && exist('external', 'dir') && exist('dat_files', 'dir')
+ addpath(genpath('swfiles'));
+ addpath(genpath('external'));
+ addpath(genpath('dat_files'));
+ else
+ error(['SpinW is not installed and the swfiles, external and/or ', ...
+ 'dat_files drectories couldn''t be found on the current ', ...
+ 'path, so the tests cannot be run.'])
+ end
+ end
+
+ % run tests
+ fpath_parts = {'+sw_tests', '+performance_tests'};
+ files = dir(fullfile(fpath_parts{:}, '*.m'));
+ for fname = {files.name}
+ for nrun = 1:nruns
+ run(erase(strjoin([fpath_parts, fname{1}], '.'), '+'));
+ end
+ end
+
+end
diff --git a/run_tests.m b/run_tests.m
new file mode 100644
index 000000000..6870a1af9
--- /dev/null
+++ b/run_tests.m
@@ -0,0 +1,67 @@
+function result = run_tests(out_dir)
+ disp(version);
+ if ~exist('spinw', 'class')
+ if exist('swfiles', 'dir') && exist('external', 'dir') && exist('dat_files', 'dir')
+ addpath(genpath('swfiles'));
+ addpath(genpath('external'));
+ addpath(genpath('dat_files'));
+ else
+ error(['SpinW is not installed and the swfiles, external and/or ', ...
+ 'dat_files drectories couldn''t be found on the current ', ...
+ 'path, so the tests cannot be run.'])
+ end
+ end
+ if nargin == 0
+ out_dir = fullfile(pwd);
+ end
+ if ~exist(out_dir, 'dir')
+ mkdir(out_dir);
+ end
+
+ import matlab.unittest.TestSuite
+ import matlab.unittest.TestRunner
+ import matlab.unittest.plugins.CodeCoveragePlugin
+ import matlab.unittest.plugins.codecoverage.CoberturaFormat
+ import matlab.unittest.selectors.HasTag
+ import matlab.unittest.plugins.XMLPlugin
+
+ % Suppress printing to make test output less verbose
+ pref = swpref;
+ pref.fid = 0;
+ pref.usemex = 0; % Tests which use mex will set it themselves
+ pref.nthread = -1;
+ pref.nspinlarge = 120;
+
+ suite = TestSuite.fromPackage('sw_tests', 'IncludingSubpackages', true);
+ if ~sw_hassymtoolbox()
+ % only run symbolic tests when the toolbox is available
+ suite = suite.selectIf(~HasTag('Symbolic'));
+ end
+ runner = TestRunner.withTextOutput;
+
+ % compile mex files
+ sw_mex('compile', true, 'test', false, 'swtest', false);
+
+ % Add coverage output
+ cov_dirs = {'swfiles', 'external'};
+ for i = 1:length(cov_dirs)
+ reportFormat = CoberturaFormat(fullfile(out_dir, ['coverage_', cov_dirs{i}, '.xml']));
+ coverage_plugin = CodeCoveragePlugin.forFolder(cov_dirs{i}, ...
+ 'Producing', reportFormat, ...
+ 'IncludingSubfolders', true);
+ runner.addPlugin(coverage_plugin);
+ if verLessThan('matlab', '9.12') % Can add cov for multiple folders only from R2022a
+ break;
+ end
+ end
+
+ % Add JUnit output - unique name so they are not overwritten on CI
+ junit_fname = ['junit_report_', computer, version('-release'), '.xml'];
+ junit_plugin = XMLPlugin.producingJUnitFormat(junit_fname);
+ runner.addPlugin(junit_plugin)
+
+ result = runner.run(suite)
+ if(any(arrayfun(@(x) x.Failed, result)))
+ error('Test failed');
+ end
+end
diff --git a/swfiles/+ndbase/cost_function_wrapper.m b/swfiles/+ndbase/cost_function_wrapper.m
new file mode 100644
index 000000000..cc0bb3ecb
--- /dev/null
+++ b/swfiles/+ndbase/cost_function_wrapper.m
@@ -0,0 +1,271 @@
+classdef cost_function_wrapper < handle & matlab.mixin.SetGet
+% ### Syntax
+%
+% `param = fit_parameter(value, lb, ub)`
+%
+% ### Description
+%
+% Class for evaluating cost function given data and parameters.
+% Optionally the parameters can be bound in which case the class will
+% perform a transformation to convert the constrained optimization problem
+% into an un-constrained problem, using the formulation devised
+% (and documented) for MINUIT [1] and also used in lmfit [2].
+%
+% [1] https://root.cern/root/htmldoc/guides/minuit2/Minuit2.pdf#section.2.3
+% [2] https://lmfit.github.io/lmfit-py/bounds.html
+%
+% ### Input Arguments
+%
+% `func`
+% : Function handle with one of the following definition:
+% * `R = func(p)` if `dat` is empty,
+% * `y = func(x,p)` if `dat` is a struct.
+% Here `x` is a vector of $N$ independent variables, `p` are the
+% $M$ parameters to be optimized and `y` is the simulated model.
+% If `resid_handle` argument is false (default) then the function returns
+% a scalar (the cost function to minimise e.g. chi-squared). If
+% `resid_handle` is true then the function returns a vector of residuals
+% (not the residuals squared).
+%
+% `parameters`
+% : Vector of doubles
+%
+% ### Name-Value Pair Arguments
+%
+% `data`
+% : Either empty or contains data to be fitted stored in a structure with
+% fields:
+% * `dat.x` vector of $N$ independent variables,
+% * `dat.y` vector of $N$ data values to be fitted,
+% * `dat.e` vector of $N$ standard deviation (positive numbers)
+% used to weight the fit. If zero or missing
+% an unweighted fit will be performed.
+%
+% `lb`
+% : Optional vector of doubles corresponding to the lower bound of the
+% parameters. Empty vector [] or vector of non-finite elements
+% (e.g. -inf and NaN) are interpreted as no lower bound.
+%
+% `ub`
+% : Optional vector of doubles corresponding to the upper bound of the
+% parameters. Empty vector [] or vector of non-finite elements
+% (e.g. inf and NaN) are interpreted as no upper bound.
+%
+% `ifix`
+% : Optional vector of ints corresponding of indices of parameters to fix
+% (overides bounds if provided)
+%
+% `resid_handle`
+% : Boolean scalar - if true and `dat` is empty then fucntion handle
+% returns array of residuals, if false (default) then function handle
+% returns a scalar cost function.
+
+ properties (SetObservable)
+ % data
+ cost_func
+ calc_resid
+ free_to_bound_funcs
+ bound_to_free_funcs
+ ifixed
+ ifree
+ pars_fixed
+ end
+
+ properties (Constant)
+ fix_tol = 1e-10
+ end
+
+ methods
+ function obj = cost_function_wrapper(fhandle, params, options)
+ arguments
+ fhandle {isFunctionHandleOrChar}
+ params double
+ options.lb double = []
+ options.ub double = []
+ options.data struct = struct()
+ options.ifix = []
+ options.resid_handle = false
+ end
+ if ischar(fhandle)
+ fhandle = str2func(fhandle); % convert to fuction handle
+ end
+ if isempty(fieldnames(options.data))
+ if options.resid_handle
+ obj.calc_resid = @(p) reshape(fhandle(p), [], 1);
+ obj.cost_func = @(p) sum(obj.calc_resid(p).^2);
+ else
+ % fhandle calculates cost_val
+ obj.cost_func = fhandle;
+ obj.calc_resid = [];
+ end
+ else
+ % fhandle calculates fit/curve function
+ if ~isfield(options.data,'e') || isempty(options.data.e) || ~any(options.data.e(:))
+ warning("ndbase:cost_function_wrapper:InvalidWeights",...
+ "Invalid weights provided - unweighted residuals will be used.")
+ obj.calc_resid = @(p) fhandle(options.data.x(:), p) - options.data.y(:);
+ else
+ obj.calc_resid = @(p) (fhandle(options.data.x(:), p) - options.data.y(:))./options.data.e(:);
+ end
+ obj.cost_func = @(p) sum(obj.calc_resid(p).^2);
+ end
+
+ % validate size of bounds
+ lb = options.lb;
+ ub = options.ub;
+ if ~isempty(lb) && numel(lb) ~= numel(params)
+ error("ndbase:cost_function_wrapper:WrongInput", ...
+ "Lower bounds must be empty or have same size as parameter vector.");
+ end
+ if ~isempty(ub) && numel(ub) ~= numel(params)
+ error("ndbase:cost_function_wrapper:WrongInput", ...
+ "Upper bounds must be empty or have same size as parameter vector.");
+ end
+ if ~isempty(lb) && ~isempty(ub) && any(ub ub
+ warning("ndbase:cost_function_wrapper:InvalidParameter",...
+ "A parameter is outside bounds set - parameter will be reset.")
+ par_bound = (lb + ub) / 2;
+ end
+ end
+ function par_bound = reset_bound_par_if_invalid_has_lb(par_bound, lb)
+ if par_bound < lb
+ warning("ndbase:cost_function_wrapper:InvalidParameter",...
+ "A parameter is outside bounds set - parameter will be reset.")
+ par_bound = par_bound + max(abs(lb), 1) / 2;
+ end
+ end
+ function par_bound = reset_bound_par_if_invalid_has_ub(par_bound, ub)
+ if par_bound > ub
+ warning("ndbase:cost_function_wrapper:InvalidParameter",...
+ "A parameter is outside bounds set - parameter will be reset.")
+ par_bound = par_bound - max(abs(ub), 1) / 2;
+ end
+ end
+ end
+end
+
+
+function isFunctionHandleOrChar(func)
+ assert(isa(func, 'function_handle') || ischar(func));
+end
+
diff --git a/swfiles/+ndbase/estimate_hessian.m b/swfiles/+ndbase/estimate_hessian.m
new file mode 100644
index 000000000..dd0e45489
--- /dev/null
+++ b/swfiles/+ndbase/estimate_hessian.m
@@ -0,0 +1,198 @@
+function [hessian, varargout] = estimate_hessian(fcost, params, options)
+% ### Syntax
+%
+% `hessian = ndbase.estimate_hessian(func_cost, params)`
+%
+% `[hessian, cost_val] = ndbase.estimate_hessian(func_cost, params)`
+%
+% `hessian = ndbase.estimate_hessian(func_cost, params, 'step', step_size)`
+%
+% `hessian = ndbase.estimate_hessian(func_cost, params, 'ivary', ivary)`
+%
+% ### Description
+%
+% Function to estimate hessian (curvature) matrix at the best-fit
+% parameters given a callable cost function, using finite-difference method.
+% The results have been checked against MATLAB's lsqnonlin.
+%
+% The covariance matrix can be calculated from the hessian
+%
+% `cov = inv(hess) * 2 * chisq_red`
+%
+% where `chisq_red` is the reduced chi-squared at the best fit parameters.
+%
+% The uncertainty on the best fit parameters is then given by
+% `param_errors = sqrt(diag(cov))`
+%
+% Note this method is more memory efficient than the error estimation in
+% e.g. ndbase.lm3 which uses the Gauss-Newton approx. to the hessian from
+% the partial derivative of the fit function with respect to each parameter
+% (jacobian of size [ndata x nparams]). However, evaluating the hessian
+% explicitly requires more function calls.
+%
+% ### Input Arguments
+%
+% `fcost`
+% : Callable cost function of form `fcost = @(params) ...` taking vector
+% of fit parameters, `params` and returning a scalar
+%
+% `params`
+% : Vector of best-fit parameters
+%
+% ### Name-Value Pair Arguments
+%
+% `step` (optional)
+% : Scalar double or vector of doubles (same size as params) governing the
+% step used in the finite difference derivatives:
+%
+% * If a scalar step_size is provided then this will be interpreted as a
+% fractional step_size i.e. the step used will be step_size*parameter.
+% * If a vector step_size is provided this will denote the absolute step
+% size for each parameter.
+% * If step_size is not provided the step size will be optimised
+% starting from a minimum fractional step_size of sqrt(eps) ~ 1e-8.
+% Note this may require additional function evauations - which may be
+% expensive.
+%
+% `ivary` (optional)
+% : indices of parameters which were varied in the fit - the final hessian
+% will be a sqare matrix of size [numel(ivary) x numel(ivary)].
+% If not provided all parameters will be varied.
+%
+% `niter` (optional)
+% : Number of iterations in optimising step size (step doubled in each
+% iteration from minimum value of sqrt(eps))
+%
+% `cost_tol` (optional)
+% : Minimum difference in cost function value for a step size to be
+% considered appropriate. Step size optimisation will be terminated once
+% this condition is met. Default is sqrt(epse) ~ 1e-8.
+%
+% `cost_val` (optional)
+% : Initial cost function evaluated at input parameters to avoid
+% additional function call if already available.
+%
+% ### Output Arguments
+%
+% `hessian`
+% : Hessian matrix from which covariance matrix can be calculated.
+%
+% `stats` (optional)
+% : Struct with following fields
+%
+% * `cost_val`: Cost function evaluted at input parameters
+% * `step_size`: Vector of steps in finite-difference method for each
+% parameter.
+% * `jacobian`: Vector of jacobian at input parameters (dcost/dp)
+
+% ### Examples
+%```
+% >> % make data
+% >> rng default;
+% >> fit_fun = @(x, params) params(1)*x + params(2)./x + params(3);
+% >> pars = [5,4,3];
+% >> x = linspace(0.1,2);
+% >> y = fit_fun(x, pars);
+% >> e = sqrt(y);
+% >> y = y + e.*randn(size(y));
+% >>
+% >> % fit data
+% >> fcost = @(params) sum(((y-fit_fun(x, params))./e).^2); % chisq
+% >> [pars_fit, chisq, ~] = ndbase.simplex([],fcost,pars);
+% >>
+% >> % estimate errors on fit parameters
+% >> hess = ndbase.estimate_hessian(fcost, pars_fit);
+% >> cov = inv(hess) * 2.0 * chisq/ (numel(y) - numel(pars_fit));
+% >> perr = sqrt(diag(cov)); % errors on best fit parameters
+%'''
+%
+ arguments
+ fcost function_handle
+ params double
+ options.step double = 0
+ options.ivary double = 0
+ options.niter (1,1) double = 16
+ options.cost_tol (1,1) double = sqrt(eps)
+ options.cost_val (1,1) double = nan
+ end
+ optimise_step = true;
+ min_step = sqrt(eps);
+ step_size = params.*min_step;
+ npar = numel(params);
+ ivary = 1:npar;
+ if options.step
+ optimise_step = false;
+ step_size = options.step;
+ if numel(step_size)==1
+ % interpret as fractional step size
+ step_size = abs(params) .* step_size;
+ end
+ end
+ if options.ivary
+ ivary = options.ivary;
+ npar = numel(ivary);
+ end
+ max_niter = options.niter;
+ cost_tol = options.cost_tol;
+
+
+ if numel(step_size) ~= numel(params)
+ error("ndbase:estimate_hessian", ...
+ "If step_size is provided it must be a scalar or vector " + ...
+ "with same number of elements as params");
+ end
+ % enforce minimum step size
+ step_size(abs(step_size) < min_step) = min_step;
+
+ % calculate jacobian at param
+ hessian = zeros(npar);
+ if isfinite(options.cost_val)
+ initial_cost = options.cost_val;
+ else
+ initial_cost = fcost(params);
+ end
+ jac_one_step = zeros(1, npar);
+ cost_one_step = zeros(1, npar);
+ for irow = 1:npar
+ ipar = ivary(irow);
+ success = false;
+ for niter = 1:max_niter
+ params(ipar) = params(ipar) + step_size(ipar);
+ cost_one_step(irow) = fcost(params);
+ delta_cost = (cost_one_step(irow) - initial_cost);
+ params(ipar) = params(ipar) - step_size(ipar);
+ if abs(delta_cost) > cost_tol || ~optimise_step
+ success = true;
+ break
+ else
+ step_size(ipar) = 2*step_size(ipar);
+ end
+ end
+ if optimise_step && ~success
+ step_size(ipar) = step_size(ipar)/2; % undo last doubling
+ warning("ndbase:estimate_hessian", ...
+ "Failed to optimise step size for parameter index %d -" + ...
+ " maximum step size used was %.2e", ipar, step_size(ipar));
+ end
+ jac_one_step(irow) = delta_cost/step_size(ipar);
+ end
+ % compute hessian
+ for irow = 1:npar
+ ipar_row = ivary(irow);
+ params(ipar_row) = params(ipar_row) + step_size(ipar_row);
+ for icol = irow:npar
+ ipar_col = ivary(icol);
+ params(ipar_col) = params(ipar_col) + step_size(ipar_col);
+ cost_two_step = fcost(params);
+ jac_two_step = (cost_two_step - cost_one_step(irow))/step_size(ipar_col);
+ hessian(irow, icol) = (jac_two_step - jac_one_step(icol))/step_size(ipar_row);
+ hessian(icol,irow) = hessian(irow, icol);
+ params(ipar_col) = params(ipar_col) - step_size(ipar_col);
+ end
+ params(ipar_row) = params(ipar_row) - step_size(ipar_row);
+ end
+ out_struct.cost_val = initial_cost;
+ out_struct.step_size = step_size;
+ out_struct.jacobian = jac_one_step;
+ varargout = {out_struct};
+end
diff --git a/swfiles/+ndbase/lm3.m b/swfiles/+ndbase/lm3.m
index 9cd3a8abc..80be94852 100644
--- a/swfiles/+ndbase/lm3.m
+++ b/swfiles/+ndbase/lm3.m
@@ -256,7 +256,7 @@
% function values at start
yBest = yCalc(:);
-
+converged=false;
if Niter > 0
% optimisation
@@ -357,6 +357,8 @@
tmp = repmat(1./sqrt(diag(cov)),[1,NpFree]);
cor = tmp.*cov.*tmp';
else
+ sigP = [];
+ iter=0;
chisqr = chi2Best/nnorm;
ok = true;
warning('WARNING: Convergence not achieved')
@@ -424,11 +426,11 @@
del=-min_abs_del;
end
end
- if dp(j)>=0
+ if dp(j) > 0
ppos=p;
ppos(j)=p(j)+del;
jac(:,j)=(func(dat.x,ppos)-f)/del;
- else
+ elseif dp(j) < 0
ppos=p; ppos(j)=p(j)+del;
pneg=p; pneg(j)=p(j)-del;
jac(:,j)=(func(dat.x,ppos)-func(dat.x,pneg))/(2*del);
diff --git a/swfiles/+ndbase/lm4.m b/swfiles/+ndbase/lm4.m
new file mode 100644
index 000000000..d83f46403
--- /dev/null
+++ b/swfiles/+ndbase/lm4.m
@@ -0,0 +1,297 @@
+function [pOpt, fVal, stat] = lm4(dat, func, p0, varargin)
+% optimization of parameters using the Levenberg Marquardt method
+%
+% ### Syntax
+%
+% `[pOpt,fVal,stat] = ndbase.simplex([],func,p0,Name,Value)`
+%
+% `[pOpt,fVal,stat] = ndbase.simplex(dat,func,p0,Name,Value)`
+%
+% ### Input Arguments
+%
+% `dat`
+% : Either empty or contains data to be fitted stored in a structure with
+% fields:
+% * `dat.x` vector of $N$ independent variables,
+% * `dat.y` vector of $N$ data values to be fitted,
+% * `dat.e` vector of $N$ standard deviation (positive numbers)
+% used to weight the fit. If zero or missing
+% `1/dat.y^2` will be assigned to each point.
+%
+% `func`
+% : Function handle with one of the following definition:
+% * `R = func(p)` if `dat` is empty,
+% * `y = func(x,p)` if `dat` is a struct.
+% Here `x` is a vector of $N$ independent variables, `p` are the
+% $M$ parameters to be optimized and `y` is the simulated model.
+% If `resid_handle` argument is false (default) then the function returns
+% a scalar (the cost funciton to minimise e.g. chi-squared). If
+% `resid_handle` is true then the function returns a vector of residuals
+% (not the residuals squared).
+%
+% `p0`
+% : vector of initial parameter guesses - starting point for the
+% optimisation.
+%
+% ### Name-Value Pair Arguments
+%
+% `'lb'`
+% : Vector with $M$ elements, lower boundary of the parameters. Default
+% value is -inf (i.e. unbounded).
+%
+% `'ub'`
+% : Vector with $M$ elements, upper boundary of the parameters. Default
+% value is inf (i.e. unbounded).
+%
+% `'resid_handle'`
+% : Boolean scalar - if true and `dat` is empty then 'func' fucntion handle
+% returns array of residuals, if false (default) then function handle
+% returns a scalar cost function.
+%
+% `'diff_step'`
+% : Vector with $M$ or 1 element, defines the fractional increment of
+% a parameter when calculating the Jacobians using the forward finite
+% difference (default is 1e-7).
+%
+% `'MaxIter'`
+% : Maximum number of iterations, default value is $100M$.
+%
+% `'gtol'`
+% : Convergence tolerance on gradient vector of cost-function wrt change
+% in parameters (default 1e-8).
+%
+% `'ftol'`
+% : Convergence tolerance on change in cost function (default 1e-8).
+%
+% `'ptol'`
+% : Convergence tolerance on relative length of parameter step vector wrt
+% parameter vector (default 1e-8).
+%
+% `'lambda0'`
+% : Initial Levenberg–Marquardt damping parameter which determines step
+% size and angle of trajectory between gradient-descent (initially) and
+% Gauss-Newton (where cost-function surface becomes increasingly
+% parabolic). Initially `large` lambda values correponds to smaller steps
+% along gradient descent trajectory. Decreasing `'lambda0'` may lead to
+% faster convergence if the solution is near the minimum but will be less
+% reliable further from the minimum where the second-order expansion is
+% less valid. Default value is 1e-2.
+%
+% `'nu_up'`
+% : Factor by which to scale the damping parameter (lambda) if parameter
+% step increases the cost value. Typically > 1 (default is 5) - ie.
+% increasing lambda to produce smaller steps such that the
+% first-order approx. is more valid.
+%
+% `'nu_dn'`
+% : Factor by which to scale the damping parameter (lambda) if parameter
+% step decreases the cost value. Typically < 1 (default is 0.3) - i.e.
+% decreasing lambda to speed-up convergence by taking larger steps as
+% the cost-function surface becomes increasingly parabolic.
+%
+% `'vary'`
+% : Boolean vector with $M$ elements (one per parameter), if an element is
+% false, the corresponding parameter will be fixed (not optimised).
+% Default is true for all parameters.
+%
+% ### Output
+%
+% `pOpt`
+% : Value of the $M$ optimal parameters.
+%
+% `fVal`
+% : Value of the cost function calculated at the optimal parameters
+%
+% `stat`
+% : Structure storing the detailed output of the calculation with
+% the following fields:
+% p Least-squares optimal estimate of the parameter values.
+% redX2 Reduced Chi squared statistic, its value
+% should be close to 1. If the value is larger, the
+% model is not a good description of the data. If the
+% value is smaller, the model is overparameterized
+% and fitting the statistical error of the data.
+% sigP Asymptotic standard error of the parameters.
+% sigY Asymptotic standard error of the curve-fit (to implement).
+% corrP Correlation matrix of the parameters (to implement).
+% Rsq R-squared cofficient of multiple determination (to implement).
+% cvgHst Convergence history (to implement)..
+% exitFlag The reason, why the code stopped:
+% 0 maximum number of iterations reached
+% 1 convergence step-size (see `ptol`)
+% 2 convergence in cost (see `ftol`)
+% 3 convergence in gradient (see `gtol`)
+% msg String, one of the above messages.
+% nIter The number of iterations executed during the fit.
+% param Input parameters passed to ndbase.lm4
+% algorithm name of algorithm (Levenberg Marquardt);
+% func Function handle corresponding to cost-function
+%
+% See also ndbase.simplex.
+
+nparams = numel(p0);
+
+inpForm.fname = {'diff_step' 'lb' 'ub' 'MaxIter' };
+inpForm.defval = {1e-7 -inf(1,nparams) inf(1,nparams) 100*nparams};
+inpForm.size = {[1 -1] [1 nparams] [1 nparams] [1 1]};
+
+inpForm.fname = [inpForm.fname {'gtol' 'ftol' 'ptol' 'lambda0'}];
+inpForm.defval = [inpForm.defval {1e-8 1e-8 1e-8 1e-2}];
+inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] [1 1]}];
+
+inpForm.fname = [inpForm.fname {'nu_up', 'nu_dn', 'resid_handle'}];
+inpForm.defval = [inpForm.defval {5 0.3, false}];
+inpForm.size = [inpForm.size {[1 1] [1 1], [1 1]}];
+
+inpForm.fname = [inpForm.fname {'vary'}];
+inpForm.defval = [inpForm.defval {true(1, nparams)}];
+inpForm.size = [inpForm.size {[1 nparams]}];
+
+param = sw_readparam(inpForm, varargin{:});
+
+cost_func_wrap = ndbase.cost_function_wrapper(func, p0, "data", dat, ...
+ 'lb', param.lb, 'ub', param.ub, ...
+ 'resid_handle', param.resid_handle, ...
+ 'ifix', find(~param.vary));
+% transform starting values into their unconstrained surrogates.
+p0_free = cost_func_wrap.get_free_parameters(p0);
+
+if isempty(dat) && ~param.resid_handle
+ % minimising scalar - empty resid
+ eval_cost_func = @eval_cost_scalar;
+ calc_hessian_and_jacobian = @calc_hessian_and_jacobian_scalar;
+ diff_step = param.diff_step; % always interpreted as fractional step even if 1 parameter
+else
+ % minimising sum square residuals
+ eval_cost_func = @eval_cost_resids;
+ calc_hessian_and_jacobian = @calc_hessian_and_jacobian_resid;
+ % get absolute diff_step for each parameter
+ diff_step = abs(p0_free).*param.diff_step;
+ min_step = sqrt(eps);
+ diff_step(abs(diff_step) < min_step) = min_step;
+end
+
+% eval at starting guess
+[cost_val, resids] = eval_cost_func(cost_func_wrap, p0_free(:));
+if isempty(resids)
+ ndof = 1; % minimising scalar function
+else
+ ndof = numel(resids) - numel(p0_free) + 1;
+end
+if isempty(p0_free)
+ % Re-calc bound params as p0 as could have fixed params outside bounds
+ pOpt = cost_func_wrap.get_bound_parameters(p0_free);
+ fVal = cost_val/ndof;
+ message = 'Parameters are fixed, no optimisation';
+ perr = zeros(size(pOpt));
+ niter = 0;
+ exit_flag = 0;
+else
+ p = p0_free(:);
+ lambda = param.lambda0;
+ [hess, jac] = calc_hessian_and_jacobian_scalar(cost_func_wrap, p, diff_step, cost_val, resids);
+ exit_flag = 0;
+ for niter = 1:param.MaxIter
+ dp = calc_parameter_step(hess, jac, lambda);
+ if norm(dp) < param.ptol*(param.ptol + norm(p))
+ message = "step size below tolerance ptol";
+ exit_flag = 1;
+ break
+ end
+ new_p = p - dp;
+ new_cost_val = cost_func_wrap.eval_cost_function(new_p);
+ dcost = new_cost_val - cost_val;
+ if dcost < 0
+ lambda = param.nu_dn*lambda; % decrease lambda
+ p = new_p;
+ [cost_val, resids] = eval_cost_func(cost_func_wrap, p);
+ [hess, jac] = calc_hessian_and_jacobian(cost_func_wrap, p, diff_step, cost_val, resids);
+ if abs(dcost) < param.ftol*ndof
+ message = "change in reduced chi-sq below tolerance ftol";
+ exit_flag = 2;
+ break
+ elseif norm(jac) < param.gtol
+ message = "change in gradient vector below tolerance gtol";
+ exit_flag = 3;
+ break
+ end
+ else
+ % increase lambda so take path closer to steepest descent
+ % don't recalc hess or jac
+ lambda = param.nu_up*lambda;
+ end
+ end
+ % collect output
+ pOpt = cost_func_wrap.get_bound_parameters(p);
+ perr = zeros(size(pOpt));
+ fVal = cost_val / ndof;
+ if exit_flag > 0
+ % converged on solution - calculate errors
+ cov = pinv(hess) * 2.0;
+ perr(cost_func_wrap.ifree) = sqrt(diag(cov));
+ else
+ message = "Failed to converge in MaxIter";
+ end
+end
+
+stat.Rsq = [];
+stat.sigY = [];
+stat.corrP = [];
+stat.cvgHst = [];
+stat.algorithm = 'Levenberg Marquardt';
+stat.func = cost_func_wrap.cost_func;
+stat.param = param;
+stat.param.Np = cost_func_wrap.get_num_free_parameters();
+stat.msg = message;
+stat.iterations = niter;
+stat.exitFlag = exit_flag;
+stat.p = pOpt;
+stat.redX2 = fVal;
+stat.sigP = perr;
+
+end
+
+function dp = calc_parameter_step(hess, jac, lambda)
+ damped_hess = hess + lambda*diag(diag(hess)); % LM
+ if rcond(damped_hess) < eps
+ dp = pinv(damped_hess)*jac; % nearly singular
+ else
+ try
+ dp = damped_hess\jac;
+ catch
+ dp = pinv(damped_hess)*jac;
+ end
+ end
+end
+
+function [hess, jac] = calc_hessian_and_jacobian_scalar(cost_func_wrap, p, diff_step, cost_val, ~)
+ % last input ignored (empty resids vector for consistent API)
+ [hess, extra_info] = ndbase.estimate_hessian(@cost_func_wrap.eval_cost_function, p, 'cost_val', cost_val, 'step', diff_step);
+ jac = extra_info.jacobian(:);
+end
+
+function [cost_val, resids] = eval_cost_scalar(cost_func_wrap, p)
+ cost_val = cost_func_wrap.eval_cost_function(p);
+ resids = [];
+end
+
+function [hess, jac] = calc_hessian_and_jacobian_resid(cost_func_wrap, p, diff_step, ~, resids)
+ % evaluate jacobian of residuals using finite difference
+ jac_resids = ones([numel(resids), numel(p)]);
+ for ipar = 1:numel(p)
+ p(ipar) = p(ipar) + diff_step(ipar);
+ resids_one_step = cost_func_wrap.eval_resid(p);
+ jac_resids(:,ipar) = (resids_one_step - resids)/diff_step(ipar);
+ p(ipar) = p(ipar) - diff_step(ipar);
+ end
+ % eval jacobian of cost function
+ jac = 2*(jac_resids')*resids;
+ % approx. hessian of cost fucntion
+ hess = 2*(jac_resids')*jac_resids;
+end
+
+function [cost_val, resids] = eval_cost_resids(cost_func_wrap, p)
+ resids = cost_func_wrap.eval_resid(p);
+ cost_val = sum(resids(:).^2);
+end
+
diff --git a/swfiles/+ndbase/simplex.m b/swfiles/+ndbase/simplex.m
index b4d21ef98..c0f8059ae 100644
--- a/swfiles/+ndbase/simplex.m
+++ b/swfiles/+ndbase/simplex.m
@@ -90,11 +90,18 @@
%
% `func`
% : Function handle with one of the following definition:
-% * `R2 = func(p)` if `dat` is empty,
+% * `R = func(p)` if `dat` is empty,
% * `y = func(x,p)` if `dat` is a struct.
% Here `x` is a vector of $N$ independent variables, `p` are the
-% $M$ parameters to be optimized and `y` is the simulated model, `R2`
-% is the value to minimize.
+% $M$ parameters to be optimized and `y` is the simulated model.
+% If `resid_handle` argument is false (default) then the function returns
+% a scalar (the cost funciton to minimise e.g. chi-squared). If
+% `resid_handle` is true then the function returns a vector of residuals
+% (not the residuals squared).
+%
+% `p0`
+% : vector of initial parameter guesses - starting point for the
+% optimisation.
%
% ### Name-Value Pair Arguments
%
@@ -126,277 +133,90 @@
% the weighted least square deviation from data). Default value is
% $10^{-3}$.
%
+% `'resid_handle'`
+% : Boolean scalar - if true and `dat` is empty then 'func' fucntion handle
+% returns array of residuals, if false (default) then function handle
+% returns a scalar cost function.
+%
+% `'vary'`
+% : Boolean vector with $M$ elements (one per parameter), if an element is
+% false, the corresponding parameter will be fixed (not optimised).
+% Default is true for all parameters.
%
% ### See Also
%
% [ndbase.lm] \| [ndbase.pso]
-
+%
% Original author: John D'Errico
% E-mail: woodchips@rochester.rr.com
% Release: 4
% Release date: 7/23/06
-if nargin == 0
- swhelp ndbase.simplex
- return
-end
-
-% number of parameters
-Np = numel(p0);
-
-% not implemented yet: 'MaxFunEvals'
-inpForm.fname = {'Display' 'TolFun' 'TolX' 'MaxIter' 'lb' 'ub' };
-inpForm.defval = {'off' 1e-3 1e-3 100*Np [] [] };
-inpForm.size = {[1 -1] [1 1] [1 1] [1 1] [-5 -2] [-3 -4] };
-inpForm.soft = {false false false false true true };
-
-param = sw_readparam(inpForm, varargin{:});
-param.Np = Np;
-
-% limits
-LB = param.lb;
-UB = param.ub;
-
-if ~isempty(LB) && ~isempty(UB) && any(UBLB);
-end
-
-% check input function
-if ischar(func)
- % convert to fuction handle
- func = str2func(func);
-end
-
-if ~isa(func,'function_handle')
- error('simplex:WrongInput','The input function is neither a string, not function handle!');
-end
-
-% define weighted least squares if dat is given
-if ~isempty(dat)
- dat.x = dat.x(:);
- dat.y = dat.y(:);
+ if nargin == 0
+ swhelp ndbase.simplex
+ return
+ end
+
+ % number of parameters
+ Np = numel(p0);
+
+ % not implemented yet: 'MaxFunEvals'
+ inpForm.fname = {'Display' 'TolFun' 'TolX' 'MaxIter' 'lb' 'ub' };
+ inpForm.defval = {'off' 1e-3 1e-3 100*Np [] [] };
+ inpForm.size = {[1 -1] [1 1] [1 1] [1 1] [-5 -2] [-3 -4] };
+ inpForm.soft = {false false false false true true };
+
+ inpForm.fname = [inpForm.fname {'resid_handle', 'vary'}];
+ inpForm.defval = [inpForm.defval {false, true(1, Np)}];
+ inpForm.size = [inpForm.size {[1 1], [1, Np]}];
+
+ param = sw_readparam(inpForm, varargin{:});
+
+ cost_func_wrap = ndbase.cost_function_wrapper(func, p0, "data", dat, ...
+ 'lb', param.lb, 'ub', param.ub, ...
+ 'resid_handle', param.resid_handle, ...
+ 'ifix', find(~param.vary));
+
+ % transform starting values into their unconstrained surrogates.
+ p0_free = cost_func_wrap.get_free_parameters(p0);
- if ~isfield(dat,'e') || isempty(dat.e) || ~any(dat.e(:))
- weight = 1./abs(dat.y);
+ if isempty(p0_free)
+ % All parameters fixed, evaluate cost at initial guess
+ % don't use p0 as could contain fixed params outside bounds
+ pOpt = cost_func_wrap.get_bound_parameters(p0_free);
+ fVal = cost_func_wrap.eval_cost_function(p0_free);
+ fit_stat.message = 'Parameters are fixed, no optimisation';
+ fit_stat.iterations = 0;
+ fit_stat.funcCount = 1;
+ exitFlag = 0;
else
- if any(dat.e(:)<0)
- error('pso:WrongInput','Standard deviations have to be positive!')
- end
- weight = 1./dat.e(:).^2;
+ % now we can call fminsearch, but with our own free parameter
+ [p_free, fVal, exitFlag, fit_stat] = fminsearch(@cost_func_wrap.eval_cost_function, p0_free, param);
+ % undo the variable transformations into the original space
+ pOpt = cost_func_wrap.get_bound_parameters(p_free);
end
- func0 = func;
- func = @(p)sum(weight.*(func(dat.x(:),p)-dat.y(:)).^2);
-end
-if isempty(LB)
- LB = repmat(-inf,Np,1);
-else
- LB = LB(:);
-end
-if (nargin<4) || isempty(UB)
- UB = inf(Np,1);
-else
- UB = UB(:);
-end
-
-if (Np~=numel(LB)) || (Np~=numel(UB))
- error('simplex:WrongInput','p0 is incompatible in size with the given limits!')
-end
-
-
-% 0 --> unconstrained variable
-% 1 --> lower bound only
-% 2 --> upper bound only
-% 3 --> dual finite bounds
-% 4 --> fixed variable
-param.BoundClass = zeros(Np,1);
-for ii=1:Np
- k = isfinite(LB(ii)) + 2*isfinite(UB(ii));
- param.BoundClass(ii) = k;
- if (k==3) && (LB(ii)==UB(ii))
- param.BoundClass(ii) = 4;
- end
-end
-
-% transform starting values into their unconstrained
-% surrogates. Check for infeasible starting guesses.
-p0u = p0;
-k = 1;
-for ii = 1:Np
- switch param.BoundClass(ii)
- case 1
- % lower bound only
- if p0(ii)<=LB(ii)
- % infeasible starting value. Use bound.
- p0u(k) = 0;
- else
- p0u(k) = sqrt(p0(ii) - LB(ii));
- end
-
- % increment k
- k=k+1;
- case 2
- % upper bound only
- if p0(ii)>=UB(ii)
- % infeasible starting value. use bound.
- p0u(k) = 0;
- else
- p0u(k) = sqrt(UB(ii) - p0(ii));
- end
-
- % increment k
- k=k+1;
- case 3
- % lower and upper bounds
- if p0(ii)<=LB(ii)
- % infeasible starting value
- p0u(k) = -pi/2;
- elseif p0(ii)>=UB(ii)
- % infeasible starting value
- p0u(k) = pi/2;
- else
- p0u(k) = 2*(p0(ii) - LB(ii))/(UB(ii)-LB(ii)) - 1;
- % shift by 2*pi to avoid problems at zero in fminsearch
- % otherwise, the initial simplex is vanishingly small
- p0u(k) = 2*pi+asin(max(-1,min(1,p0u(k))));
- end
-
- % increment k
- k=k+1;
- case 0
- % unconstrained variable. x0u(i) is set.
- p0u(k) = p0(ii);
-
- % increment k
- k=k+1;
- case 4
- % fixed variable. drop it before fminsearch sees it.
- % k is not incremented for this variable.
- end
-
-end
-% if any of the unknowns were fixed, then we need to shorten
-% x0u now.
-if k<=Np
- p0u(k:Np) = [];
-end
-
-% were all the variables fixed?
-if isempty(p0u)
- % All variables were fixed. quit immediately, setting the
- % appropriate parameters, then return.
-
- % undo the variable transformations into the original space
- x = xtransform(p0u,param);
-
- % stuff fval with the final value
- fVal = func(x,p0);
-
- stat = struct;
- stat.msg = 'Parameters are fixed, no optimisation';
- stat.p = p0;
- stat.sigP = [];
- stat.Rsq = [];
- stat.sigY = [];
- stat.corrP = [];
- stat.cvgHst = [];
- stat.iterations = 0;
- stat.funcCount = 1;
- stat.algorithm = 'Nelder-Mead simplex direct search';
- stat.exitFlag = 0;
- stat.param = param;
-
- if isempty(dat)
- stat.func = func;
- else
- stat.func = func0;
- end
- % return with no call at all to fminsearch
- return
-end
-
-
-% now we can call fminsearch, but with our own
-% intra-objective function.
-intrafun = @(x)func(xtransform(x,param));
-
-[pu,fVal,exitFlag,stat0] = fminsearch(intrafun,p0u,param);
-
-% undo the variable transformations into the original space
-pOpt = xtransform(pu,param);
-
-stat = struct;
-stat.p = pOpt;
-stat.sigP = [];
-if isempty(dat)
- stat.redX2 = fVal;
-else
- % divide R2 with the statistical degrees of freedom
- stat.redX2 = fVal/(numel(dat.x)-Nv+1);
-end
-stat.msg = stat0.message;
-stat.Rsq = [];
-stat.sigY = [];
-stat.corrP = [];
-stat.cvgHst = [];
-stat.iterations = stat0.iterations;
-stat.funcCount = stat0.funcCount;
-stat.algorithm = stat0.algorithm;
-stat.exitFlag = exitFlag;
-stat.param = param;
-
-if isempty(dat)
- stat.func = func;
-else
- stat.func = func0;
-end
-
-end % mainline end
-
-% ======================================
-function xtrans = xtransform(x,p)
-% converts unconstrained variables into their original domains
-
-xtrans = x*0;
-% k allows some variables to be fixed, thus dropped from the
-% optimization.
-k=1;
-for i = 1:p.Np
- switch p.BoundClass(i)
- case 1
- % lower bound only
- xtrans(i) = p.lb(i) + x(k).^2;
-
- k=k+1;
- case 2
- % upper bound only
- xtrans(i) = p.ub(i) - x(k).^2;
-
- k=k+1;
- case 3
- % lower and upper bounds
- xtrans(i) = (sin(x(k))+1)/2;
- xtrans(i) = xtrans(i)*(p.ub(i) - p.lb(i)) + p.lb(i);
- % just in case of any floating point problems
- xtrans(i) = max(p.lb(i),min(p.ub(i),xtrans(i)));
-
- k=k+1;
- case 4
- % fixed variable, bounds are equal, set it at either bound
- xtrans(i) = p.lb(i);
- case 0
- % unconstrained variable.
- xtrans(i) = x(k);
-
- k=k+1;
- end
+ % setup output struct
+ stat.sigP = [];
+ stat.Rsq = [];
+ stat.sigY = [];
+ stat.corrP = [];
+ stat.cvgHst = [];
+ stat.algorithm = 'Nelder-Mead simplex direct search';
+ stat.func = cost_func_wrap.cost_func;
+ stat.param = param;
+ stat.param.Np = cost_func_wrap.get_num_free_parameters();
+ stat.msg = fit_stat.message;
+ stat.iterations = fit_stat.iterations;
+ stat.funcCount = fit_stat.funcCount;
+ stat.exitFlag = exitFlag;
+ stat.p = pOpt;
+ if isempty(dat)
+ stat.redX2 = fVal;
+ else
+ % divide R2 with the statistical degrees of freedom
+ stat.redX2 = fVal/(numel(dat.x)-stat.param.Np+1);
+ end
+
end
-end % sub function xtransform end
\ No newline at end of file
diff --git a/swfiles/+swplot/logo.m b/swfiles/+swplot/logo.m
index 92af4fb3c..85ea5e2d0 100644
--- a/swfiles/+swplot/logo.m
+++ b/swfiles/+swplot/logo.m
@@ -100,9 +100,9 @@ function logo(varargin)
txt0 = sprintf([ver0 '\nWritten by:\n S' char(225) 'ndor T' char(243) ...
'th\n sandor.toth@psi.ch\n Paul Scherrer Institut\n\nContributed:\n Simon Ward\n Mechthild Enderle\n Bj' char(246) 'rn F' ...
- char(229) 'k\n Duc Manh Lee\n\n' ...
- 'Icluding contributions from many authors through\nMatlab File Exchange:\n'...
- ' fminsearchnd\n eigenshuffle\n fireprint\n\n'...
+ char(229) 'k\n Duc Manh Le\n Rebecca Fair\n\n' ...
+ 'Including contributions from many authors through\nMatlab File Exchange:\n'...
+ ' fminsearchnd\n fireprint\n\n'...
'GNU General Public License\n'...
'You may copy, distribute and modify\nthe software as long as you '...
'track changes/dates\nin source files. Any modifications to or '...
@@ -199,4 +199,4 @@ function logo(varargin)
jFrame_fHGxClient.getWindow.setAlwaysOnTop(IsOnTop);
end
-end
\ No newline at end of file
+end
diff --git a/swfiles/+swplot/patchfacefcn.m b/swfiles/+swplot/patchfacefcn.m
index 6a36e2cb6..7319002be 100644
--- a/swfiles/+swplot/patchfacefcn.m
+++ b/swfiles/+swplot/patchfacefcn.m
@@ -141,6 +141,9 @@
T = M(1:3,4)';
R = M(1:3,1:3);
P = (P-T)*R;
+if ~verLessThan('matlab','9.4')
+ P = (P-T)*R;
+end
% shift the origin to the first vertex of every triangle
E1 = V(obj.Faces(:,2),:)-V(obj.Faces(:,1),:);
diff --git a/swfiles/+swplot/plotmag.m b/swfiles/+swplot/plotmag.m
index cd3f24797..e46f490f0 100644
--- a/swfiles/+swplot/plotmag.m
+++ b/swfiles/+swplot/plotmag.m
@@ -208,6 +208,13 @@
setappdata(hFigure,'base',obj.basisvector);
end
+if obj.symbolic
+ warning('plotmag:symbolic','A symbolic magnetic structure can not be plotted.')
+ varargout = cell(1:nargout);
+ return
+end
+
+
% the basis vectors in columns.
BV = obj.basisvector;
@@ -325,8 +332,14 @@
end
% normalize the longest moment vector to scale*(shortest bond length)
- M = M/sqrt(max(sum(M.^2,1)))*param.scale*lBond;
-
+ if isa(M,'sym')
+ warning('sw_plotmag:symcalc','This is a symbolic calculaiton, normalisation can''t be achieved.')
+ temp = cellfun(@(x) M./sqrt(sum(x,1).^2),mat2cell(M,3,[1 1 1 1]),'UniformOutput',false);
+ [m, ind] = min(cellfun(@(x) sum(x(:)),temp));
+ M = temp{ind}*param.scale*lBond;
+ else
+ M = M/sqrt(max(sum(M.^2,1)))*param.scale*lBond;
+ end
if param.centered
% double the length for centered moments
M = 2*M;
@@ -350,7 +363,7 @@
end
% shift positions
- vpos = bsxfun(@plus,vpos,BV\param.shift);
+ vpos = bsxfunsym(@plus,vpos,BV\param.shift);
% prepare legend labels
mAtom.name = obj.unit_cell.label(mAtom.idx);
diff --git a/swfiles/+swsym/generator.m b/swfiles/+swsym/generator.m
index 1d64aaef7..453711c5b 100644
--- a/swfiles/+swsym/generator.m
+++ b/swfiles/+swsym/generator.m
@@ -127,7 +127,11 @@
end
fclose(fid);
if symIdx == 0
- error('generator:WrongInput','Symmetry name does not exists (case insensitive)!');
+ error('generator:WrongInput', ...
+ ['The symmetry name ' symName 'is not recognised by ' ...
+ 'SpinW. Available symmetry symbols can be found in ' ...
+ strrep(symPath, '\', '\\')]);
+ % Windows paths don't print correctly with single \
end
symNum = symIdx;
symStr = textLine(20:end);
@@ -191,7 +195,7 @@
nSign = -1;
elseif symStr(ii)=='+'
nSign = 1;
- elseif (symStr(ii)=='1')||(symStr(ii)=='2')||(symStr(ii)=='3')
+ elseif ~isnan(str2double(symStr(ii)))
symOp(nNew,4,nOp) = (symStr(ii)-'0')/(symStr(ii+2)-'0');
ii = ii+2;
end
diff --git a/swfiles/+swsym/operator.m b/swfiles/+swsym/operator.m
index f0eb328d6..5b6261797 100644
--- a/swfiles/+swsym/operator.m
+++ b/swfiles/+swsym/operator.m
@@ -88,7 +88,7 @@
idxR = permute(sumn(abs(bsxfun(@minus,symOp(:,1:3,:),RS)),[1 2]),[3 1 2]);
idxT = permute(any(bsxfun(@minus,symOp(:,4,:),TS),1),[3 1 2]);
-
+
% adds new operator to the list if it differs from all
if all(idxR | idxT)
symOp = cat(3,symOp,[RS TS]);
diff --git a/swfiles/@cif/importcif.m b/swfiles/@cif/importcif.m
index d419a1d3e..954eeb731 100644
--- a/swfiles/@cif/importcif.m
+++ b/swfiles/@cif/importcif.m
@@ -72,7 +72,7 @@
isfirst = true;
else
if isfirst
- cifStr2{end} = [cifStr2{end} ' '' ' cifStr{ii}(2:end)];
+ cifStr2{end} = [strtrim(cifStr2{end}) ' '' ' strtrim(cifStr{ii}(2:end))];
else
if all(ii~=lastBL)
cifStr2{end} = [cifStr2{end} '\n' cifStr{ii}];
@@ -229,11 +229,7 @@
% COMMENT
if strin(1) == '#'
- endcomIdx = find(strin(1:end)=='?',1,'first');
- if isempty(endcomIdx)
- endcomIdx = numel(strin)+1;
- end
-
+ endcomIdx = numel(strin)+1;
strout{1,end+1} = 'comment';
strout{2,end} = strin(2:(endcomIdx-1));
strin = strin(endcomIdx:end);
@@ -242,7 +238,7 @@
% VARIABLE
if strin(1) == '_'
- whiteIdx = find(strin(1:end)==' ',1,'first');
+ whiteIdx = find(isspace(strin),1,'first');
if isempty(whiteIdx)
whiteIdx = numel(strin)+1;
end
@@ -277,7 +273,7 @@
bracket1Idx = find(strin(1:end)=='(',1,'first');
bracket2Idx = find(strin(1:end)==')',1,'first');
- whiteIdx = find(strin(1:end)==' ',1,'first');
+ whiteIdx = find(isspace(strin),1,'first');
if isempty(bracket1Idx)
bracket1Idx = numel(strin)+1;
end
@@ -314,7 +310,7 @@
% EMPTY NUMBER
if strin(1) == '?'
- whiteIdx = find(strin(1:end)==' ',1,'first');
+ whiteIdx = find(isspace(strin),1,'first');
strout{1,end+1} = 'number';
strout{2,end} = NaN;
strin = strin((whiteIdx+1):end);
@@ -329,7 +325,7 @@
% STRING
if (isstrprop(strin(1), 'alpha')) || true
- whiteIdx = find(strin(1:end)==' ',1,'first');
+ whiteIdx = find(isspace(strin),1,'first');
if isempty(whiteIdx)
whiteIdx = numel(strin)+1;
end
diff --git a/swfiles/@spinw/addaniso.m b/swfiles/@spinw/addaniso.m
index 676211bb4..0bc284b70 100644
--- a/swfiles/@spinw/addaniso.m
+++ b/swfiles/@spinw/addaniso.m
@@ -22,7 +22,7 @@ function addaniso(obj, matrixIdx, varargin)
%
% ```
% >>cryst = spinw
-% >>cryst.genlattice('lat_const',[4 4 3],'spgr','P 4')
+% >>cryst.genlattice('lat_const',[4 4 3],'sym','P 4')
% >>cryst.addatom('r',[1/4 1/4 1/2],'S',1)
% >>cryst.addmatrix('label','A1','value',diag([-0.1 0 0]))
% >>cryst.gencoupling
@@ -93,7 +93,7 @@ function addaniso(obj, matrixIdx, varargin)
if nargin > 3
atomIdx = varargin{2};
- if obj.lattice.sym > 1
+ if size(obj.lattice.sym, 3) > 1
error('spinw:addaniso:SymmetryProblem','atomIdx is not allowed when crystal symmetry is higher than P1!');
end
@@ -105,7 +105,7 @@ function addaniso(obj, matrixIdx, varargin)
atomTypeIdx = varargin{1};
if length(obj.single_ion.aniso) ~= nMagAtom
- addField.aniso = zeros(nMagAtom,1);
+ addField.aniso = zeros(1,nMagAtom);
end
% select atoms by label
@@ -131,7 +131,7 @@ function addaniso(obj, matrixIdx, varargin)
end
atomTypeIdx = find(isSelectedAtom);
end
-
+
for ii = 1:length(atomTypeIdx)
aTemp = addField.aniso(mAtom.idx == atomTypeIdx(ii));
if nargin > 3
diff --git a/swfiles/@spinw/addatom.m b/swfiles/@spinw/addatom.m
index 73336c635..001d68eff 100644
--- a/swfiles/@spinw/addatom.m
+++ b/swfiles/@spinw/addatom.m
@@ -47,7 +47,8 @@ function addatom(obj, varargin)
% number is guessed from the given label of the atom. For example if
% `label` is `MCr3+` or `Cr3+` then the $S=3/2$ high spin state is
% assumed for Cr$^{3+}$. The spin values for every ion is stored in the
-% [magion.dat] file. If the atom type is unknown $S=0$ is assumed.
+% [magion.dat] file. If the atom type is unknown $S=0$ is assumed. Only
+% positive S are allowed.
%
% `color`
% : RGB color of the atoms for plotting stored in a matrix with dimensions
@@ -86,10 +87,11 @@ function addatom(obj, varargin)
% mixture of isotopes.
%
% `bn`
-% : Neutron scattering length, given as double. Not implemented yet.
+% : Neutron scattering length, given as double.
%
% `bx`
-% : X-ray scattering length.
+% : X-ray scattering length, given as double. Not yet implmented, this
+% input will be ignored.
%
% `biso`
% : Isotropic displacement factors in units of \\ang$^2$.
@@ -124,18 +126,37 @@ function addatom(obj, varargin)
inpForm.fname = [inpForm.fname {'bx' 'formfactn' 'formfactx' 'b' 'formfact' 'A' 'Z' }];
inpForm.defval = [inpForm.defval {[] [] [] [] [] [] [] }];
-inpForm.size = [inpForm.size {[1 -7] [-8 -9] [-8 -9] [1 -7] [-8 -9] [1 -7] [1 -7]}];
+inpForm.size = [inpForm.size {[1 -7] [-1 -9] [-1 -10] [1 -7] [-8 -9] [1 -7] [1 -7]}];
inpForm.soft = [inpForm.soft {true true true true true true true }];
newAtom = sw_readparam(inpForm, varargin{:});
+if any(sw_sub1(newAtom.S) < 0)
+ error('spinw:addatom:WrongInput','Require S>=0');
+end
+
if isempty(newAtom.formfactn)
newAtom.formfactn = newAtom.formfact;
end
if isempty(newAtom.bn)
newAtom.bn = newAtom.b;
+else
+ warning('spinw:addatom:DeprecationWarning', ...
+ 'bn is deprecated please use b instead.');
+ if ~isempty(newAtom.b)
+ warning('spinw:addatom:WrongInput', ...
+ ['Both b and bn have been provided - note that bn will ',...
+ 'be used for the neutron scattering length (b will be ignored).']);
+ end
+end
+if ~isempty(newAtom.bx)
+ warning('spinw:addatom:DeprecationWarning', ...
+ ['X-ray scattering length is not currently supported and will ' ...
+ 'be deprecated in the next release. Note that the input ' ...
+ 'provided for bx will be ignored.']);
end
+
if ~any(size(newAtom.r)-[1 3])
newAtom.r = newAtom.r';
end
@@ -324,7 +345,7 @@ function addatom(obj, varargin)
newAtom.formfactx = {newAtom.formfactx};
end
if iscell(newAtom.formfactx)
- [~,newAtom.ffx] = sw_cff(newAtom.fromfactx);
+ [~,newAtom.ffx] = sw_cff(newAtom.formfactx);
%newAtom.ffx = permute(newAtom.ffx,[3 2 1]);
elseif ~isempty(newAtom.formfactx)
newAtom.ffx = newAtom.formfactx;
@@ -339,8 +360,11 @@ function addatom(obj, varargin)
newAtom.b = ones(2,nNewAtom);
% get neutron scattering length
-newAtom.b(1,:) = sw_nb(newAtom.label);
-
+if isempty(newAtom.bn)
+ newAtom.b(1,:) = sw_nb(newAtom.label);
+else
+ newAtom.b(1,:) = newAtom.bn;
+end
newAtom.Z = int32(newAtom.Z);
@@ -409,4 +433,4 @@ function addatom(obj, varargin)
warning('spinw:addatom:WrongInput','Occupancy on some site is larger than 1!')
end
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/addcoupling.m b/swfiles/@spinw/addcoupling.m
index 6eca0a3e8..c851b114a 100644
--- a/swfiles/@spinw/addcoupling.m
+++ b/swfiles/@spinw/addcoupling.m
@@ -97,6 +97,11 @@ function addcoupling(obj, varargin)
param.mat = find(ismember(obj.matrix.label,param.mat));
end
+if isempty(param.mat)
+ error('spinw:addcoupling:WrongMatrixLabel', ...
+ 'The selected matrix label does not exists!')
+end
+
if isempty(param.type)
param.type = 0*param.mat;
end
@@ -113,13 +118,18 @@ function addcoupling(obj, varargin)
param.type(qSel) = 0;
param.type(bqSel) = 1;
if any(isnan(param.type))
- error(['spinw:addcoupling:WrongInput','Wrong coupling type, '...
+ error('spinw:addcoupling:WrongInput',['Wrong coupling type, '...
'currently only quadratic and biquadratic exchanges '...
'are supported!']);
end
end
+if param.type == 1 && sw_mattype(obj.matrix.mat(:,:,param.mat))~=1
+ error('spinw:addcoupling:WrongInput', ...
+ 'Biquadratic exchange matrix has to be isotropic!');
+end
+
if isempty(param.sym)
nosympar = true;
if isempty(param.subIdx)
@@ -140,21 +150,40 @@ function addcoupling(obj, varargin)
'provided for each input matrix!'])
end
-% select atoms
-if ~isnumeric(param.atom)
- if ~iscell(param.atom)
- param.atom = {param.atom};
+if ~isempty(param.atom)
+ % select atoms based on label or index
+ if ~isnumeric(param.atom)
+ % atom labels provided - convert to index
+ if ~iscell(param.atom)
+ % single label provided as string - convert to cell for consistency
+ param.atom = {param.atom};
+ end
+ % replace label with index (note can have more than one atom with
+ % the same label)
+ param.atom = cellfun(@(label) find(strcmp(obj.unit_cell.label, label)), ...
+ param.atom, 'UniformOutput', false); % outputs cell
+ if any(cellfun(@(idx) isempty(idx), param.atom))
+ error('spinw:addcoupling:WrongInput', 'Atom label does not exist in unit cell.');
+ end
+ else
+ % indices provided - convert to a cell if not already
+ if ~iscell(param.atom)
+ param.atom = num2cell(param.atom);
+ end
+ % check that all are present in matom
+ if ~all(cellfun(@(idx) any(obj.matom.idx==idx), param.atom))
+ error('spinw:addcoupling:WrongInput', 'Atom index does not correspond to a valid magentic atom.');
+ end
end
if numel(param.atom)>2
- error('spinw:addcoupling:WrongInput','Only two different atom label can be given at once!');
- end
- %aIdx1 = find(ismember(obj.unit_cell.label,param.atom{1}));
- aIdx1 = find(cellfun(@(C)~isempty(C),strfind(obj.unit_cell.label,param.atom{1})));
- if numel(param.atom)>1
- %aIdx2 = find(ismember(obj.unit_cell.label,param.atom{2}));
- aIdx2 = find(cellfun(@(C)~isempty(C),strfind(obj.unit_cell.label,param.atom{2})));
+ error('spinw:addcoupling:WrongInput','A maximum of 2 atom labels can be provided.');
else
- aIdx2 = aIdx1;
+ aIdx1 = param.atom{1}; % atom 1 index in matom.idx
+ if numel(param.atom)>1
+ aIdx2 = param.atom{2}; % atom 2 index in matom.idx
+ else
+ aIdx2 = aIdx1;
+ end
end
end
@@ -204,8 +233,7 @@ function addcoupling(obj, varargin)
end
if isempty(idx)
- warning('spinw:addcoupling:NoBond','No matrix assigned, since no bond fulfilled the given conditions!')
- return
+ error('spinw:addcoupling:NoBond','No matrix assigned, since no bond fulfilled the given conditions!')
end
Jmod = obj.coupling.mat_idx(:,idx);
@@ -221,11 +249,6 @@ function addcoupling(obj, varargin)
'assigned on some coupling, duplicate assigments are removed!']);
end
-if isempty(param.mat)
- warning('spinw:addcoupling:WrongMatrixLabel','The selected matrix label does not exists!')
- return
-end
-
if any(Jmod(3,:))
error('spinw:addcoupling:TooManyCoupling',['The maximum '...
'number of allowed couplings (3) per bond is reached!']);
diff --git a/swfiles/@spinw/addg.m b/swfiles/@spinw/addg.m
index 86007bb92..641e80f6a 100644
--- a/swfiles/@spinw/addg.m
+++ b/swfiles/@spinw/addg.m
@@ -21,7 +21,7 @@ function addg(obj, matrixIdx, varargin)
%
% ```
% >>cryst = spinw
-% >>cryst.genlattice('lat_const',[4 4 3],'spgr','P 4')
+% >>cryst.genlattice('lat_const',[4 4 3],'sym','P 4')
% >>cryst.addatom('r',[1/4 1/4 1/2],'S',1)
% >>cryst.addmatrix('label','g_1','value',diag([2 1 1]))
% >>cryst.gencoupling
@@ -128,9 +128,19 @@ function addg(obj, matrixIdx, varargin)
error('spinw:addg:WrongCouplingTypeIdx','Input matrix does not exists!');
end
+% check matrix has the correct properties for a valid g-tensor
+% see Eq. 15.38 in Abragam and Bleaney (1970) EPR of Transition Ions
+gtensor = obj.matrix.mat(:, :, matrixIdx);
+G = gtensor' * gtensor;
+eigvals = eig(G);
+if ~issymmetric(G) || ~all(eigvals > 10*eps)
+ error('spinw:addg:InvalidgTensor', ...
+ 'Check input matrix - g*.g must be symmeteric positive-definite');
+end
+
if nargin > 3
atomIdx = varargin{2};
- if obj.lattice.sym > 1
+ if size(obj.lattice.sym, 3) > 1
error('spinw:addg:SymmetryProblem','atomIdx is not allowed when crystal symmetry is not P1!');
end
@@ -142,7 +152,7 @@ function addg(obj, matrixIdx, varargin)
atomTypeIdx = varargin{1};
if length(obj.single_ion.g) ~= nMagAtom
- addField.g = zeros(nMagAtom,1);
+ addField.g = zeros(1, nMagAtom, 'int32');
end
% select atoms by label
@@ -170,10 +180,9 @@ function addg(obj, matrixIdx, varargin)
end
else
- addField.g = zeros(1,nMagAtom) + matrixIdx;
+ addField.g = zeros(1, nMagAtom, 'int32') + matrixIdx;
end
-addField.g = int32(addField.g);
obj.single_ion = addField;
end
\ No newline at end of file
diff --git a/swfiles/@spinw/addmatrix.m b/swfiles/@spinw/addmatrix.m
index 4badf070b..f68ad5d48 100644
--- a/swfiles/@spinw/addmatrix.m
+++ b/swfiles/@spinw/addmatrix.m
@@ -46,7 +46,8 @@ function addmatrix(obj, varargin)
%
% `'label'`
% : Label string for plotting default value is `'matI'`, where $I$ is the index
-% of the matrix.
+% of the matrix. Add '-' to the end of the label to plot bond as dashed
+% line/cylinder.
%
% `'color'`
% : Color for plotting, either row vector
@@ -80,8 +81,11 @@ function addmatrix(obj, varargin)
newMat = sw_readparam(inpForm, varargin{:});
if ~isnumeric(newMat.value) && ~isa(newMat.value,'sym')
- warning('spinw:addmatrix:WrongInput','Matrix value has to be numeric or symbolic variable!')
- return
+ error('spinw:addmatrix:WrongInput','Matrix value has to be numeric or symbolic variable!')
+end
+
+if isnumeric(newMat.value)
+ newMat.value = double(newMat.value);
end
if ~isempty(newMat.value)
@@ -206,4 +210,4 @@ function addmatrix(obj, varargin)
%spinw.validate(obj);
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/addtwin.m b/swfiles/@spinw/addtwin.m
index 3f2395606..38e4cd506 100644
--- a/swfiles/@spinw/addtwin.m
+++ b/swfiles/@spinw/addtwin.m
@@ -87,17 +87,23 @@ function addtwin(obj,varargin)
[~, param.rotC(:,:,ii)] = sw_rot(param.axis,param.phi(ii));
end
else
+ % rotC matrix explicitly provided
nTwin = size(param.rotC,3);
+ for itwin = 1:nTwin
+ % check valid rotation or reflection (|R|=1 and R.T = R^-1)
+ twin_rotc = param.rotC(:,:,itwin);
+ if abs(det(twin_rotc))~=1 || ~isequal(twin_rotc*(twin_rotc'), eye(3))
+ error('spinw:addtwin:WrongInput', ...
+ 'rotC matrix for twin %d must be a valid rotation', itwin);
+ end
+ end
+
end
if size(param.vol,2)>cryst = spinw
% >>opStr = 'x+1/2,y+1/2,z;x+1/2,y,z+1/2;x,y+1/2,z+1/2';
-% >>cryst.genlattice('lat_const',[8 8 8],'spgr',opStr,'label','FCC')
+% >>cryst.genlattice('lat_const',[8 8 8],'sym',opStr,'label','FCC')
% >>cryst.addatom('r',[0 0 0],'label','Atom1')
% >>atomList = cryst.atom
% >>atomList.r>>
diff --git a/swfiles/@spinw/basisvector.m b/swfiles/@spinw/basisvector.m
index 33731a4b6..04353f5a5 100644
--- a/swfiles/@spinw/basisvector.m
+++ b/swfiles/@spinw/basisvector.m
@@ -78,8 +78,10 @@
angSym = sym([0 0 0]);
angSym(specIdx) = sym(round(ang(specIdx)*180/pi))*pi/180;
- symAng0 = [sym('alpha','positive') sym('beta','positive') sym('gamma','positive')];
- angSym(~specIdx) = symAng0(~specIdx);
+ if any(~specIdx)
+ symAng0 = [sym('alpha','positive') sym('beta','positive') sym('gamma','positive')];
+ angSym(~specIdx) = symAng0(~specIdx);
+ end
alpha = angSym(1);
beta = angSym(2);
@@ -104,4 +106,4 @@
basisVector = basisVector*diag(obj.lattice.lat_const);
end
-end
\ No newline at end of file
+end
diff --git a/dev/@spinw/couplingtable.m b/swfiles/@spinw/couplingtable.m
similarity index 100%
rename from dev/@spinw/couplingtable.m
rename to swfiles/@spinw/couplingtable.m
diff --git a/swfiles/@spinw/energy.m b/swfiles/@spinw/energy.m
index c9453f410..06594f866 100644
--- a/swfiles/@spinw/energy.m
+++ b/swfiles/@spinw/energy.m
@@ -1,21 +1,21 @@
function Eout = energy(obj, varargin)
% calculates the ground state energy
-%
+%
% ### Syntax
-%
+%
% `E = energy(obj,Name,Value)`
-%
+%
% ### Description
-%
+%
% `E = energy(obj,Name,Value)` calculates the classical ground state energy
% per spin. The calculation correctly takes into account the magnetic
% supercell. The function gives correct results on single-k magnetic
% structures even defined on magnetic supercells. For multi-k magnetic
% structures first a definition of a larger supercell is necessary where an
% effective $k=0$ representation is possible.
-%
+%
% ### Examples
-%
+%
% After optimising the magnetic structure (by minimizing the ground state
% energy), the energy per spin is calculated. This can be compared to
% different ground state structures to decide which is the right classical
@@ -31,20 +31,20 @@
% >>cryst.optmagsteep('nRun',10)
% >>cryst.energy>>
% ```
-%
+%
% ### Input Arguments
-%
+%
% `obj`
% : [spinw] object.
-%
+%
% ### Name-Value Pair Arguments
-%
+%
% `'epsilon'`
-% : The smallest value of incommensurability that is tolerated
+% : The smallest value of incommensurability that is tolerated
% without warning. Default is $10^{-5}$.
-%
+%
% ### Output Arguments
-%
+%
% `E`
% : Energy per moment (anisotropy + exchange + Zeeman energy).
%
@@ -59,9 +59,9 @@
% ion anisotropy one has to be carefull! In the triangular case one has to
% extend the unit cell to `nExt = [3 3 1]` (in the hexagonal setting), in
% this case the energy will be correct.}}
-%
+%
% ### See Also
-%
+%
% [spinw] \| [spinw.anneal] \| [spinw.newcell]
%
@@ -103,7 +103,7 @@
end
if nMagExt>0
-
+
dR = [SS.all(1:3,:) zeros(3,nMagExt)];
atom1 = [SS.all(4,:) 1:nMagExt];
atom2 = [SS.all(5,:) 1:nMagExt];
@@ -127,8 +127,19 @@
Ml = repmat(permute(M1,[1 3 2]),[1 3 1]);
Mr = repmat(permute(M2,[3 1 2]),[3 1 1]);
-
- exchE = sum(sum(sum(Ml.*JJ.*Mr,3),2),1);
+
+ Q = Ml.*JJ.*Mr;
+ if isa(Ml,'sym') || isa(Ml,'sym') || isa(Ml,'sym')
+ [n1, n2, n3] = size(Q);
+ sum_3 = 0;
+ for k = 1:n3
+ sum_3 = sum_3 + Q(:,:,k);
+ end
+ else
+ sum_3 = sum(Q,3);
+ end
+ exchE = sum(sum(sum_3,2),1);
+
% TODO
% correct energy for twins
ZeemanE = -sum(SI.field*permute(mmat(SI.g,permute(M0,[1 3 2])),[1 3 2]),2)*obj.unit.muB;
diff --git a/swfiles/@spinw/fitspec.m b/swfiles/@spinw/fitspec.m
index ddf37181e..1e55545d0 100644
--- a/swfiles/@spinw/fitspec.m
+++ b/swfiles/@spinw/fitspec.m
@@ -89,7 +89,11 @@
%
% `'imagChk'`
% : Checks that the imaginary part of the spin wave dispersion is
-% smaller than the energy bin size. Default is `true`.
+% smaller than the energy bin size.
+% If false, will not check
+% If 'penalize' will apply a penalty to iterations that yield imaginary modes
+% If true, will stop the fit if an iteration gives imaginary modes
+% Default is `penalize`.
%
% Parameters for visualizing the fit results:
%
@@ -179,8 +183,8 @@
inpForm.soft = [inpForm.soft {false false false false false }];
inpForm.fname = [inpForm.fname {'maxiter' 'sw' 'optmem' 'tid' 'imagChk'}];
-inpForm.defval = [inpForm.defval {20 1 0 tid0 true }];
-inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] [1 1] [1 1] }];
+inpForm.defval = [inpForm.defval {20 1 0 tid0 'penalize'}];
+inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] [1 1] [1 -8] }];
inpForm.soft = [inpForm.soft {false false false false false }];
param = sw_readparam(inpForm, varargin{:});
@@ -212,6 +216,15 @@
param0.plot = false;
param0.tid = 0;
+% Parses the imagChk parameter - we need to set it to true/false for spinw.spinwave()
+if strncmp(param.imagChk, 'penalize', 1)
+ param0.imagChk = true;
+ param0.penalize_imag = true;
+else
+ param0.imagChk = logical(param.imagChk(1));
+ param0.penalize_imag = false;
+end
+
x = zeros(nRun,nPar);
redX2 = zeros(nRun,1);
@@ -251,13 +264,13 @@
[x(idx,:),~, output(idx)] = ndbase.pso(dat,@(x,p)spec_fitfun(obj, data, param.func, p, param0),x0,'lb',param.xmin,'ub',param.xmax,...
'TolX',param.tolx,'TolFun',param.tolfun,'MaxIter',param.maxiter);
- redX2(idx) = output.redX2;
+ redX2(idx) = output(idx).redX2;
case 'simplex'
[x(idx,:),~, output(idx)] = ndbase.simplex(dat,@(x,p)spec_fitfun(obj, data, param.func, p, param0),x0,'lb',param.xmin,'ub',param.xmax,...
'TolX',param.tolx,'TolFun',param.tolfun,'MaxIter',param.maxiter);
- redX2(idx) = output.redX2;
+ redX2(idx) = output(idx).redX2;
case 'lm'
% does not work due to the binning of the spectrum
@@ -330,7 +343,6 @@
param.nPoints = 50;
-
parfunc(obj,x);
% Number of different correlation functions measured.
@@ -351,7 +363,19 @@
% calculate neutron scattering cross section
spec = sw_neutron(spec,'n',data.n,'pol',data.corr.type{1}(1) > 1);
% bin the data along energy
- spec = sw_egrid(spec,'component',data.corr,'Evect',param.Evect,'imagChk',param.imagChk);
+ try
+ spec = sw_egrid(spec,'component',data.corr,'Evect',param.Evect,'imagChk',param.imagChk);
+ catch ex
+ % If we get imaginary modes, set all bins to zero to get high Rw
+ if strcmp(ex.identifier, 'egrid:BadSolution') && param.penalize_imag
+ spec.Evect = param.Evect;
+ spec.swConv = zeros(numel(spec.Evect)-1, size(spec.Sperp, 2));
+ spec.swInt = spec.Sperp;
+ spec.component = 'Sperp';
+ else
+ rethrow(ex);
+ end
+ end
% generate center bin
spec.Evect = (spec.Evect(1:(end-1))+spec.Evect(2:end))/2;
@@ -424,7 +448,7 @@
if param.plot
text(0.05,0.9,['x = [' sprintf('%6.4f ',x) sprintf(']\nRw = %6.4f',sqrt(R))],'Units','normalized','fontsize',12);
axis([0.5 Qc+0.5 param.Evect(1) param.Evect(end)]);
- legend(pHandle(1:2),'simulation','data')
+ legend(pHandle([1, length(simE)+2]),'simulation','data')
xlabel('Scan index')
ylabel('Energy transfer (meV)')
title('Spin wave dispersion fit')
@@ -487,4 +511,4 @@
points = R*(a*cos(phi)+b*sin(phi))+repmat(r0,1,N);
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/formula.m b/swfiles/@spinw/formula.m
index 633b627d9..68bda0b03 100644
--- a/swfiles/@spinw/formula.m
+++ b/swfiles/@spinw/formula.m
@@ -13,11 +13,11 @@
% ### Examples
%
% The formula of the crystal stored in the
-% [https://goo.gl/do6oTh](https://goo.gl/do6oTh) linked file will be
+% [https://raw.githubusercontent.com/SpinW/Models/master/cif/Ca2RuO4.cif](https://raw.githubusercontent.com/SpinW/Models/master/cif/Ca2RuO4.cif) linked file will be
% printed onto the Command Window.
%
% ```
-% >>cryst = spinw('https://goo.gl/do6oTh')
+% >>cryst = spinw('https://raw.githubusercontent.com/SpinW/Models/master/cif/Ca2RuO4.cif')
% >>cryst.formula>>
% ```
%
diff --git a/swfiles/@spinw/fourier.m b/swfiles/@spinw/fourier.m
index dd2a12fc8..464956cca 100644
--- a/swfiles/@spinw/fourier.m
+++ b/swfiles/@spinw/fourier.m
@@ -214,7 +214,10 @@
atom2 = param.sublat(atom2);
nMag = max(param.sublat);
nMag0 = numel(obj.matom.idx);
+ nsubl = nMag / nMag0;
fprintf0(fid,'Remapping magnetic atoms into a new set of sublattices...\n');
+else
+ nsubl = 1;
end
% number of magnetic atoms in the magnetic supercell
@@ -289,11 +292,11 @@
% save results in a struct
% scale ft with the number of sublattices
-res.ft = ft*(nMag/nMag0);
+res.ft = ft * nsubl;
res.hkl = hkl;
% Heisenberg output
res.isiso = size(ft,1)==1;
fprintf0(fid,'Calculation finished.\n');
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/gencoupling.m b/swfiles/@spinw/gencoupling.m
index 26517a0d3..32faf12ac 100644
--- a/swfiles/@spinw/gencoupling.m
+++ b/swfiles/@spinw/gencoupling.m
@@ -58,6 +58,11 @@ function gencoupling(obj, varargin)
% equivalent, default value is $10^{-3}$\\ang. Only used, when no
% space group is defined.
%
+% `'tolMaxDist'`
+% : Tolerance added to maxDistance to ensure bonds between same atom in
+% neighbouring unit cells are included when maxDistance is equal to a
+% lattice parameter.
+%
% `'dMin'`
% : Minimum bond length, below which an error is triggered.
% Default value is 0.5 \\ang.
@@ -84,7 +89,7 @@ function gencoupling(obj, varargin)
% is there any symmetry operator?
isSym = size(obj.lattice.sym,3) > 0;
-inpForm.fname = {'forceNoSym' 'maxDistance' 'tol' 'tolDist' 'dMin' 'maxSym' 'fid'};
+inpForm.fname = {'forceNoSym' 'maxDistance' 'tolMaxDist' 'tolDist' 'dMin' 'maxSym' 'fid'};
inpForm.defval = {false 8 1e-5 1e-3 0.5 [] -1 };
inpForm.size = {[1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] };
inpForm.soft = {false false false false false true false };
@@ -93,12 +98,32 @@ function gencoupling(obj, varargin)
param = sw_readparam(inpForm, varargin{:});
pref = swpref;
-tol = param.tol;
+if any([param.maxDistance, param.tolMaxDist, param.tolDist, param.dMin, param.maxSym] < 0)
+ error('spinw:gencoupling:NegativeDistances','All distances should be positive.');
+end
+
+tolMaxDist = param.tolMaxDist;
tolD = param.tolDist;
+if tolD > param.maxDistance
+ error('spinw:gencoupling:TolDist', ...
+ ['Tolerance on symmetrically equivalent bond lengths ' ...
+ 'larger than max distance.']);
+end
+
% to avoid some problem with symmetry if maxDistance equal to a lattice
% constant
-param.maxDistance = param.maxDistance + tol;
+if tolMaxDist > param.maxDistance
+ error('spinw:gencoupling:TolMaxDist', ...
+ 'Tolerance on max distance larger than max distance.');
+end
+param.maxDistance = param.maxDistance + tolMaxDist;
+
+if param.maxDistance < param.dMin
+ error('spinw:gencoupling:MaxDLessThanMinD', ...
+ 'maxDistance is smaller then dMin parameter');
+end
+
if isempty(param.maxSym)
param.maxSym = param.maxDistance;
@@ -130,7 +155,7 @@ function gencoupling(obj, varargin)
if fid ~= 0
fprintf0(fid,['Creating the bond list (maxDistance = %g ' obj.unit.label{1}...
- ', nCell = %dx%dx%d)...\n'],param.maxDistance-tol,nC);
+ ', nCell = %dx%dx%d)...\n'],param.maxDistance-tolMaxDist,nC);
end
% save the sym/nosym method into obj
@@ -193,65 +218,73 @@ function gencoupling(obj, varargin)
% cMat = sortrows(cMat',6)'; too slow
[~,cIdx] = sort(cMat(6,:));
cMat = cMat(:,cIdx);
-% cutoff at maximum distance
-cMat = cMat(:,cMat(6,:)<=param.maxDistance);
-% index the bonds
-cMat(7,:) = cumsum([1 diff(cMat(6,:))>tolD]);
-
-% check whether some atoms are too close
+% check cutoff for bond lengths
if cMat(6,1) < param.dMin
error('spinw:gencoupling:AtomPos',['Some atoms are too close (Dmin=' ...
num2str(cMat(6,1)) '<' num2str(param.dMin) '), check your crystal structure!']);
+elseif cMat(6,1) > param.maxDistance
+ error('spinw:gencoupling:maxDistance', ...
+ 'There are no bonds with distance < maxDistance');
end
+cMat = cMat(:,cMat(6,:)<=param.maxDistance);
+% index the bonds
+cMat(7,:) = cumsum([1 diff(cMat(6,:))>tolD]);
+
dRA = cMat(6,:);
% keep the bond length
cMat = cMat([1:5 7],:);
% cMat rows: [la, lb, lc, atom1, atom2, idx]
% symmetry equivalent couplings
+nSym = int32(0);
if isSym
- % store the final sorted couplings in nMat
- nMat = zeros(6,0);
- ii = 1;
- idx = 1;
- % maximum bond index for symmetry operations
- maxidxSym = max(cMat(6,dRA<=param.maxSym));
-
- while ii <= maxidxSym
- % select columns from sorM with a certain idx value
- sortMs = cMat(:,cMat(6,:) == ii);
- while size(sortMs,2)>0
- [genC, unC] = swsym.bond(mAtom.r, obj.basisvector, sortMs(:,1), obj.lattice.sym, tol);
- genCAll = [genC [-genC(1:3,:);genC([5 4],:)]];
- % remove from sortMs the identical couplings
- iNew = isnew(genCAll(1:5,:),sortMs(1:5,:),tol);
- sortMs(:,~iNew) = [];
- % remove identical couplings from the symmetry generated
- % list
- genC(:,~unC) = [];
- if sum(~iNew) ~= sum(unC)
- error('spinw:gencoupling:SymProblem','Symmetry error! ii=%d, idx=%d. Try to change ''tol'' parameter.',ii,idx);
+ idxSym = dRA<=param.maxSym;
+ if ~any(idxSym)
+ warning('spinw:gencoupling:maxSym',['No bonds with distances < '...
+ 'maxSym = ', num2str(param.maxSym), ' - all bonds will be ' ...
+ 'reduced to P0 symmetry.']);
+ else
+ % store the final sorted couplings in nMat
+ nMat = zeros(6,0);
+ ii = 1;
+ idx = 1;
+ % maximum bond index for symmetry operations
+ maxidxSym = max(cMat(6,idxSym));
+
+ while ii <= maxidxSym
+ % select columns from sorM with a certain idx value
+ sortMs = cMat(:,cMat(6,:) == ii);
+ while size(sortMs,2)>0
+ [genC, unC] = swsym.bond(mAtom.r, obj.basisvector, sortMs(:,1), obj.lattice.sym, tolD);
+ genCAll = [genC [-genC(1:3,:);genC([5 4],:)]];
+ % remove from sortMs the identical couplings
+ iNew = isnew(genCAll(1:5,:),sortMs(1:5,:), tolD);
+ sortMs(:,~iNew) = [];
+ % remove identical couplings from the symmetry generated
+ % list
+ genC(:,~unC) = [];
+ if sum(~iNew) ~= sum(unC)
+ error('spinw:gencoupling:SymProblem', ...
+ ['Symmetry error! ii=%d, idx=%d. Try to '...
+ 'change ''tol'' parameter.'], ii, idx)
+ end
+ % move the non-unique (not new) couplings (symmetry equivalent ones)
+ nMat = [nMat [genC;ones(1,size(genC,2))*idx]]; %#ok
+ idx = idx + 1;
end
- % move the non-unique (not new) couplings (symmetry equivalent ones)
- nMat = [nMat [genC;ones(1,size(genC,2))*idx]]; %#ok
- idx = idx + 1;
+ ii = ii + 1;
end
- ii = ii + 1;
+
+ % include the increase of bond index in case bonds are splitted due to
+ % symmetry inequivalency
+ cMat = cMat(:,cMat(6,:)>maxidxSym);
+ cMat(6,:) = cMat(6,:) + nMat(6,end)-maxidxSym;
+ cMat = [nMat cMat];
+
+ % save the value of maximum bond index that is generated by symmetry
+ nSym = int32(nMat(6,end));
end
-
- % include the increase of bond index in case bonds are splitted due to
- % symmetry inequivalency
- cMat = cMat(:,cMat(6,:)>maxidxSym);
- cMat(6,:) = cMat(6,:) + nMat(6,end)-maxidxSym;
- cMat = [nMat cMat];
-
- % save the value of maximum bond index that is generated by symmetry
- nSym = int32(nMat(6,end));
-
-else
- nSym = int32(0);
-
end
% default anisotropy and g-tensor values
diff --git a/swfiles/@spinw/genlattice.m b/swfiles/@spinw/genlattice.m
index e6f982b50..a4dc97823 100644
--- a/swfiles/@spinw/genlattice.m
+++ b/swfiles/@spinw/genlattice.m
@@ -26,9 +26,9 @@
% ### Example
%
% ```
-% crystal.genlattice('lat_const',[3 3 4],'angled',[90 90 120],'spgr','P 6')
-% crystal.genlattice('lat_const',[3 3 4],'angled',[90 90 120],'spgr',168)
-% crystal.genlattice('lat_const',[3 3 4],'angled',[90 90 120],'spgr','-y,x-y,z; -x,-y,z','label','R -3 m')
+% crystal.genlattice('lat_const',[3 3 4],'angled',[90 90 120],'sym','P 6')
+% crystal.genlattice('lat_const',[3 3 4],'angled',[90 90 120],'sym',168)
+% crystal.genlattice('lat_const',[3 3 4],'angled',[90 90 120],'sym','-y,x-y,z; -x,-y,z','label','R -3 m')
% ```
%
% The three lines are equivalent, both will create hexagonal lattice, with
@@ -51,7 +51,7 @@
% : `[a, b, c]` lattice parameters in units defined in [spinw.unit] (with \\ang
% being the default), dimensions are $[1\times 3]$.
%
-% `spgr`
+% `spgr` or 'sym'
% : Defines the space group. Can have the following values:
%
% * **space group label** string, name of the space group, can be any
@@ -68,10 +68,14 @@
% If the `spgr` option is 0, no symmetry will be used. The
% [spinw.gencoupling] function will determine the equivalent bonds based on
% bond length.
+%
+% Can also provide spacegroup and label (see below) in a cell e.g.
+% {'-x,y,-z', 'P 2'}
%
% `label`
% : Optional label for the space group if the generators are given in the
% `spgr` option.
+%
% `bv`
% : Basis vectors given in a matrix with dimensions of $[3\times 3]$, where
% each column defines a basis vector.
@@ -118,9 +122,64 @@
param = sw_readparam(inpForm, varargin{:});
-% new option, but keep the old one as well
+% input validation
if ~isempty(param.spgr)
- param.sym = param.spgr;
+ warning('spinw:genlattice:DeprecationWarning',...
+ ['spgr parameter name is being deprecated, please use sym',...
+ ' instead']);
+ if ~isempty(param.sym)
+ error('spinw:genlattice:WrongInput', ...
+ 'Both sym and spgr provided - note sym will be used.');
+ else
+ param.sym = param.spgr;
+ end
+end
+if any(strcmp('angled', varargin(1:2:end))) && ...
+ any(strcmp('angle', varargin(1:2:end)))
+ warning('spinw:genlattice:WrongInput', ...
+ 'Both angle and angled provided - angled will be used.');
+end
+if isempty(param.sym)
+ if norm(param.origin) > 1e-10
+ warning('spinw:genlattice:WrongInput', ...
+ ['Origin provided without symmetry/spacegroup (both required in ',...
+ 'same function call) - it will be ignored.']);
+ end
+ if ~strcmp(param.perm, 'abc')
+ warning('spinw:genlattice:WrongInput', ...
+ ['Perm provided without symmetry/spacegroup (both required in ',...
+ 'same function call) - it will be ignored.']);
+ end
+else
+ % check valid perm
+ invalid_perm_msg = ['Invalid permutation supplied - it must be a ', ...
+ 'string or numeric array that is a permutation of [1,2,3] or ',...
+ 'abc respectively.'];
+ if ischar(param.perm)
+ param.perm = param.perm-'a'+1; % casts to int array e.g. [1,2,3]
+ elseif ~isnumeric(param.perm)
+ error('spinw:genlattice:WrongInput', invalid_perm_msg);
+ end
+ % check param.perm is a permutation of [1,2,3]
+ if ~any(ismember(perms([1,2,3]), param.perm, 'rows'))
+ error('spinw:genlattice:WrongInput', invalid_perm_msg);
+ end
+ % check valid origin
+ if any(param.origin > 1) || any(param.origin < 0)
+ error('spinw:genlattice:WrongInput', ...
+ 'Invalid origin supplied, it must be fractional coordinates.');
+ end
+end
+if iscell(param.sym)
+ if numel(param.sym) ~= 2
+ error('spinw:genlattice:WrongInput', ...
+ ['Cell input for spgr/sym must have two elements {spgr, label}', ...
+ ' e.g. {"-x,y,-z", "P 2"}'])
+ elseif ~isempty(param.label)
+ warning('spinw:genlattice:WrongInput', ...
+ ['Label provided in spgr/sym argument and in label argument - ', ...
+ 'the label will be taken from the label argument']);
+ end
end
if ~isempty(param.bv)
@@ -148,7 +207,12 @@
% rotate a-axis along x
phi = atan2(norm(cross(BV1(:,1),a)),dot(BV1(:,1),a));
% rotate the basis vectors
- [BV2, R2] = sw_rot(c,phi,BV1);
+ if dot(cross(BV1(:,1), BV1(:,2)), BV1(:,3)) > 1e-10
+ [BV2, R2] = sw_rot(c, phi, BV1);
+ else
+ [BV2, R2] = sw_rot(c, -phi, BV1); % ensure BV2 right-handed
+ end
+
% check the sign of cross(x,y)
if c*cross(BV2(:,1),BV2(:,2))<0
@@ -174,7 +238,7 @@
angle2 = @(V1,V2)atan2(norm(cross(V1,V2)),dot(V1,V2));
- obj.lattice.angle = [angle2(b,c),angle2(a,c),angle2(b,c)];
+ obj.lattice.angle = [angle2(b,c),angle2(a,c),angle2(a,b)];
else
@@ -191,10 +255,6 @@
end
-if numel(param.sym) == 1 && param.sym==0
- param.sym = [];
-end
-
% copy the apporiate label string
if ~isempty(param.sym) && isempty(param.label)
if ischar(param.sym)
@@ -210,23 +270,22 @@
param.sym = {param.sym};
end
[symOp, symInfo] = swsym.operator(param.sym{1});
-
% permute the symmetry operators if necessary
- if ischar(param.perm)
- param.perm = param.perm-'a'+1;
- end
obj.lattice.sym = symOp(param.perm,[param.perm 4],:);
% assign the origin for space group operators
obj.lattice.origin = param.origin;
-
- if isnumeric(param.sym{1}) && numel(param.sym{1})==1
+ % set spacegroup label if known from param.sym if the user has not
+ % provided a label in the function call
+ if ~isempty(param.label) && ischar(param.label)
+ obj.lattice.label = strtrim(param.label);
+ elseif isnumeric(param.sym{1}) && numel(param.sym{1})==1
obj.lattice.label = symInfo.name;
else
- obj.lattice.label = strtrim(param.label);
+ obj.lattice.label = ''; % can't infer label from matrix of sym ops
end
else
- if ~isempty(param.label)
+ if ~isempty(param.label) && ischar(param.label)
obj.lattice.label = strtrim(param.label);
end
end
diff --git a/swfiles/@spinw/genmagstr.m b/swfiles/@spinw/genmagstr.m
index ec57af2b0..ac241e603 100644
--- a/swfiles/@spinw/genmagstr.m
+++ b/swfiles/@spinw/genmagstr.m
@@ -32,7 +32,7 @@ function genmagstr(obj, varargin)
%
% ```
% >>USb = spinw
-% >>USb.genlattice('lat_const',[6.203 6.203 6.203],'spgr','F m -3 m')
+% >>USb.genlattice('lat_const',[6.203 6.203 6.203],'sym','F m -3 m')
% >>USb.addatom('r',[0 0 0],'S',1)
% >>FQ = cat(3,[0;0;1+1i],[0;1+1i;0],[1+1i;0;0])>>
% >>k = [0 0 1;0 1 0;1 0 0];
@@ -50,12 +50,18 @@ function genmagstr(obj, varargin)
%
% `'mode'`
% : Mode that determines how the magnetic structure is generated:
-% * `'random'` (reads -)
-% generates random zero-k magnetic structure.
-% * `'direct'` (reads `S`, `n`, `k`)
+% * `'random'` (optionally reads `k`, `n`, `nExt`)
+% generates a random structure in the structural cell if no other
+% arguments are specified here or previously in this spinw
+% object. If `nExt` is specified all spins in the supercell are
+% randomised. If `k` is specified a random helical structure with
+% moments perpendicular to `n` (default value: `[0 0 1]`) with
+% the specified `k` propagation vector is generated. (`n` is not
+% otherwise used).
+% * `'direct'` (reads `S`, optionally reads `k`, `nExt`)
% direct input of the magnetic structure using the
% parameters of the single-k magnetic structure.
-% * `'tile'` (reads `S`, `n`, `k`)
+% * `'tile'` (reads `S`, optionally reads `nExt`)
% Simply extends the existing or input structure
% (`S`) into a magnetic supercell by replicating it.
% If no structure is stored in the [spinw] object a random
@@ -67,7 +73,7 @@ function genmagstr(obj, varargin)
% Magnetic ordering wavevector `k` will be set to zero. To
% generate structure with non-zero k, use the `'helical'` or
% `'direct'` option.
-% * `'helical'` (reads `S`, `n`, `k`, `r0`)
+% * `'helical'` (reads `S`, optionally reads `n`, `k`, `r0`, `nExt`, `epsilon`)
% generates helical structure in a single cell or in a
% supercell. In contrary to the `'extend'` option the
% magnetic structure is not generated by replication but
@@ -80,7 +86,7 @@ function genmagstr(obj, varargin)
% cell. In the first case $r$ denotes the atomic
% positions, while for the second case $r$ denotes the
% position of the origin of the cell.
-% * `'rotate'` (reads `S`, `n`, `k`)
+% * `'rotate'` (optionally reads `S`, `phi`, `phid`, `n`, `nExt`)
% uniform rotation of all magnetic moments with a
% `phi` angle around the given `n` vector. If
% `phi=0`, all moments are rotated so, that the first
@@ -102,7 +108,7 @@ function genmagstr(obj, varargin)
% vector. The default value for `func` is `@gm_spherical3d`. For planar
% magnetic structure use `@gm_planar`. Only `func` and `x`
% have to be defined for this mode.
-% * `'fourier'` (reads `S`, `n`, `k`, `r0`)
+% * `'fourier'` (reads `S`, optionally reads `k`, `r0`, `nExt`, `epsilon`)
% same as `'helical'`, except the `S` option is taken as the
% Fourier components, thus if it contains real numbers, it will
% generate a sinusoidally modulated structure instead of
@@ -132,8 +138,9 @@ function genmagstr(obj, varargin)
%
% `'n'`
% : Normal vector to the spin rotation plane for single-k magnetic
-% structures, stored in a 3-element row vector. Default value `[0 0 1]`. The
-% coordinate system of the vector is determined by `unit`.
+% structures, stored in a 3-element row vector, is automatically
+% normalised to a unit vector. Default value `[0 0 1]`. The coordinate
+% system of the vector is determined by `unit`.
%
% `'S'`
% : Vector values of the spins (expectation value), dimensions are $[3\times n_{spin} n_K]$.
@@ -219,6 +226,17 @@ function genmagstr(obj, varargin)
inpForm.soft = [inpForm.soft {true true false false false }];
param = sw_readparam(inpForm, varargin{:});
+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 ' ...
+ 'structure to be defined with another mode first'])
+end
+
+% Complex n causes a crash, error out instead
+if ~isreal(param.n)
+ error('spinw:genmagstr:WrongInput', 'n should be real')
+end
if isempty(param.k)
noK = true;
@@ -231,12 +249,39 @@ function genmagstr(obj, varargin)
error('spinw:genmagstr:WrongInput','''nExt'' has to be larger than 0!');
end
+% input type for S, check whether it is complex type
+cmplxS = ~isreal(param.S);
+if strcmpi(param.mode, 'helical') && cmplxS
+ error('spinw:genmagstr:WrongInput', ...
+ ['S must be real for helical mode. To specify complex basis ' ...
+ 'vectors directly use fourier mode.'])
+end
+
if strcmp(param.mode,'extend')
+ warning('spinw:genmagstr:DeprecationWarning',...
+ 'extend mode is deprecated, please use tile mode instead');
param.mode = 'tile';
end
-% input type for S, check whether it is complex type
-cmplxS = ~isreal(param.S);
+mode_args = struct("random", ["k", "n", "nExt", "unit"], ...
+ "direct", ["S", "k", "nExt", "unit"], ...
+ "tile", ["S", "nExt", "unit"], ...
+ "helical", ["S", "n", "k", "r0", "nExt", "epsilon", "unit"], ...
+ "rotate", ["S", "phi", "phid", "n", "nExt", "unit"], ...
+ "func", ["func", "x0"], ...
+ "fourier", ["S", "k", "r0", "nExt", "epsilon", "unit"]);
+if ~any(strcmp(fields(mode_args), param.mode))
+ error('spinw:genmagstr:WrongMode','Wrong param.mode value!');
+else
+ 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)
+ warning('spinw:genmagstr:UnreadInput', ...
+ 'Input(s) %s will be ignored in %s mode', ...
+ string(join(unused_args', ', ')), param.mode)
+ end
+end
switch lower(param.unit)
case 'lu'
@@ -284,10 +329,6 @@ function genmagstr(obj, varargin)
% number of magnetic atoms in the supercell
nMagExt = nMagAtom*prod(nExt);
-if nMagAtom==0
- error('spinw:genmagstr:NoMagAtom','There is no magnetic atom in the unit cell with S>0!');
-end
-
% Create mAtom.Sext matrix.
mAtom = sw_extendlattice(nExt, mAtom);
@@ -296,17 +337,7 @@ function genmagstr(obj, varargin)
% default value
param.n = repmat([0 0 1],[nK 1]);
end
-n = bsxfun(@rdivide,param.n,sqrt(sum(param.n.^2,2)));
-
-if size(param.n,1) ~= nK
- error('spinw:genmagstr:WrongInput',['The number of normal vectors has'...
- ' to be equal to the number of k-vectors!'])
-end
-
-% if the magnetic structure is not initialized start with a random real one
-if strcmp(param.mode,'tile') && (nMagAtom > size(param.S,2))
- param.mode = 'random';
-end
+n = bsxfunsym(@rdivide,param.n,sqrt(sum(param.n.^2,2)));
% convert input into symbolic variables
if obj.symb
@@ -315,7 +346,7 @@ function genmagstr(obj, varargin)
n = sym(n);
end
-if ~cmplxS && ~strcmpi(param.mode,'fourier') && ~strcmpi(param.mode,'direct') && any(k)
+if ~cmplxS && ~strcmpi(param.mode,'fourier') && ~strcmpi(param.mode,'direct') && any(k(:))
param.S = param.S + 1i*cross(repmat(permute(n,[2 3 1]),[1 size(param.S,2) 1]),param.S);
end
@@ -327,9 +358,11 @@ function genmagstr(obj, varargin)
% cells defined in obj
% -the number of spins stored in obj is not equal to the number
% of spins in the final structure
- if any(double(obj.mag_str.nExt) - double(param.nExt)) || (size(param.S,2) ~= nMagExt)
- S = param.S(:,1:nMagAtom,:);
- S = repmat(S,[1 prod(nExt) 1]);
+ if nMagAtom ~= size(param.S,2) && nMagExt ~= size(param.S,2)
+ error('spinw:genmagstr:WrongInput', ['Incorrect input size for S, ' ...
+ 'S must be provided for each magnetic atom']);
+ elseif any(double(obj.mag_str.nExt) - double(param.nExt)) || (size(param.S,2) ~= nMagExt)
+ S = repmat(param.S,[1 prod(nExt) 1]);
else
S = param.S;
end
@@ -341,7 +374,7 @@ function genmagstr(obj, varargin)
% Create random spin directions and use a single k-vector
S = randn(nMagExt,3);
S = bsxfun(@rdivide,S,sqrt(sum(S.^2,2)));
- S = bsxfun(@times,S,mAtom.Sext')';
+ S = bsxfunsym(@times,S,mAtom.Sext')';
if noK
k = [0 0 0];
end
@@ -352,11 +385,18 @@ function genmagstr(obj, varargin)
S0 = param.S;
% Magnetic ordering wavevector in the extended unit cell.
kExt = k.*nExt;
- % Warns about the non sufficient extension of the unit cell.
- % we substitute random values for symbolic km
+ % Substitute random values for symbolic km
skExt = sw_sub1(kExt,'rand');
if any(abs(skExt(:)-round(skExt(:)))>param.epsilon) && prod(nExt) > 1
+ % Warns about the non sufficient extension of the unit cell.
warning('spinw:genmagstr:UCExtNonSuff','In the extended unit cell k is still larger than epsilon!');
+ else
+ idx = find(sum(k, 1) == 0);
+ if any(round(skExt(:)) > 1) || any(nExt(idx) > 1)
+ % Warns about overextension of the unit cell.
+ warning('spinw:genmagstr:UCExtOver', ...
+ 'Unit cell has been extended beyond what is required for the given k')
+ end
end
% number of spins in the input
nSpin = size(param.S,2);
@@ -387,14 +427,25 @@ function genmagstr(obj, varargin)
if addPhase
% additional phase for each spin in the magnetic supercell
%phi = sum(bsxfun(@times,2*pi*kExt',r),1);
- phi = sum(bsxfun(@times,2*pi*permute(kExt,[2 3 1]),r),1);
+ phi = sum(bsxfunsym(@times,2*pi*permute(kExt,[2 3 1]),r),1);
% add the extra phase for each spin in the unit cell
% TODO check
- S = bsxfun(@times,S0(:,mod(0:(nMagExt-1),nSpin)+1,:),exp(-1i*phi));
+ S = bsxfunsym(@times,S0(:,mod(0:(nMagExt-1),nSpin)+1,:),exp(-1i*phi));
else
S = S0;
end
+ if strcmpi(param.mode, 'helical')
+ for ik=1:size(k, 1)
+ Sk = real(param.S(:, :, ik));
+ if any(dot(repmat(n(ik, :), size(Sk, 2), 1)', Sk) > 1e-4)
+ warning('spinw:genmagstr:SnParallel', ...
+ ['There are spin components parallel to n, the ' ...
+ 'amplitude of these components will be modulated']);
+ break;
+ end
+ end
+ end
case 'direct'
% direct input of real magnetic moments
S = param.S;
@@ -417,10 +468,10 @@ function genmagstr(obj, varargin)
S = param.S;
if ~isreal(param.phi)
- % rotate the first spin along [100]
- S1 = S(:,1)-sum(n*S(:,1))*n';
- S1 = S1/norm(S1);
- param.phi = -atan2(cross(n,[1 0 0])*S1,[1 0 0]*S1);
+ error('spinw:genmagstr:ComplexPhi', ...
+ ['Phi should be real! If you really intended phi to have ' ...
+ 'a complex component, this undocumented feature has been ' ...
+ 'removed. Please contact the spinw developers.'])
end
if param.phi == 0
@@ -435,6 +486,11 @@ function genmagstr(obj, varargin)
% Axis of rotation defined by the spin direction
nRot = cross(n,S1);
+ if all(nRot == 0)
+ error('spinw:genmagstr:InvalidRotation', ...
+ ['Cannot automatically determine rotation as the ' ...
+ 'first spin is parallel to n']);
+ end
% Angle of rotation.
phi = -atan2(norm(cross(S1,n)),dot(S1,n));
else
@@ -459,9 +515,6 @@ function genmagstr(obj, varargin)
if any(k)
S = S + 1i*cross(repmat(permute(n,[2 3 1]),[1 size(S,2) 1]),S);
end
-
- otherwise
- error('spinw:genmagstr:WrongMode','Wrong param.mode value!');
end
% normalize the magnetic moments
diff --git a/swfiles/@spinw/horace.m b/swfiles/@spinw/horace.m
index 7460d1efe..9a438092b 100644
--- a/swfiles/@spinw/horace.m
+++ b/swfiles/@spinw/horace.m
@@ -1,17 +1,17 @@
function [w, s] = horace(obj, qh, qk, ql, varargin)
% spin wave calculator with interface to Horace
-%
+%
% ### Syntax
-%
+%
% `[w, s] = horace(obj, qh, qk, ql,Name,Value)`
-%
+%
% ### Description
-%
+%
% `[w, s] = horace(obj, qh, qk, ql,Name,Value)` produces spin wave
% dispersion and intensity for [Horace](http://horace.isis.rl.ac.uk).
-%
+%
% ### Examples
-%
+%
% This example creates a `d3d` object, a square in $(h,k,0)$ plane and in
% energy between 0 and 10 meV. Then calculates the inelastice neutron
% scattering intensity of the square lattice antiferromagnet stored in
@@ -27,17 +27,17 @@
% >>snapnow
% >>>horace off
% ```
-%
+%
% ### Input Arguments
-%
+%
% `obj`
% : [spinw] object.
-%
+%
% `qh`, `qk`, `ql`
% : Reciprocal lattice vectors in reciprocal lattice units.
-%
+%
% ### Name-Value Pair Arguments
-%
+%
% `'component'`
% : Selects the previously calculated intensity component to be
% convoluted. The possible options are:
@@ -47,15 +47,15 @@
% * `'Sab'` convolutes the selected components of the spin-spin
% correlation function.
% For details see [sw_egrid].
-%
+%
% `'norm'`
% : If `true` the spin wave intensity is normalized to mbarn/meV/(unit
% cell) units. Default is `false`.
-%
+%
% `'dE'`
% : Energy bin size, for intensity normalization. Use 1 for no
% division by `dE` in the intensity.
-%
+%
% `'param'`
% : Input parameters (can be used also within Tobyfit). Additional
% parameters (`'mat'`,`'selector'`) might be necessary, for details see
@@ -72,7 +72,7 @@
% of `spinw.horace` will be forwarded. In this case it is recommended
% to use [sw_readparam] function to handle the variable number
% arguments within `func()`.
-%
+%
% `'parfunc'`
% : Parser function of the `param` input. Default value is
% `@spinw.matparser` which can be used directly by Tobyfit. For user
@@ -82,7 +82,7 @@
% ```
% where obj is an spinw type object, param is the parameter
% values forwarded from` spinw.horace` directly.
-%
+%
% `'func'`
% : User function that will be called after the parameters set on
% the [spinw] object. It can be used to optimize magnetic
@@ -91,7 +91,7 @@
% ```
% fun(obj)
% ```
-%
+%
% `'fid'`
% : Defines whether to provide text output. The default value is determined
% by the `fid` preference stored in [swpref]. The possible values are:
@@ -100,8 +100,15 @@
% * `fid` File ID provided by the `fopen` command, the output is written
% into the opened file stream.
%
+% `'useFast'`
+% : whether to use the 'fastmode' option in spinwave() or not. This method
+% calculates only the unpolarised neutron cross-section and
+% *ignores all negative energy branches*. In general it produces
+% the same spectra as spinw.spinwave, with some rounding errors,
+% but can be 2-3 times faster and uses less memory.
+%
% ### Output Arguments
-%
+%
% `w`
% : Cell that contains the spin wave energies. Every cell elements
% contains a vector of spin wave energies for the corresponding
@@ -111,9 +118,9 @@
% : Cell that contains the calculated element of the spin-spin
% correlation function. Every cell element contains a vector of
% intensities in the same order as the spin wave energies in `w`.
-%
+%
% ### See Also
-%
+%
% [spinw] \| [spinw.spinwave] \| [spinw.matparser] \| [sw_readparam]
%
@@ -122,13 +129,25 @@
return
end
-inpForm.fname = {'component' 'norm' 'dE' 'parfunc' 'param' 'func' 'fid'};
-inpForm.defval = {'Sperp' false 0 @obj.matparser [] [] -1 };
-inpForm.size = {[1 -1] [1 1] [1 1] [1 1] [1 -2] [1 1] [1 1]};
-inpForm.soft = {false false false false true true false};
+inpForm.fname = {'component' 'norm' 'dE' 'parfunc' 'param' 'func' 'fid' 'useFast'};
+inpForm.defval = {'Sperp' false 0 @obj.matparser [] [] -1 1};
+inpForm.size = {[1 -1] [1 1] [1 1] [1 1] [1 -2] [1 1] [1 1] [1 1]};
+inpForm.soft = {false false false false true true false false};
warnState = warning('off','sw_readparam:UnreadInput');
-param = sw_readparam(inpForm, varargin{:});
+% To handle matlab fitting syntax, which may set the first argument to the
+% fittable parameter (without keyword).
+if ~ischar(varargin{1})
+ param = sw_readparam(inpForm, varargin{2:end});
+ if isempty(param.param)
+ varargin = [varargin {'param'} varargin(1)];
+ end
+ % always override keyword specified parameters
+ param.param = varargin{1};
+ varargin(1) = [];
+else
+ param = sw_readparam(inpForm, varargin{:});
+end
pref = swpref;
if ~isempty(param.param)
@@ -158,24 +177,6 @@
param.func(obj);
end
-% calculate spin wave spectrum
-if nargin > 5
- % include the fitmode option to speed up calculation
- if numel(varargin) == 1
- varargin{1}.fitmode = 2;
- spectra = obj.spinwave([qh(:) qk(:) ql(:)]',varargin{1});
- else
- spectra = obj.spinwave([qh(:) qk(:) ql(:)]',varargin{:},'fitmode',true);
- end
-else
- spectra = obj.spinwave([qh(:) qk(:) ql(:)]','fitmode',true);
-end
-warning(warnState);
-
-% calculate Sperp
-spectra = sw_neutron(spectra,'pol',false);
-
-
% parse the component string
if iscell(param.component)
nConv = numel(param.component);
@@ -193,17 +194,63 @@
param.component = {param.component};
end
+needSab = false;
+for ii = 1:numel(parsed)
+ par0 = parsed{ii};
+ for jj = 1:length(par0.type)
+ if par0.type{jj}(1) ~= 1
+ needSab = true;
+ break;
+ end
+ end
+end
+
+% calculate spin wave spectrum
+if nargin > 5
+ % include the fitmode option to speed up calculation
+ if numel(varargin) == 1
+ varargin{1}.fitmode = 2;
+ if needSab || ~param.useFast
+ spectra = obj.spinwave([qh(:) qk(:) ql(:)]',varargin{1});
+ else
+ spectra = obj.spinwave([qh(:) qk(:) ql(:)]',varargin{1},'fastmode',true);
+ end
+ else
+ if needSab || ~param.useFast
+ spectra = obj.spinwave([qh(:) qk(:) ql(:)]',varargin{:},'fitmode',true);
+ else
+ spectra = obj.spinwave([qh(:) qk(:) ql(:)]',varargin{:},'fitmode',true,'fastmode',true);
+ end
+ end
+else
+ if needSab || ~param.useFast
+ spectra = obj.spinwave([qh(:) qk(:) ql(:)]','fitmode',true);
+ else
+ spectra = obj.spinwave([qh(:) qk(:) ql(:)]','fitmode',true,'fastmode',true);
+ end
+end
+warning(warnState);
+
+% calculate Sperp
+if needSab || ~param.useFast
+ spectra = sw_neutron(spectra,'pol',false);
+end
+
% pack all cross section into a cell for easier looping
if iscell(spectra.omega)
nTwin = numel(spectra.omega);
omega = spectra.omega;
- Sab = spectra.Sab;
+ if needSab
+ Sab = spectra.Sab;
+ end
Sperp = spectra.Sperp;
-
+
else
nTwin = 1;
omega = {spectra.omega};
- Sab = {spectra.Sab};
+ if needSab
+ Sab = {spectra.Sab};
+ end
Sperp = {spectra.Sperp};
end
@@ -261,10 +308,10 @@
r0 = 2.8179403267e-15; % m
% cross section constant in mbarn
p2 = (g*gamma*r0/2)^2*1e28*1e3; % mbarn
-
+
% convert intensity to mbarn/meV units using the energy bin size
DSF = DSF*p2/param.dE;
-
+
fprintf0(obj.fileid,'Intensity is converted to mbarn/meV units.\n');
if spectra.gtensor
fprintf0(fid,'g-tensor was already included in the spin wave calculation.\n');
@@ -278,4 +325,4 @@
% intensity in cell
s = mat2cell(DSF' ,nHkl,ones(nMode*nTwin,1));
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/horace_sqw.m b/swfiles/@spinw/horace_sqw.m
new file mode 100644
index 000000000..fb6406961
--- /dev/null
+++ b/swfiles/@spinw/horace_sqw.m
@@ -0,0 +1,213 @@
+function weight = horace_sqw(obj, qh, qk, ql, en, pars, varargin)
+% Calculate spectral weight from a spinW model for Horace. Uses disp2sqw
+% as the back-end function to calculate the convolution.
+%
+% >> weight = swobj.horace_sqw(qh,qk,ql,en,pars,swobj,pars,kwpars)
+%
+% Input:
+% ------
+% qh,qk,ql,en Arrays containing points at which to evaluate sqw from the
+% broadened dispersion
+%
+% pars Arguments needed by the function.
+% - pars = [model_pars scale_factor resolution_pars]
+% - Should be a vector of parameters
+% - The first N parameters relate to the spin wave dispersion
+% and correspond to spinW matrices in the order defined by
+% the 'mat' option [N=numel(mat)]
+% - The next M parameters relate to the convolution parameters
+% corresponding to the convolution function defined by the
+% 'resfun' option (either one or two parameters depending
+% on function type.
+% - The last parameter is a scale factor for the intensity
+% If this is omitted, a scale factor of 1 is used;
+%
+% kwpars - A series of 'keywords' and parameters. Specific to this
+% function is:
+%
+% - 'resfun' - determines the convolution / resolution
+% function to get S(q,w). It can be either a string:
+% 'gauss' - gaussian with single fixed (fittable) FWHM
+% 'lor' - lorentzian with single fixed (fittable) FWHM
+% 'voigt' - pseudo-voigt with single fixed (fittable) FWHM
+% @fun - a function handle satisfying the requirements of
+% the 'fwhm' parameter of disp2sqw.
+% NB. For 'gauss' and 'lor' only one fwhm parameter may be
+% specified. For 'voigt', fwhm = [width lorz_frac]
+% contains two parameters - the fwhm and lorentzian fraction
+% [default: 'gauss']
+% - 'partrans' - a function to transform the fit parameters
+% This transformation will be applied before each iteration
+% and the transformed input parameter vector passed to
+% spinW and the convolution function.
+% [default: @(y)y % identity operation]
+% - 'coordtrans' - a matrix to transform the input coordinates
+% (qh,qk,ql,en) before being sent to SpinW.
+% [default: eye(4) % identity]
+%
+% In addition, the following parameters are used by this function
+% and will also be passed on to spinw.matparser which will
+% do the actual modification of spinW model parameters:
+%
+% - 'mat' - A cell array of labels of spinW named 'matrix' or
+% matrix elements. E.g. {'J1', 'J2', 'D(3,3)'}. These will
+% be the model parameters to be varied in a fit, their
+% order in this cell array will be the same as in the
+% fit parameters vector.
+% [default: [] % empty matrix - no model parameters]
+%
+% All other parameters will be passed to spinW. See the help
+% for spinw/spinwave, spinw/matparser and spinw/sw_neutron
+% for more information.
+%
+% swobj The spinwave object which defines the magnetic system to be
+% calculated.
+%
+% Output:
+% -------
+% weight Array with spectral weight at the q,e points
+% If q and en given: weight is an nq x ne array, where nq
+% is the number of q points, and ne the
+% number of energy points
+% If qw given together: weight has the same size and dimensions
+% as q{1} i.e. qh
+%
+% Example:
+% --------
+%
+% tri = sw_model('triAF',[5 1]); % J1=5, J2=1 (AFM)
+% spinw_pars = {'mat', {'J1', 'J2'}, 'hermit', true, ...
+% 'useMex', true, 'optmem', 100};
+% [wf,fp] = fit_sqw(w1, @tri.horace_sqw, {[J1 J2 fwhm] spinw_pars});
+
+% Error checking
+if ~isnumeric(qh) || ~isnumeric(qk) || ~isnumeric(ql) || ~isnumeric(en)
+ error('horace_sqw:BadInput', 'Inputs for qh, qk, ql and en must be numeric vectors.');
+elseif numel(qh) ~= numel(qk) || numel(qh) ~= numel(ql) || numel(qh) ~= numel(en)
+ error('horace_sqw:BadInput', 'Inputs for qh, qk, ql and en must be same size.');
+end
+
+inpForm.fname = {'resfun' 'partrans' 'coordtrans' 'mat'};
+inpForm.defval = {'gauss' @(y)y eye(4) []};
+inpForm.size = {[1 -2] [1 1] [4 4] [1 -1]};
+inpForm.soft = {false false false false};
+
+warnState = warning('off','sw_readparam:UnreadInput');
+param = sw_readparam(inpForm, varargin{:});
+
+% Sets the number of spinW model parameters. All others are convolution pars.
+n_horace_pars = numel(param.mat);
+if isempty(n_horace_pars);
+ n_horace_pars = 0;
+end
+model_pars = pars(1:n_horace_pars);
+res_pars = pars((n_horace_pars+1):end);
+scale_factor = 1;
+
+% Transforms the input parameters
+pars = param.partrans(pars);
+
+% Transforms input coordinates if needed
+if sum(sum(abs(param.coordtrans - eye(4)))) > 0
+ qc = [qh(:) qk(:) ql(:) en(:)];
+ qh = sum(bsxfun(@times, param.coordtrans(1,:), qc),2);
+ qk = sum(bsxfun(@times, param.coordtrans(2,:), qc),2);
+ ql = sum(bsxfun(@times, param.coordtrans(3,:), qc),2);
+ en = sum(bsxfun(@times, param.coordtrans(4,:), qc),2);
+ clear qc;
+end
+
+% Determine which resolution function to use.
+if ischar(param.resfun)
+ if strncmp(lower(param.resfun), 'gauss', 5)
+ fwhm = res_pars(1);
+ if numel(res_pars) > 1
+ scale_factor = res_pars(2);
+ end
+ elseif strncmp(lower(param.resfun), 'lor', 3)
+ fwhm = @(emat, cen) lorz_internal(emat, cen, res_pars);
+ if numel(res_pars) > 1
+ scale_factor = res_pars(2);
+ end
+ elseif strncmp(lower(param.resfun), 'voigt', 3)
+ fwhm = @(emat, cen) voigt_internal(emat, cen, res_pars);
+ if numel(res_pars) > 2
+ scale_factor = res_pars(3);
+ end
+ elseif strncmp(lower(param.resfun), 'sho', 3)
+ resfun_id = find(cellfun(@(x) ~isempty(strfind(x, 'resfun')), varargin) ...
+ .* cellfun(@(x) ~iscell(x), varargin));
+ args = varargin([1:(resfun_id-1) (resfun_id+2):numel(varargin)]);
+ weight = sho_internal(qh, qk, ql, en, obj, res_pars, model_pars, args{:});
+ return
+ else
+ error('horace_sqw:UnknowResFun', ...
+ sprintf('Unknown resolution function %s', param.resfun));
+ end
+else
+ fwhm = param.resfun;
+ if numel(res_pars) > 0
+ scale_factor = res_pars(1);
+ end
+end
+
+weight = disp2sqw(qh, qk, ql, en, @obj.horace, {model_pars varargin{:}}, fwhm);
+
+if scale_factor ~= 1
+ weight = weight * scale_factor;
+end
+
+end
+
+%--------------------------------------------------------------------------------------------------
+function out = lorz_internal(Emat, center, fwhm)
+% Calculates a Lorentzian function for disp2sqw.
+ % fwhm should be scalar.
+ out = abs(fwhm(1)/pi) ./ (bsxfun(@minus, center, Emat).^2 + fwhm(1)^2);
+end
+
+
+function out = voigt_internal(Emat, center, fwhm)
+% Calculates a pseudo-Voigt function for disp2sqw.
+ lorfrac = fwhm(2);
+ fwhm = fwhm(1);
+ sig = fwhm / sqrt(log(256));
+ Ediff2 = bsxfun(@minus, center, Emat).^2;
+ out = (abs(fwhm/pi) ./ (Ediff2 + fwhm^2)) .* lorfrac + ...
+ (exp(-Ediff2 ./ (2*sig^2)) ./ (sig*sqrt(2*pi))) .* (1 - lorfrac);
+end
+
+function weight = sho_internal(qh, qk, ql, en, obj, res_pars, varargin)
+% Calculates weight from SHO model
+ [e, sf] = obj.horace(qh, qk, ql, varargin{:});
+
+ gam = 0.1;
+ if numel(res_pars) > 1
+ gam = res_pars(1);
+ end
+
+ temp = 0;
+ if numel(res_pars) > 1
+ temp = res_pars(2);
+ end
+
+ amp = 1;
+ if numel(res_pars) > 1
+ amp = res_pars(3);
+ end
+
+ if abs(temp) 0) .* (~isnan(sf{ii})));
+ weight(ip) = weight(ip) + (4.*gam.*e{ii}(ip).*sf{ii}(ip)) ./ ...
+ (pi.*((en(ip).^2-e{ii}(ip).^2).^2 + 4.*(gam.*en(ip)).^2));
+ end
+ weight = amp .* Bose .* weight;
+end
diff --git a/swfiles/@spinw/intmatrix.m b/swfiles/@spinw/intmatrix.m
index 348a344bd..ed0e24978 100644
--- a/swfiles/@spinw/intmatrix.m
+++ b/swfiles/@spinw/intmatrix.m
@@ -62,31 +62,38 @@
% ### Output Arguments
%
% `SS`
-% : structure with fields `iso`, `ani`, `dm`, `gen`, `bq`, `dip` and
-% `all`. It describes the bonds between spins. Every field is a matrix,
-% where every column is a coupling between two spins. The
-% first 3 rows contain the unit cell translation vector
-% between the interacting spins, the 4th and 5th rows contain
-% the indices of the two interacting spins in the
-% [spinw.matom] list. The following rows contains the
-% strength of the interaction. For isotropic exchange it is a
-% single number, for DM interaction it is a column vector
-% `[DMx; DMy; DMz]`, for anisotropic interaction `[Jxx; Jyy;
-% Jzz]` and for general interaction `[Jxx; Jxy; Jxz; Jyx; Jyy;
-% Jyz; Jzx; Jzy; Jzz]` and for biquadratic exchange it is also
-% a single number. For example:
-% ```
-% SS.iso = [dl_a; dl_b; dl_c; matom1; matom2; Jval]
-% ```
+% : structure where every field is a matrix. Every column is a coupling
+% between two spins. The first 3 rows contain the unit cell translation
+% vector between the interacting spins, the 4th and 5th rows contain
+% the indices of the two interacting spins in the [spinw.matom] list.
+% Subsequent rows in the matrix depend on the field
+% SS will always have the following fields
+% * `all`
+% * `dip`
+% Subsequent rows in these matrices are the elements of the 3 x 3
+% exchange matrix `[Jxx; Jxy; Jxz; Jyx; Jyy; Jyz; Jzx; Jzy; Jzz]`
+% and the final row indicates whether the coupling is
+% bilinear (0) or biquadratic (1). The `dip` field contains the dipolar
+% interactions only that are not added to `SS.all.
% If `plotmode` is `true`, two additional rows are added to `SS.all`,
-% that contains the `idx` indices of the
-% `obj.matrix(:,:,idx)` corresponding matrix for each
-% coupling and the `idx` values of the couplings (stored in
-% `spinw.coupling.idx`). The `dip` field contains the dipolar
-% interactions that are not added to the `SS.all` field.
+% that contains the `idx` indices of the `obj.matrix(:,:,idx)`
+% corresponding matrix for each coupling and the `idx` values of the
+% couplings (stored in `spinw.coupling.idx`).
+%
+% If fitmode = false there are additional fields
+% * `iso` : One subsequent row which is the isotropic exchane
+% * `ani` : Subsequent rows contain anisotropic interaction `[Jxx; Jyy;
+% Jzz]`),
+% * `dm` : Subsequent rows contain DM interaction `[DMx; DMy; DMz]`
+% * `gen` : Subsequent rows contain a general interaction
+% `[Jxx; Jxy; Jxz; Jyx; Jyy; Jyz; Jzx; Jzy; Jzz]`
+% * `bq` : One subsequent row which is the isotropic exchagne as in
+% SS.iso but only the biquadratic couplings which are not
+% included in SS.iso
%
% `SI`
% : single ion properties stored in a structure with fields:
+%
% * `aniso` Matrix with dimensions of $[3\times 3\times n_{magAtom}]$,
% where the classical energy of the $i$-th spin is expressed
% as `E_aniso = spin(:)*A(:,:,i)*spin(:)'`
@@ -209,7 +216,9 @@
% remove Heisenberg
colSym = colSym & ~isHeis(:)';
% rotate the matrices: R*M*R'
- JJ.mat(:,:,colSym) = mmat(rotOpB(:,:,colSel(colSym)),mmat(JJ.mat(:,:,colSym),permute(rotOpB(:,:,colSel(colSym)),[2 1 3])));
+ if any(colSym)
+ JJ.mat(:,:,colSym) = mmat(rotOpB(:,:,colSel(colSym)),mmat(JJ.mat(:,:,colSym),permute(rotOpB(:,:,colSel(colSym)),[2 1 3])));
+ end
end
mat_type = SS.all(8,:);
@@ -219,11 +228,7 @@
% don't calculate these for speedup in case of fitting
if ~param.fitmode
JJ.type = sw_mattype(JJ.mat);
-
- % new type for biquadratic exchange
- if any(JJ.type(mat_type==1)~=1)
- error('spinw:intmatrix:DataError','Biquadratic exchange matrix has to be isotropic!')
- end
+
% for biquadratic exchange type = 5
JJ.type(mat_type==1) = 5;
@@ -271,7 +276,12 @@
JJ.gen = reshape(JJ.mat,1,9,size(JJ.mat,3));
SS.gen = [SS.all(:,JJ.type == 4); shiftdim(JJ.gen(1,:,JJ.type == 4),1)];
- idx = any(SS.gen(6:end,:));
+ if obj.symbolic
+ idx = any(~sw_always(SS.gen(6:end,:)==0));
+ else
+ idx = any(SS.gen(6:end,:));
+ end
+
%SS.gen = SS.gen(:,sum(SS.gen(6:end,:).^2,1)~=0);
SS.gen = SS.gen(:,idx);
@@ -299,7 +309,7 @@
Edip = -obj.unit.mu0*obj.unit.muB^2/4/pi;
% dipole-dipole interaction matrices
- rrmat = bsxfun(@times,permute(Edip./lA(1,rSel).^3,[1 3 2]),rrmat);
+ rrmat = Edip.*bsxfun(@times,permute(1./lA(1,rSel).^3,[1 3 2]),rrmat);
% multiply with the g-tensors on the left and right sides
rrmat = mmat(permute(SI.g(:,:,coupling.atom1(rSel)),[2 1 3]),rrmat);
Jdip = mmat(rrmat,SI.g(:,:,coupling.atom2(rSel)));
@@ -393,4 +403,4 @@
% Save external field.
SI.field = obj.single_ion.field;
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/optmagk.m b/swfiles/@spinw/optmagk.m
index f848ebfe1..3ab926c3e 100644
--- a/swfiles/@spinw/optmagk.m
+++ b/swfiles/@spinw/optmagk.m
@@ -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
%
@@ -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.
@@ -57,8 +56,10 @@
inpForm.size = {[3 -1] };
inpForm.soft = {true };
+warning('off','sw_readparam:UnreadInput')
param = sw_readparam(inpForm, varargin{:});
-
+softparamcheck(["kbase"], 'optmagk', param, varargin{:});
+warning('on','sw_readparam:UnreadInput')
% calculate symbolic Fourier transformation if obj is in symbolic mode
@@ -123,9 +124,11 @@
kones = ones(1,D);
+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
optfun2 = @(p)(1e-2+optfun(p)-optfun(pOpt0))^2;
diff --git a/swfiles/@spinw/optmagsteep.m b/swfiles/@spinw/optmagsteep.m
index f6b0a623d..bb72961b4 100644
--- a/swfiles/@spinw/optmagsteep.m
+++ b/swfiles/@spinw/optmagsteep.m
@@ -354,7 +354,11 @@
% create swplot figure if it doesn't exist
if param.plot
- hFigure = swplot.activefigure;
+ try
+ hFigure = swplot.activefigure;
+ catch
+ hFigure = obj.plot();
+ end
end
while (rIdx < nRun) && (dM>param.TolX)
@@ -450,7 +454,8 @@
end
if rIdx == nRun
- warning('Convergence was not reached!')
+ warning('spinw:optmagsteep:NotConverged', ...
+ 'Convergence was not reached!')
end
% Save optimised magnetic structure into the spinw object.
@@ -467,7 +472,7 @@
if param.saveAll
optm.M = Msave;
else
- optm.M = M(1:end-1);
+ optm.M = M;
end
optm.dM = dM;
optm.e = E(1:rIdx);
diff --git a/swfiles/@spinw/optmagstr.m b/swfiles/@spinw/optmagstr.m
index 7547ca91f..7080d37a0 100644
--- a/swfiles/@spinw/optmagstr.m
+++ b/swfiles/@spinw/optmagstr.m
@@ -11,10 +11,13 @@
% optimizer that as the name suggests is general however usually less
% efficient than [spinw.optmagk] or [spinw.optmagsteep]. However this
% function enables the usage of constraint functions to improve the
-% optimization. This function is most usefull if there is 1-2 parameters
-% that has to be optimized, such as a canting angle of the spins in
-% magnetic field. To optimize large number of spin angles
-% [spinw.optmagsteep] might be faster.
+% optimization. This function is most useful if there are 1-2 parameters
+% that have to be optimized, such as a canting angle of the spins in
+% a magnetic field. To optimize large numbers of spin angles
+% [spinw.optmagsteep] might be faster. Only obj.mag_str.nExt is used from
+% an already initialised magnetic structure, initial k and S are determined
+% from the optimisation function parameters. If a magnetic structure has
+% not been initialised in obj, nExt = [1 1 1] is used.
%
% ### Examples
%
@@ -171,22 +174,19 @@
inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] [1 1] [1 1] [1 -4] [1 1]}];
inpForm.soft = [inpForm.soft {0 0 0 0 0 1 false}];
-% creat initial magnetic structure
warnState = warning('off','sw_readparam:UnreadInput');
param = sw_readparam(inpForm, varargin{:});
pref = swpref;
-obj.genmagstr(param);
-
-magStr = obj.magstr;
-
-% starting magnetic structure from spinw object
-if isempty(magStr.S)
- obj.genmagstr('mode','random');
+% If magnetic structure hasn't been initialised
+% just use nExt = [1 1 1]
+if isempty(obj.mag_str.F)
+ S = obj.unit_cell.S;
+ nExt = [1 1 1];
+else
+ S = sqrt(sum(obj.magstr.S.^2,1));
+ nExt = double(obj.magstr.N_ext);
end
-
-S = sqrt(sum(magStr.S.^2,1));
-nExt = double(magStr.N_ext);
nMagExt = length(S);
if param.tid == -1
@@ -195,16 +195,25 @@
% determine the limits from the constraint function
+xparam_warn_id = 'spinw:optmagstr:WrongLengthXParam';
+xparam_warn_msg = ['Provided %s is the wrong length, expected length %i, got %i. ' ...
+ 'This input parameter will be ignored.'];
if nargout(param.func) == 6
[~, ~, ~, fname, pname, limit] = param.func(S,[]);
% limits of the fitting parameters
nPar = size(limit,2);
% limits of the parameters
if numel(param.xmin) ~= nPar
+ if ~isempty(param.xmin)
+ warning(xparam_warn_id, xparam_warn_msg, 'xmin', nPar, numel(param.xmin));
+ end
param.xmin = limit(1,:);
end
if numel(param.xmax) ~= nPar
+ if ~isempty(param.xmax)
+ warning(xparam_warn_id, xparam_warn_msg, 'xmax', nPar, numel(param.xmax));
+ end
param.xmax = limit(2,:);
end
@@ -244,6 +253,9 @@
% Initial parameters are random if param.x0 is undefined/wrong size
xRand = (numel(param.x0)~= nPar);
+if xRand && ~isempty(param.x0)
+ warning(xparam_warn_id, xparam_warn_msg, 'x0', nPar, numel(param.x0));
+end
if (~xRand) || (param.nRun<1)
param.nRun = 1;
diff --git a/swfiles/@spinw/powspec.m b/swfiles/@spinw/powspec.m
index 6f6e9fdb4..79f644dbd 100644
--- a/swfiles/@spinw/powspec.m
+++ b/swfiles/@spinw/powspec.m
@@ -161,6 +161,21 @@
% * `1` Display the timing in the Command Window.
% * `2` Show the timing in a separat pup-up window.
%
+% `dE`
+% : Energy resolution (FWHM) can be function, or a numeric matrix that
+% has length 1 or the number of energy bin centers.
+%
+% `'neutron_output'`
+% : If `true`, the spinwave will output only `Sperp`, the S(q,w) component
+% perpendicular to Q that is measured by neutron scattering, and will
+% *not* output the full Sab tensor. (Usually sw_neutron is used to
+% calculate `Sperp`.) Default value is `false`.
+%
+% `'fastmode'`
+% : If `true`, will set `'neutron_output', true`, `'fitmode', true`,
+% `'sortMode', false`, and will only output intensity for positive energy
+% (neutron energy loss) modes. Default value is `false`.
+%
% The function accepts some parameters of [spinw.scga] with the most important
% parameters are:
%
@@ -204,18 +219,28 @@
inpForm.fname = {'nRand' 'Evect' 'T' 'formfact' 'formfactfun' 'tid' 'nInt'};
inpForm.defval = {100 zeros(1,0) T0 false @sw_mff tid0 1e3 };
inpForm.size = {[1 1] [1 -1] [1 1] [1 -2] [1 1] [1 1] [1 1] };
+inpForm.soft = {false false false false false false false };
inpForm.fname = [inpForm.fname {'hermit' 'gtensor' 'title' 'specfun' 'imagChk'}];
inpForm.defval = [inpForm.defval {true false title0 @spinwave true }];
inpForm.size = [inpForm.size {[1 1] [1 1] [1 -3] [1 1] [1 1] }];
+inpForm.soft = [inpForm.soft {false false false false false}];
inpForm.fname = [inpForm.fname {'extrap' 'fibo' 'optmem' 'binType' 'component'}];
inpForm.defval = [inpForm.defval {false false 0 'ebin' 'Sperp' }];
inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] [1 -4] [1 -5] }];
+inpForm.soft = [inpForm.soft {false false false false false}];
+
+inpForm.fname = [inpForm.fname {'fid' , 'dE'}];
+inpForm.defval = [inpForm.defval {-1, []}];
+inpForm.size = [inpForm.size {[1 1], [-6, -7]}];
+inpForm.soft = [inpForm.soft {false true}];
+
+inpForm.fname = [inpForm.fname {'neutron_output' 'fastmode'}];
+inpForm.defval = [inpForm.defval {false false }];
+inpForm.size = [inpForm.size {[1 1] [1 1] }];
+inpForm.soft = [inpForm.soft {false false}];
-inpForm.fname = [inpForm.fname {'fid'}];
-inpForm.defval = [inpForm.defval {-1 }];
-inpForm.size = [inpForm.size {[1 1]}];
param = sw_readparam(inpForm, varargin{:});
@@ -246,7 +271,6 @@
end
nQ = numel(hklA);
-powSpec = zeros(max(1,nE),nQ);
fprintf0(fid,'Calculating powder spectra...\n');
@@ -260,6 +284,7 @@
sw_timeit(0,1,param.tid,'Powder spectrum calculation');
+nk = param.nRand;
if param.fibo
% apply the Fibonacci numerical integration on a sphere
% according to J. Phys. A: Math. Gen. 37 (2004) 11591
@@ -279,11 +304,13 @@
QF(1,:) = cos(theta).*sin(phi);
QF(2,:) = cos(theta).*cos(phi);
+ nk = F;
end
% lambda value for SCGA, empty will make integration in first loop
specQ.lambda = [];
+hkl = zeros(3, nk * nQ);
for ii = 1:nQ
if param.fibo
Q = QF*hklA(ii);
@@ -291,40 +318,40 @@
rQ = randn(3,param.nRand);
Q = bsxfun(@rdivide,rQ,sqrt(sum(rQ.^2)))*hklA(ii);
end
- hkl = (Q'*obj.basisvector)'/2/pi;
-
- switch funIdx
- case 0
- % general function call allow arbitrary additional parameters to
- % pass to the spectral calculation function
- warnState = warning('off','sw_readparam:UnreadInput');
- specQ = param.specfun(obj,hkl,varargin{:});
- warning(warnState);
- case 1
- % @spinwave
- specQ = spinwave(obj,hkl,struct('fitmode',true,'notwin',true,...
- 'Hermit',param.hermit,'formfact',param.formfact,...
- 'formfactfun',param.formfactfun,'gtensor',param.gtensor,...
- 'optmem',param.optmem,'tid',0,'fid',0),'noCheck');
-
- case 2
- % @scga
- specQ = scga(obj,hkl,struct('fitmode',true,'formfact',param.formfact,...
- 'formfactfun',param.formfactfun,'gtensor',param.gtensor,...
- 'fid',0,'lambda',specQ.lambda,'nInt',param.nInt,'T',param.T,...
- 'plot',false),'noCheck');
- end
-
- specQ = sw_neutron(specQ,'pol',false);
- specQ.obj = obj;
- % use edge grid by default
- specQ = sw_egrid(specQ,struct('Evect',param.Evect,'T',param.T,'binType',param.binType,...
- 'imagChk',param.imagChk,'component',param.component),'noCheck');
- powSpec(:,ii) = sum(specQ.swConv,2)/param.nRand;
- sw_timeit(ii/nQ*100,0,param.tid);
+ i0 = (ii-1) * nk + 1;
+ hkl(:, i0:(i0+nk-1)) = (Q'*obj.basisvector)'/2/pi;
end
-sw_timeit(100,2,param.tid);
+switch funIdx
+ case 0
+ % general function call allow arbitrary additional parameters to
+ % pass to the spectral calculation function
+ warnState = warning('off','sw_readparam:UnreadInput');
+ specQ = param.specfun(obj,hkl,varargin{:});
+ warning(warnState);
+ case 1
+ % @spinwave
+ specQ = spinwave(obj,hkl,struct('fitmode',true,'notwin',true,...
+ 'Hermit',param.hermit,'formfact',param.formfact,...
+ 'formfactfun',param.formfactfun,'gtensor',param.gtensor,...
+ 'optmem',param.optmem,'tid',param.tid,'fid',0, ...
+ 'neutron_output', param.neutron_output, 'fastmode', ...
+ param.fastmode),'noCheck');
+ case 2
+ % @scga
+ specQ = scga(obj,hkl,struct('fitmode',true,'formfact',param.formfact,...
+ 'formfactfun',param.formfactfun,'gtensor',param.gtensor,...
+ 'fid',0,'lambda',specQ.lambda,'nInt',param.nInt,'T',param.T,...
+ 'plot',false),'noCheck');
+end
+if funIdx ~= 1 || (~param.neutron_output && ~param.fastmode)
+ specQ = sw_neutron(specQ,'pol',false);
+end
+specQ.obj = obj;
+% use edge grid by default
+specQ = sw_egrid(specQ,struct('Evect',param.Evect,'T',param.T,'binType',param.binType,...
+ 'imagChk',param.imagChk,'component',param.component, 'dE', param.dE),'noCheck');
+powSpec = squeeze(sum(reshape(specQ.swConv, max(1, nE), nk, nQ), 2) / param.nRand);
fprintf0(fid,'Calculation finished.\n');
@@ -377,4 +404,4 @@
F = num(end-1);
F1 = num(end-2);
end
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/private/softparamcheck.m b/swfiles/@spinw/private/softparamcheck.m
new file mode 100644
index 000000000..817340fed
--- /dev/null
+++ b/swfiles/@spinw/private/softparamcheck.m
@@ -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
\ No newline at end of file
diff --git a/swfiles/@spinw/private/vararginnames.m b/swfiles/@spinw/private/vararginnames.m
new file mode 100644
index 000000000..9077e9ac6
--- /dev/null
+++ b/swfiles/@spinw/private/vararginnames.m
@@ -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
\ No newline at end of file
diff --git a/swfiles/@spinw/spinw.m b/swfiles/@spinw/spinw.m
index beedd7630..2a9fcaf8e 100644
--- a/swfiles/@spinw/spinw.m
+++ b/swfiles/@spinw/spinw.m
@@ -483,25 +483,22 @@
if ishandle(firstArg)
% get spinw object from graphics handle
- switch get(firstArg,'Tag')
- case 'sw_crystal'
- figDat = getappdata(firstArg);
- obj = copy(figDat.obj);
- case 'sw_spectra'
- figDat = getappdata(firstArg);
- obj = copy(figDat.spectra.obj);
+ figDat = getappdata(firstArg);
+ if isfield(figDat, 'spectra')
+ figDat = figDat.spectra;
end
- return
-
- end
-
- if isa(firstArg, 'spinw')
+ if isfield(figDat, 'obj') && isa(figDat.obj, 'spinw')
+ obj = copy(figDat.obj);
+ return
+ else
+ error('spinw:spinw:WrongInput', ...
+ 'No spinw object could be found in the input figure');
+ end
+ elseif isa(firstArg, 'spinw')
% it is used when objects are passed as arguments.
obj = copy(firstArg);
return
- end
-
- if isstruct(firstArg)
+ elseif isstruct(firstArg)
objS = initfield(firstArg);
% change lattice object
@@ -518,8 +515,7 @@
obj.(fNames{ii}) = objS.(fNames{ii});
end
return;
- end
- if ischar(firstArg)
+ elseif ischar(firstArg)
% import data from file (cif/fst are supported)
objS = initfield(struct);
@@ -529,7 +525,10 @@
end
obj = sw_import(firstArg,false,obj);
-
+ else
+ error('spinw:spinw:WrongInput', ...
+ ['Cannot create spinw object from input of type ' ...
+ class(firstArg)])
end
end % .spinw
diff --git a/swfiles/@spinw/spinwave.m b/swfiles/@spinw/spinwave.m
index d3b30094a..d9d7c18f9 100644
--- a/swfiles/@spinw/spinwave.m
+++ b/swfiles/@spinw/spinwave.m
@@ -39,28 +39,28 @@
% >>sw_plotspec(spec)
% >>snapnow
% ```
-%
+%
% ### Input Arguments
%
% `obj`
% : [spinw] object.
-%
+%
% `Q`
% : Defines the $Q$ points where the spectra is calculated, in reciprocal
% lattice units, size is $[3\times n_{Q}]$. $Q$ can be also defined by
% several linear scan in reciprocal space. In this case `Q` is cell type,
% where each element of the cell defines a point in $Q$ space. Linear scans
% are assumed between consecutive points. Also the number of $Q$ points can
-% be specified as a last element, it is 100 by defaults.
-%
+% be specified as a last element, it is 100 by defaults.
+%
% For example to define a scan along $(h,0,0)$ from $h=0$ to $h=1$ using
% 200 $Q$ points the following input should be used:
% ```
-% Q = {[0 0 0] [1 0 0] 50}
+% Q = {[0 0 0] [1 0 0] 200}
% ```
%
% For symbolic calculation at a general reciprocal space point use `sym`
-% type input.
+% type input.
%
% For example to calculate the spectrum along $(h,0,0)$ use:
% ```
@@ -88,12 +88,27 @@
% F = formfactfun(atomLabel,Q)
% ```
% where the parameters are:
-% * `F` row vector containing the form factor for every input
+% * `F` row vector containing the form factor for every input
% $Q$ value
% * `atomLabel` string, label of the selected magnetic atom
% * `Q` matrix with dimensions of $[3\times n_Q]$, where each
% column contains a $Q$ vector in $\\ang^{-1}$ units.
%
+% `'cmplxBase'`
+% : If `true`, we use a local coordinate system fixed by the
+% complex magnetisation vectors:
+% $\begin{align} e_1 &= \Im(\hat{M})\\
+% e_3 &= Re(\hat{M})\\
+% e_2 &= e_3\times e_1
+% \end{align}$
+% If `false`, we use a coordinate system fixed to the moments:
+% $\begin{align} e_3 \parallel S_i\\
+% e_2 &= \S_i \times [1, 0, 0]\\
+% e_1 &= e_2 \times e_3
+% \end{align}$
+% Except if $S_i \parallel [1, 0, 0], e_2 = [0, 0, 1]$. The default is
+% `false`.
+%
% `'gtensor'`
% : If true, the g-tensor will be included in the spin-spin correlation
% function. Including anisotropic g-tensor or different
@@ -101,17 +116,29 @@
% isotropic g-tensor is possible afterwards using the [sw_instrument]
% function.
%
+% `'neutron_output'`
+% : If `true`, will output only `Sperp`, the S(q,w) component perpendicular
+% to Q that is measured by neutron scattering, and will *not* output the
+% full Sab tensor. (Usually sw_neutron is used to calculate `Sperp`.)
+% Default value is `false`.
+%
% `'fitmode'`
-% : If `true`, function is optimized for multiple consecutive calls (e.g.
+% : If `true`, function is optimized for multiple consecutive calls (e.g.
% the output spectrum won't contain the copy of `obj`), default is
% `false`.
%
+% `'fastmode'`
+% : If `true`, will set `'neutron_output', true`, `'fitmode', true`,
+% `'sortMode', false`, and will only output intensity for positive energy
+% (neutron energy loss) modes. Default value is `false`.
+%
% `'notwin'`
% : If `true`, the spectra of the twins won't be calculated. Default is
-% `false`.
+% `false`.
%
% `'sortMode'`
-% : If `true`, the spin wave modes will be sorted. Default is `true`.
+% : If `true`, the spin wave modes will be sorted by continuity. Default is
+% `true`.
%
% `'optmem'`
% : Parameter to optimise memory usage. The list of Q values will be cut
@@ -131,7 +158,7 @@
%
% `'hermit'`
% : Method for matrix diagonalization with the following logical values:
-%
+%
% * `true` using Colpa's method (for details see [J.H.P. Colpa, Physica 93A (1978) 327](http://www.sciencedirect.com/science/article/pii/0378437178901607)),
% the dynamical matrix is converted into another Hermitian
% matrix, that will give the real eigenvalues.
@@ -144,7 +171,7 @@
% expected. In this case only White's method work. The solution in this
% case is wrong, however by examining the eigenvalues it can give a hint
% where the problem is.}}
-%
+%
% `'saveH'`
% : If true, the quadratic form of the Hamiltonian is also saved in the
% output. Be carefull, it can take up lots of memory. Default value is
@@ -157,7 +184,8 @@
%
% `'saveSabp'`
% : If true, the dynamical structure factor in the rotating frame
-% $S'(k,\omega)$ is saved. Default value is `false`.
+% $S'(k,\omega)$ is saved. For incommensurate structures only. Default
+% value is `false`.
%
% `'title'`
% : Gives a title string to the simulation that is saved in the output.
@@ -191,6 +219,9 @@
% $S^{xx}$, $S^{xy}$, $S^{xz}$, etc. If given, magnetic form
% factor is included. Intensity is in \\hbar units, normalized
% to the crystallographic unit cell.
+% * `Sperp` The component of `Sab` perpendicular to $Q$, which neutron
+% scattering measures. This is outputed *instead* of `Sab`
+% if the `'neutron_output', true` is specified.
% * `H` Quadratic form of the Hamiltonian. Only saved if `saveH` is
% true.
% * `V` Transformation matrix from the normal magnon modes to the
@@ -201,21 +232,32 @@
% dimensions are $[3\times 3\times n_{mode}\times n_{Q}]$,
% but the number of modes are equal to twice the number of
% magnetic atoms.
-% * `formfact` Cell containing the labels of the magnetic ions if form
-% factor in included in the spin-spin correlation function.
-% * `cmplxBase` The local coordinate system on each magnetic moment is
-% defined by the complex magnetic moments:
-% $\begin{align} e_1 &= \Im(\hat{M})\\
-% e_3 &= Re(\hat{M})\\
-% e_2 &= e_3\times e_1
-% \end{align}$
-%
% * `hkl` Contains the input $Q$ values, dimensions are $[3\times n_{Q}]$.
% * `hklA` Same $Q$ values, but in $\\ang^{-1}$ unit, in the
% lab coordinate system, dimensins are $[3\times n_{Q}]$.
+% * `formfact`Logical value, whether the form factor has been included in
+% the spin-spin correlation function.
% * `incomm` Logical value, tells whether the calculated spectra is
% incommensurate or not.
+% * `helical` Logical value, whether the magnetic structure is a helix
+% i.e. whether 2*k is non-integer.
+% * `norm` Logical value, is always false.
+% * `nformula`Number of formula units in the unit cell that have been
+% used to scale Sab, as given in spinw.unit.nformula.
+% * `param` Struct containing input parameters, each corresponds to the
+% input parameter of the same name:
+% * `notwin`
+% * `sortMode`
+% * `tol`
+% * `omega_tol`
+% * `hermit`
+% * `title` Character array, the title for the output spinwave, default
+% is 'Numerical LSWT spectrum'
+% * `gtensor` Logical value, whether a g-tensor has been included in the
+% calculation.
% * `obj` The copy (clone) of the input `obj`, see [spinw.copy].
+% * `datestart`Character array, start date and time of the calculation
+% * `dateend` Character array, end date and time of the calculation
%
% The number of magnetic modes (labeled by `nMode`) for commensurate
% structures is double the number of magnetic atoms in the magnetic cell.
@@ -250,24 +292,29 @@
% use mex file by default?
useMex = pref.usemex;
+if isa(hkl, 'sym') && ~obj.symbolic
+ obj.symbolic(true);
+ setnosym = onCleanup(@()obj.symbolic(false));
+end
+
% calculate symbolic spectrum if obj is in symbolic mode
if obj.symbolic
if numel(hkl) == 3
hkl = sym(hkl);
end
-
+
if ~isa(hkl,'sym')
inpForm.fname = {'fitmode'};
inpForm.defval = {false };
inpForm.size = {[1 1] };
param0 = sw_readparam(inpForm, varargin{:});
-
+
if ~param0.fitmode
warning('spinw:spinwave:MissingInput','No hkl value was given, spin wave spectrum for general Q (h,k,l) will be calculated!');
end
spectra = obj.spinwavesym(varargin{:});
else
- spectra = obj.spinwavesym(varargin{:},'hkl',hkl);
+ spectra = obj.spinwavesym(varargin{:},'hkl',hkl(:));
end
return
end
@@ -297,8 +344,26 @@
inpForm.defval = [inpForm.defval {false -1 -1 }];
inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] }];
+inpForm.fname = [inpForm.fname {'neutron_output' 'fastmode'}];
+inpForm.defval = [inpForm.defval {false false }];
+inpForm.size = [inpForm.size {[1 1] [1 1] }];
+
param = sw_readparam(inpForm, varargin{:});
+if param.fastmode
+ param.neutron_output = true;
+ param.fitmode = true;
+ param.sortMode = false;
+ if any(useMex) && (param.saveV || param.saveH || param.saveSabp)
+ warning('spinw:spinwave:fastmodewithsave', ...
+ ['You have set both "usemex" and "fastmode" and also ' ...
+ 'requested that S, V or H is saved, but mex files do not ' ...
+ 'support saving intermediate matrices. So in this case ' ...
+ 'the mex files will *not* be used. Set all "save*" ' ...
+ 'options to "false" to use mex files.']);
+ end
+end
+
if ~param.fitmode
% save the time of the beginning of the calculation
spectra.datestart = datestr(now);
@@ -328,6 +393,21 @@
% whether the structure is incommensurate
incomm = any(abs(km-round(km)) > param.tol);
+if incomm && prod(nExt) > 1
+ warning('spinw:spinwave:IncommKinSupercell', ...
+ ['The results for an incommensurate modulation in a ' ...
+ 'supercell have not been scientifically validated.']);
+end
+if ~incomm && param.saveSabp
+ warning('spinw:spinwave:CommensurateSabp', ['The dynamical structure '...
+ 'factor in the rotating frame has been requested, but the ', ...
+ 'structure is commensurate so this will have no effect.']);
+end
+
+% If only one hkl value, convert to column vector
+if numel(hkl) == 3
+ hkl = hkl(:);
+end
% Transform the momentum values to the new lattice coordinate system
hkl = obj.unit.qmat*hkl;
@@ -335,7 +415,7 @@
% Calculates momentum transfer in A^-1 units.
hklA = 2*pi*(hkl'/obj.basisvector)';
-% Check for 2*km
+% Check for 2*km (if 2*km is integer, the magnetic structure is not a true helix)
tol = param.tol*2;
helical = sum(abs(mod(abs(2*km)+tol,1)-tol).^2) > tol;
@@ -370,35 +450,57 @@
warning('spinw:spinwave:Twokm',['The two times the magnetic ordering '...
'wavevector 2*km = G, reciproc lattice vector, use magnetic supercell to calculate spectrum!']);
end
-
+
+ if param.saveSabp
+ Sabp = [];
+ omegap = [];
+ end
+
hkl0 = cell(1,nTwin);
hklExt = cell(1,nTwin);
-
+
for tt = 1:nTwin
% without the k_m: (k, k, k)
hkl0{tt} = repmat(hkl{tt},[1 3]);
-
+
% for wavevectors in the extended unit cell km won't be multiplied by
% nExt (we devide here to cancel the multiplication later)
kme = km./nExt;
hklExt{tt} = [bsxfun(@minus,hkl{tt},kme') hkl{tt} bsxfun(@plus,hkl{tt},kme')];
-
+
% calculate dispersion for (k-km, k, k+km)
hkl{tt} = [bsxfun(@minus,hkl{tt},km') hkl{tt} bsxfun(@plus,hkl{tt},km')];
+ if param.neutron_output
+ hklAf{tt} = [hklA hklA hklA];
+ end
end
nHkl = nHkl*3;
nHkl0 = nHkl0*3;
else
hkl0 = hkl;
hklExt = hkl;
+ helical = false;
end
hkl = cell2mat(hkl);
hkl0 = cell2mat(hkl0);
hklExt = cell2mat(hklExt);
+if param.neutron_output
+ if incomm
+ hklAf = cell2mat(hklAf);
+ elseif ~param.notwin
+ hklAf = repmat(hklA, [1 nTwin]);
+ else
+ hklAf = hklA;
+ end
+ % Normalized scattering wavevector in xyz coordinate system.
+ hklAf = bsxfun(@rdivide, hklAf, sqrt(sum(hklAf.^2, 1)));
+else
+ hklAf = [];
+end
% determines a twin index for every q point
-twinIdx = repmat(1:nTwin,[nHkl 1]);
+twinIdx = repmat(1:nTwin,[nHkl0 1]);
twinIdx = twinIdx(:);
% Create the interaction matrix and atomic positions in the extended
@@ -450,40 +552,25 @@
'(nMagExt = %d, nHkl = %d, nTwin = %d)...\n'],nMagExt, nHkl0, nTwin);
end
-% Local (e1,e2,e3) coordinate system fixed to the moments,
+% If cmplxBase is false, we use a local (e1,e2,e3) coordinate system fixed
+% to the moments:
% e3||Si,ata
% e2 = Si x [1,0,0], if Si || [1,0,0] --> e2 = [0,0,1]
% e1 = e2 x e3
-% Local (e1,e2,e3) coordinate system fixed to the moments.
-% TODO add the possibility that the coordinate system is fixed by the
-% comples magnetisation vectors: e1 = imag(M), e3 = real(M), e2 =
-% cross(e3,e1)
+% If cmplxBase is true, we use a coordinate system fixed by the
+% complex magnetisation vectors:
+% e1 = imag(M)
+% e3 = real(M)
+% e2 = e3 x e1
if ~param.cmplxBase
- if obj.symbolic
- e3 = simplify(M0./[S0; S0; S0]);
- % e2 = Si x [1,0,0], if Si || [1,0,0] --> e2 = [0,0,1]
- e2 = [zeros(1,nMagExt); e3(3,:); -e3(2,:)];
- % select zero vector and make them parallel to [0,0,1]
- selidx = abs(e2)>0;
- if isa(selidx,'sym')
- e2(3,~any(~sw_always(abs(e2)==0))) = 1;
- else
- e2(3,~any(abs(e2)>0)) = 1;
- end
- E0 = sqrt(sum(e2.^2,1));
- e2 = simplify(e2./[E0; E0; E0]);
- % e1 = e2 x e3
- e1 = simplify(cross(e2,e3));
- else
- % e3 || Si
- e3 = bsxfun(@rdivide,M0,S0);
- % e2 = Si x [1,0,0], if Si || [1,0,0] --> e2 = [0,0,1]
- e2 = [zeros(1,nMagExt); e3(3,:); -e3(2,:)];
- e2(3,~any(abs(e2)>1e-10)) = 1;
- e2 = bsxfun(@rdivide,e2,sqrt(sum(e2.^2,1)));
- % e1 = e2 x e3
- e1 = cross(e2,e3);
- end
+ % e3 || Si
+ e3 = bsxfun(@rdivide,M0,S0);
+ % e2 = Si x [1,0,0], if Si || [1,0,0] --> e2 = [0,0,1]
+ e2 = [zeros(1,nMagExt); e3(3,:); -e3(2,:)];
+ e2(3,~any(abs(e2)>1e-10)) = 1;
+ e2 = bsxfun(@rdivide,e2,sqrt(sum(e2.^2,1)));
+ % e1 = e2 x e3
+ e1 = cross(e2,e3);
else
F0 = obj.mag_str.F;
RF0 = sqrt(sum(real(F0).^2,1));
@@ -496,13 +583,6 @@
e1 = e1./repmat(sqrt(sum(e1.^2,1)),[3 1]);
% e2 = cross(e3,e1)
e2 = cross(e3,e1);
-
- if obj.symbolic
- e1 = simplify(e1);
- e2 = simplify(e2);
- e3 = simplify(e3);
- end
-
end
% assign complex vectors that define the rotating coordinate system on
% every magnetic atom
@@ -567,14 +647,14 @@
bqAtom2 = SS.bq(5,:);
bqJJ = SS.bq(6,:);
nbqCoupling = numel(bqJJ);
-
+
% matrix elements: M,N,P,Q
bqM = sum(eta(:,bqAtom1).*eta(:,bqAtom2),1);
bqN = sum(eta(:,bqAtom1).*zed(:,bqAtom2),1);
bqO = sum(zed(:,bqAtom1).*zed(:,bqAtom2),1);
bqP = sum(conj(zed(:,bqAtom1)).*zed(:,bqAtom2),1);
bqQ = sum(zed(:,bqAtom1).*eta(:,bqAtom2),1);
-
+
Si = S0(bqAtom1);
Sj = S0(bqAtom2);
% C_ij matrix elements
@@ -582,28 +662,28 @@
bqB0 = (Si.*Sj).^(3/2).*(bqM.*bqO + bqQ.*bqN).*bqJJ;
bqC = Si.*Sj.^2.*(conj(bqQ).*bqQ - 2*bqM.^2).*bqJJ;
bqD = Si.*Sj.^2.*(bqQ).^2.*bqJJ;
-
+
% Creates the serial indices for every matrix element in ham matrix.
% Aij(k) matrix elements (b^+ b)
idxbqA = [bqAtom1' bqAtom2'];
% b b^+ elements
idxbqA2 = [bqAtom1' bqAtom2']+nMagExt;
-
+
% Bij(k) matrix elements (b^+ b^+)
idxbqB = [bqAtom1' bqAtom2'+nMagExt];
% transpose of B (b b)
%idxbqB2 = [bqAtom2'+nMagExt bqAtom1']; % SP2
-
+
idxbqC = [bqAtom1' bqAtom1'];
idxbqC2 = [bqAtom1' bqAtom1']+nMagExt;
-
+
idxbqD = [bqAtom1' bqAtom1'+nMagExt];
%idxbqD2 = [bqAtom1'+nMagExt bqAtom1]; % SP2
-
+else
+ bqdR = [];
end
-
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% MEMORY MANAGEMENT LOOP
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -639,10 +719,11 @@
fprintf0(fid,[yesNo{param.gtensor+1} ' g-tensor is included in the '...
'calculated structure factor.\n']);
+z1 = zed;
if param.gtensor
-
+
gtensor = SI.g;
-
+
if incomm
% keep the rotation invariant part of g-tensor
nx = [0 -n(3) n(2);n(3) 0 -n(1);-n(2) n(1) 0];
@@ -650,16 +731,13 @@
m1 = eye(3);
gtensor = 1/2*gtensor - 1/2*mmat(mmat(nx,gtensor),nx) + 1/2*mmat(mmat(nxn-m1,gtensor),nxn) + 1/2*mmat(mmat(nxn,gtensor),2*nxn-m1);
end
+ for i1 = 1:size(gtensor, 3)
+ z1(:,i1) = gtensor(:,:,i1) * zed(:,i1);
+ end
end
hklIdx = [floor(((1:nSlice)-1)/nSlice*nHkl)+1 nHkl+1];
-% Empty omega dispersion of all spin wave modes, size: 2*nMagExt x nHkl.
-omega = zeros(2*nMagExt,0);
-
-% empty Sab
-Sab = zeros(3,3,2*nMagExt,0);
-
% Empty matrices to save different intermediate results for further
% analysis: Hamiltonian, eigenvectors, dynamical structure factor in the
% rotating frame
@@ -685,9 +763,97 @@
FF = repmat(param.formfactfun(permute(obj.unit_cell.ff(1,:,obj.matom.idx),[3 2 1]),hklA0),[prod(nExt) 1]);
else
spectra.formfact = false;
+ FF = [];
+end
+
+if (~isstring(useMex) && ~ischar(useMex) && useMex) || strcmp(useMex, 'auto')
+ % For large unit cells, the original mex code is much better because it parallelizes the individual
+ % eigen/cholesky decompositions operations (chol / eig) in addition to parallelising over Q-points
+ % The new code uses Eigen for these operations which is strictly serial so parallises better over Q-points
+ % but will be super slow for large nMagExt
+ if nMagExt > pref.nspinlarge % This threshold needs to be explored more
+ useMex = 'old';
+ else
+ useMex = 'new';
+ end
+end
+
+use_swloop = any(useMex) && strcmp(useMex, 'new') && ~param.saveH && ~param.saveV && ~param.saveSabp;
+
+if ~use_swloop
+ if param.fastmode
+ nMode = nMagExt;
+ else
+ nMode = 2 * nMagExt;
+ end
+
+ % Empty omega dispersion of all spin wave modes, size: 2*nMagExt x nHkl.
+ omega = zeros(2*nMagExt, nHkl);
+ if param.neutron_output
+ Sperp = zeros(nMode, nHkl);
+ else
+ SabFull = zeros(3,3,nMode,nHkl);
+ end
+
+ if incomm
+ % resize matrices due to the incommensurability (k-km,k,k+km) multiplicity
+ kmIdx = repmat(sort(repmat([1 2 3],1,nHkl0/3)),1,nTwin);
+ % Rodrigues' rotation formula.
+ nx = [0 -n(3) n(2); n(3) 0 -n(1); -n(2) n(1) 0];
+ nxn = n'*n;
+ K1 = 1/2*(eye(3) - nxn - 1i*nx);
+ K2 = nxn;
+ m1 = eye(3);
+ end
end
-for jj = 1:nSlice
+if use_swloop
+ pars = struct('hermit', param.hermit, 'omega_tol', param.omega_tol, 'formfact', param.formfact, ...
+ 'incomm', incomm, 'helical', helical, 'nTwin', nTwin, 'bq', any(bq), 'field', any(SI.field), ...
+ 'nThreads', pref.nthread, 'n', n, 'rotc', obj.twin.rotc, 'fastmode', param.fastmode, ...
+ 'neutron_output', param.neutron_output, 'hklA', hklAf, 'nformula', obj.unit.nformula, ...
+ 'nCell', prod(nExt));
+ ham_diag = diag(accumarray([idxA2; idxD2], 2*[A20 D20], [1 1]*2*nMagExt));
+ idxAll = [idxA1; idxB; idxD1]; ABCD = [AD0 2*BC0 conj(AD0)];
+ bqABCD = []; bq_ham_d = []; idxBq = []; ham_MF = {};
+ if pars.bq
+ bqABCD = [bqA0 conj(bqA0) 2*bqB0];
+ idxBq = [idxbqA; idxbqA2; idxbqB];
+ bq_ham_d = diag(accumarray([idxbqC; idxbqC2; idxbqD], [bqC bqC 2*bqD], [1 1]*2*nMagExt));
+ assert(isreal(bqABCD), 'Internal logical error');
+ end
+ if pars.field
+ for ii = 1:nTwin
+ ham_MF{ii} = accumarray(idxMF, MF(:,:,ii), [1 1]*2*nMagExt);
+ end
+ end
+ try
+ [omega, Sab, warn1, orthWarn0] = swloop(pars, hklExt, ...
+ ABCD, idxAll, ham_diag, dR, RR, S0, z1, FF, bqdR, bqABCD, idxBq, bq_ham_d, ham_MF);
+ catch err
+ if ~isempty(strfind(err.message, 'notposdef'))
+ error('chol_omp:notposdef', 'Hamiltonian is not positive definite');
+ elseif ~isempty(strfind(err.message, 'Eigensolver'))
+ error('swloop:notconverge', 'Could not determine eigenvalues of Hamiltonian');
+ else
+ rethrow(err);
+ end
+ end
+ if param.hermit && sum(abs(imag(omega(:)))) < 1e-5
+ omega = real(omega);
+ end
+ if param.neutron_output
+ Sperp = Sab;
+ clear Sab;
+ end
+ if incomm
+ nHkl0 = nHkl0/3;
+ nHklT = nHkl / nTwin;
+ kmIdx = cell2mat(arrayfun(@(x) (x+nHkl0):(x+2*nHkl0-1), [0:(nTwin-1)]*nHklT + 1, 'UniformOutput', false));
+ hkl = hkl(:, kmIdx);
+ end
+else
+ for jj = 1:nSlice
% q indices selected for every chunk
hklIdxMEM = hklIdx(jj):(hklIdx(jj+1)-1);
% q values contatining the k_m vector
@@ -697,20 +863,20 @@
% twin indices for every q point
twinIdxMEM = twinIdx(hklIdxMEM);
nHklMEM = size(hklExtMEM,2);
-
+
% Creates the matrix of exponential factors nCoupling x nHkl size.
% Extends dR into 3 x 3 x nCoupling x nHkl
% ExpF = exp(1i*permute(sum(repmat(dR,[1 1 nHklMEM]).*repmat(...
% permute(hklExtMEM,[1 3 2]),[1 nCoupling 1]),1),[2 3 1]))';
ExpF = exp(1i*permute(sum(bsxfun(@times,dR,permute(hklExtMEM,[1 3 2])),1),[2 3 1]))';
-
+
% Creates the matrix elements containing zed.
A1 = bsxfun(@times, AD0 ,ExpF);
B = bsxfun(@times, BC0 ,ExpF);
D1 = bsxfun(@times,conj(AD0),ExpF);
-
-
-
+
+
+
% Store all indices
% SP1: speedup for creating the matrix elements
%idxAll = [idxA1; idxB; idxC; idxD1]; % SP1
@@ -718,26 +884,26 @@
% Store all matrix elements
%ABCD = [A1 B conj(B) D1]; % SP1
ABCD = [A1 2*B D1];
-
+
% Stores the matrix elements in ham.
%idx3 = repmat(1:nHklMEM,[4*nCoupling 1]); % SP1
idx3 = repmat(1:nHklMEM,[3*nCoupling 1]);
idxAll = [repmat(idxAll,[nHklMEM 1]) idx3(:)];
idxAll = idxAll(:,[2 1 3]);
-
+
ABCD = ABCD';
-
-
+
+
% quadratic form of the boson Hamiltonian stored as a square matrix
ham = accumarray(idxAll,ABCD(:),[2*nMagExt 2*nMagExt nHklMEM]);
-
+
ham = ham + repmat(accumarray([idxA2; idxD2],2*[A20 D20],[1 1]*2*nMagExt),[1 1 nHklMEM]);
-
+
if any(bq)
% bqExpF = exp(1i*permute(sum(repmat(bqdR,[1 1 nHklMEM]).*repmat(...
% permute(hklExtMEM,[1 3 2]),[1 nbqCoupling 1]),1),[2 3 1]))';
bqExpF = exp(1i*permute(sum(bsxfun(@times,bqdR,permute(hklExtMEM,[1 3 2])),1),[2 3 1]))';
-
+
bqA = bsxfun(@times, bqA0, bqExpF);
bqA2 = bsxfun(@times,conj(bqA0),bqExpF);
bqB = bsxfun(@times, bqB0, bqExpF);
@@ -752,7 +918,7 @@
ham = ham + accumarray(idxbqAll,bqABCD(:),[2*nMagExt 2*nMagExt nHklMEM]);
% add diagonal terms
ham = ham + repmat(accumarray([idxbqC; idxbqC2; idxbqD],[bqC bqC 2*bqD],[1 1]*2*nMagExt),[1 1 nHklMEM]);
-
+
end
if any(SI.field)
% different field for different twin
@@ -761,36 +927,44 @@
ham(:,:,twinIdxMEM==ii) = ham(:,:,twinIdxMEM==ii) + ...
repmat(accumarray(idxMF,MF(:,:,ii),[1 1]*2*nMagExt),[1 1 nTwinQ]);
end
-
+
%ham = ham + repmat(accumarray(idxMF,MF,[1 1]*2*nMagExt),[1 1 nHklMEM]);
end
-
+
ham = (ham + conj(permute(ham,[2 1 3])))/2;
-
+
% diagonal of the boson commutator matrix
gCommd = [ones(nMagExt,1); -ones(nMagExt,1)];
% boson commutator matrix
gComm = diag(gCommd);
%gd = diag(g);
-
+
if param.hermit
% All the matrix calculations are according to Colpa's paper
% J.H.P. Colpa, Physica 93A (1978) 327-353
-
+
% basis functions of the magnon modes
V = zeros(2*nMagExt,2*nMagExt,nHklMEM);
-
- if useMex && nHklMEM>1
+
+ if any(useMex) && nHklMEM>1
% use mex files to speed up the calculation
% mex file will return an error if the matrix is not positive definite.
- [K2, invK] = chol_omp(ham,'Colpa','tol',param.omega_tol);
- [V, omega(:,hklIdxMEM)] = eig_omp(K2,'sort','descend');
+ [Ksq, invK] = chol_omp(ham,'Colpa','tol',param.omega_tol);
+ [V, omega(:,hklIdxMEM)] = eig_omp(Ksq,'sort','descend');
% the inverse of the para-unitary transformation V
- for ii = 1:nHklMEM
- V(:,:,ii) = V(:,:,ii)*diag(sqrt(gCommd.*omega(:,hklIdxMEM(ii))));
+ if param.fastmode
+ % Only transform the positive energy modes (first half of V)
+ for ii = 1:nMagExt
+ V(:,ii,:) = bsxfun(@times, squeeze(V(:,ii,:)), sqrt(omega(ii,hklIdxMEM)));
+ end
+ V = sw_mtimesx(invK, V(:,1:nMagExt,:));
+ else
+ for ii = 1:nHklMEM
+ V(:,:,ii) = V(:,:,ii)*diag(sqrt(gCommd.*omega(:,hklIdxMEM(ii))));
+ end
+ % V = bsxfun(@times, invK, V);
+ V = sw_mtimesx(invK, V);
end
- %V = mtimesx(invK,V);
- V = bsxfun(@times,invK,V);
else
for ii = 1:nHklMEM
[K, posDef] = chol(ham(:,:,ii));
@@ -822,95 +996,187 @@
' diagonalization try the param.hermit=false option']);
end
end
-
- K2 = K*gComm*K';
- K2 = 1/2*(K2+K2');
- % Hermitian K2 will give orthogonal eigenvectors
- [U, D] = eig(K2);
+
+ Ksq = K*gComm*K';
+ Ksq = 1/2*(Ksq+Ksq');
+ % Hermitian Ksq will give orthogonal eigenvectors
+ [U, D] = eig(Ksq);
D = diag(D);
-
+
% sort modes accordign to the real part of the energy
[~, idx] = sort(real(D),'descend');
U = U(:,idx);
% omega dispersion
- omega(:,end+1) = D(idx); %#ok
-
+ omega(:, hklIdxMEM(ii)) = D(idx);
+
% the inverse of the para-unitary transformation V
- V(:,:,ii) = inv(K)*U*diag(sqrt(gCommd.*omega(:,end))); %#ok
+ V(:,:,ii) = inv(K)*U*diag(sqrt(gCommd.*omega(:, hklIdxMEM(ii)))); %#ok
end
end
else
% All the matrix calculations are according to White's paper
% R.M. White, et al., Physical Review 139, A450?A454 (1965)
-
- gham = mmat(gComm,ham);
-
+ if any(useMex)
+ gham = sw_mtimesx(gComm, ham);
+ else
+ gham = mmat(gComm,ham);
+ end
+
[V, D, orthWarn] = eigorth(gham,param.omega_tol,useMex);
-
+
orthWarn0 = orthWarn || orthWarn0;
-
- for ii = 1:nHklMEM
- % multiplication with g removed to get negative and positive
- % energies as well
- omega(:,end+1) = D(:,ii); %#ok
- M = diag(gComm*V(:,:,ii)'*gComm*V(:,:,ii));
- V(:,:,ii) = V(:,:,ii)*diag(sqrt(1./M));
+
+ if param.fastmode
+ % Only transform the positive energy modes (first half of V)
+ omega(1:nMagExt, hklIdxMEM) = D(1:nMagExt,:);
+ for ii = 1:nMagExt
+ V(:,ii,:) = bsxfun(@times, V(:,ii,:), sqrt(1 ./ sum(bsxfun(@times,gCommd,conj(V(:,ii,:)).*V(:,ii,:)))));
+ end
+ else
+ for ii = 1:nHklMEM
+ % multiplication with g removed to get negative and positive
+ % energies as well
+ omega(:,hklIdxMEM(ii)) = D(:,ii);
+ M = diag(gComm*V(:,:,ii)'*gComm*V(:,:,ii));
+ V(:,:,ii) = V(:,:,ii)*diag(sqrt(1./M));
+ end
end
end
-
+
if param.saveV
Vsave(:,:,hklIdxMEM) = V;
end
if param.saveH
Hsave(:,:,hklIdxMEM) = ham;
end
-
+
+ if param.fastmode
+ V = V(:,1:nMagExt,:);
+ end
+
% Calculates correlation functions.
% V right
VExtR = repmat(permute(V ,[4 5 1 2 3]),[3 3 1 1 1]);
% V left: conjugate transpose of V
VExtL = conj(permute(VExtR,[1 2 4 3 5]));
-
+
% Introduces the exp(-ikR) exponential factor.
ExpF = exp(-1i*sum(repmat(permute(hklExt0MEM,[1 3 2]),[1 nMagExt 1]).*repmat(RR,[1 1 nHklMEM]),1));
% Includes the sqrt(Si/2) prefactor.
ExpF = ExpF.*repmat(sqrt(S0/2),[1 1 nHklMEM]);
-
- ExpFL = repmat(permute(ExpF,[1 4 5 2 3]),[3 3 2*nMagExt 2]);
+
+ ExpFL = repmat(permute(ExpF,[1 4 5 2 3]),[3 3 nMode 2]);
% conj transpose of ExpFL
ExpFR = conj(permute(ExpFL,[1 2 4 3 5]));
-
- zeda = repmat(permute([zed conj(zed)],[1 3 4 2]),[1 3 2*nMagExt 1 nHklMEM]);
+
+ zeda = repmat(permute([zed conj(zed)],[1 3 4 2]),[1 3 nMode 1 nHklMEM]);
% conj transpose of zeda
zedb = conj(permute(zeda,[2 1 4 3 5]));
-
+
% calculate magnetic structure factor using the hklExt0 Q-values
% since the S(Q+/-k,omega) correlation functions also belong to the
% F(Q)^2 form factor
-
+
if param.formfact
% include the form factor in the z^alpha, z^beta matrices
- zeda = zeda.*repmat(permute(FF(:,hklIdxMEM),[3 4 5 1 2]),[3 3 2*nMagExt 2 1]);
- zedb = zedb.*repmat(permute(FF(:,hklIdxMEM),[3 4 1 5 2]),[3 3 2 2*nMagExt 1]);
+ zeda = zeda.*repmat(permute(FF(:,hklIdxMEM),[3 4 5 1 2]),[3 3 nMode 2 1]);
+ zedb = zedb.*repmat(permute(FF(:,hklIdxMEM),[3 4 1 5 2]),[3 3 2 nMode 1]);
end
-
+
if param.gtensor
% include the g-tensor
zeda = mmat(repmat(permute(gtensor,[1 2 4 3]),[1 1 1 2]),zeda);
zedb = mmat(zedb,repmat(gtensor,[1 1 2]));
end
+
+
% Dynamical structure factor from S^alpha^beta(k) correlation function.
% Sab(alpha,beta,iMode,iHkl), size: 3 x 3 x 2*nMagExt x nHkl.
% Normalizes the intensity to single unit cell.
- Sab = cat(4,Sab,squeeze(sum(zeda.*ExpFL.*VExtL,4)).*squeeze(sum(zedb.*ExpFR.*VExtR,3))/prod(nExt));
-
- sw_timeit(jj/nSlice*100,0,param.tid);
-end
+ Sab = reshape(sum(zeda.*ExpFL.*VExtL,4),[3 3 nMode nHklMEM]) .* ...
+ reshape(sum(zedb.*ExpFR.*VExtR,3),[3 3 nMode nHklMEM]) / prod(nExt);
+
+ if incomm
+ if helical
+ % integrating out the arbitrary initial phase of the helix
+ Sab = 1/2*Sab - 1/2*mmat(mmat(nx,Sab),nx) + 1/2*mmat(mmat(nxn-m1,Sab),nxn) + 1/2*mmat(mmat(nxn,Sab),2*nxn-m1);
+ end
+ kmIdxMEM = kmIdx(hklIdxMEM);
+
+ % Save the structure factor in the rotating frame
+ if param.saveSabp
+ Sabp = cat(4, Sabp, Sab(:,:,:,kmIdxMEM==2));
+ omegap = cat(2, omega(:,kmIdxMEM==2));
+ end
+
+ % Rotate Sab back into lab frame
+ Sab(:,:,:,kmIdxMEM==1) = mmat(Sab(:,:,:,kmIdxMEM==1), K1);
+ Sab(:,:,:,kmIdxMEM==2) = mmat(Sab(:,:,:,kmIdxMEM==2), K2);
+ Sab(:,:,:,kmIdxMEM==3) = mmat(Sab(:,:,:,kmIdxMEM==3), conj(K1));
+ end
+
+ if ~param.notwin
+ % Rotate the calculated correlation function into the twin coordinate system using rotC
+ for ii = 1:nTwin
+ % select the ii-th twin from the Q points
+ idx = find((hklIdxMEM <= (nHkl0 * ii)) .* (hklIdxMEM > (nHkl0 * (ii-1))));
+ if ~isempty(idx)
+ % convert the matrix into cell of 3x3 matrices
+ SabT = reshape(Sab(:,:,:,idx),3,3,[]);
+ % select the rotation matrix of twin ii
+ rotC = obj.twin.rotc(:,:,ii);
+ % rotate correlation function using arrayfun
+ SabRot = arrayfun(@(idx)(rotC*SabT(:,:,idx)*(rotC')),1:size(SabT,3),'UniformOutput',false);
+ SabRot = cat(3,SabRot{:});
+ % resize back the correlation matrix
+ Sab(:,:,:,idx) = reshape(SabRot, [3 3 nMode numel(idx)]);
+ end
+ end
+ end
+
+ if param.neutron_output
+ if obj.unit.nformula > 0
+ Sab = Sab/double(obj.unit.nformula);
+ end
-if param.sortMode
- % sort the spin wave modes
- [omega, Sab] = sortmode(omega,reshape(Sab,9,size(Sab,3),[]));
- Sab = reshape(Sab,3,3,size(Sab,2),[]);
+ % get symmetric component of Sab only
+ Sab = (Sab + permute(Sab,[2 1 3 4]))/2;
+
+ % Normalized scattering wavevector in xyz coordinate system.
+ hklAN = hklAf(:, hklIdxMEM);
+
+ % avoid NaN for Q=0
+ NaNidx = find(any(isnan(hklAN)));
+ for kk = 1:numel(NaNidx)
+ if NaNidx(kk) < size(hklAN,2)
+ hklAN(:,NaNidx(kk)) = hklAN(:,NaNidx(kk)+1);
+ else
+ hklAN(:,NaNidx(kk)) = [1;0;0];
+ end
+ end
+
+ hkla = repmat(permute(hklAN,[1 3 2]),[1 3 1]);
+ hklb = repmat(permute(hklAN,[3 1 2]),[3 1 1]);
+
+ % Perpendicular part of the scattering wavevector.
+ qPerp = repmat(eye(3),[1 1 numel(hklIdxMEM)])- hkla.*hklb;
+ qPerp = repmat(permute(qPerp,[1 2 4 3]),[1 1 nMode 1]);
+
+ % Dynamical structure factor for neutron scattering
+ % Sperp: nMode x nHkl.
+ Sperp(:,hklIdxMEM) = permute(sumn(qPerp.*Sab,[1 2]),[3 4 1 2]);
+ else
+ SabFull(:,:,:,hklIdxMEM) = Sab;
+ end
+
+ sw_timeit(jj/nSlice*100,0,param.tid);
+ end
+ if param.fastmode
+ omega = omega(1:nMagExt,:);
+ end
+ if ~param.neutron_output
+ Sab = SabFull;
+ end
end
[~,singWarn] = lastwarn;
@@ -919,7 +1185,7 @@
% If number of formula units are given per cell normalize to formula
% unit
-if obj.unit.nformula > 0
+if obj.unit.nformula > 0 && ~param.neutron_output
Sab = Sab/double(obj.unit.nformula);
end
@@ -936,92 +1202,62 @@
% END MEMORY MANAGEMENT LOOP
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-if incomm
- % resize matrices due to the incommensurability (k-km,k,k+km) multiplicity
- kmIdx = repmat(sort(repmat([1 2 3],1,nHkl0/3)),1,nTwin);
- % Rodrigues' rotation formula.
- nx = [0 -n(3) n(2); n(3) 0 -n(1); -n(2) n(1) 0];
- nxn = n'*n;
- K1 = 1/2*(eye(3) - nxn - 1i*nx);
- K2 = nxn;
-
- % keep the rotation invariant part of Sab
- %nx = [0 -n(3) n(2);n(3) 0 -n(1);-n(2) n(1) 0];
- %nxn = n'*n;
- m1 = eye(3);
-
- % if the 2*km vector is integer, the magnetic structure is not a true
- % helix
- %tol = param.tol*2;
- %helical = sum(abs(mod(abs(2*km)+tol,1)-tol).^2) > tol;
-
- if helical
- % integrating out the arbitrary initial phase of the helix
- Sab = 1/2*Sab - 1/2*mmat(mmat(nx,Sab),nx) + 1/2*mmat(mmat(nxn-m1,Sab),nxn) + 1/2*mmat(mmat(nxn,Sab),2*nxn-m1);
- end
-
- % Save the structure factor in the rotating frame
- if param.saveSabp
- Sabp = Sab(:,:,:,kmIdx==2);
- omegap = omega(:,kmIdx==2);
- end
-
- % dispersion
+if incomm && ~use_swloop
+ % Rearrange the Sab and omega matrices
omega = [omega(:,kmIdx==1); omega(:,kmIdx==2); omega(:,kmIdx==3)];
- % exchange matrices
- Sab = cat(3,mmat(Sab(:,:,:,kmIdx==1),K1), mmat(Sab(:,:,:,kmIdx==2),K2), ...
- mmat(Sab(:,:,:,kmIdx==3),conj(K1)));
-
+ if ~param.neutron_output
+ Sab = cat(3, Sab(:,:,:,kmIdx==1), Sab(:,:,:,kmIdx==2), Sab(:,:,:,kmIdx==3));
+ else
+ Sperp = [Sperp(:,kmIdx==1); Sperp(:,kmIdx==2); Sperp(:,kmIdx==3)];
+ end
hkl = hkl(:,kmIdx==2);
nHkl0 = nHkl0/3;
-else
- helical = false;
end
-if ~param.notwin
- % Rotate the calculated correlation function into the twin coordinate
- % system using rotC
- SabAll = cell(1,nTwin);
- for ii = 1:nTwin
- % select the ii-th twin from the Q points
- idx = (1:nHkl0) + (ii-1)*nHkl0;
- % select correlation function of twin ii
- SabT = Sab(:,:,:,idx);
- % size of the correlation function matrix
- sSabT = size(SabT);
- % convert the matrix into cell of 3x3 matrices
- SabT = reshape(SabT,3,3,[]);
- % select the rotation matrix of twin ii
- rotC = obj.twin.rotc(:,:,ii);
- % rotate correlation function using arrayfun
- SabRot = arrayfun(@(idx)(rotC*SabT(:,:,idx)*(rotC')),1:size(SabT,3),'UniformOutput',false);
- SabRot = cat(3,SabRot{:});
- % resize back the correlation matrix
- SabAll{ii} = reshape(SabRot,sSabT);
+if ~param.notwin && nTwin > 1
+ omega = mat2cell(omega, size(omega, 1), repmat(nHkl0, [1 nTwin]));
+ if param.neutron_output
+ Sperp = mat2cell(Sperp, size(Sperp, 1), repmat(nHkl0, [1 nTwin]));
+ else
+ Sab = squeeze(mat2cell(Sab, 3, 3, size(Sab, 3), repmat(nHkl0, [1 nTwin])))';
end
- Sab = SabAll;
-
- if nTwin == 1
- Sab = Sab{1};
+end
+
+if param.sortMode && ~param.neutron_output
+ if ~param.notwin
+ for ii = 1:nTwin
+ % sort the spin wave modes
+ [omega{ii}, Sab{ii}] = sortmode(omega{ii},reshape(Sab{ii},9,size(Sab{ii},3),[]));
+ Sab{ii} = reshape(Sab{ii},3,3,size(Sab{ii},2),[]);
+ end
else
- omega = mat2cell(omega,size(omega,1),repmat(nHkl0,[1 nTwin]));
+ % sort the spin wave modes
+ [omega, Sab] = sortmode(omega,reshape(Sab,9,size(Sab,3),[]));
+ Sab = reshape(Sab,3,3,size(Sab,2),[]);
end
-
end
% Creates output structure with the calculated values.
spectra.omega = omega;
-spectra.Sab = Sab;
+if param.neutron_output
+ spectra.Sperp = Sperp;
+else
+ spectra.Sab = Sab;
+end
spectra.hkl = obj.unit.qmat\hkl(:,1:nHkl0);
spectra.hklA = hklA;
spectra.incomm = incomm;
spectra.helical = helical;
spectra.norm = false;
-spectra.nformula = double(obj.unit.nformula);
+spectra.nformula = int32(obj.unit.nformula);
% Save different intermediate results.
if param.saveV
- spectra.V = Vsave;
+ if param.notwin
+ spectra.V = Vsave;
+ else
+ spectra.V = mat2cell(Vsave, size(Vsave, 1), size(Vsave, 1), repmat(nHkl0, [1 nTwin]));
+ end
end
if param.saveH
spectra.H = Hsave;
@@ -1040,7 +1276,11 @@
spectra.title = param.title;
spectra.gtensor = param.gtensor;
-if ~param.fitmode
+if param.fitmode
+ % Copies only fields needed by downstream functions (sw_egrid, sw_neutron, sw_plotspec)
+ spectra.obj = struct('single_ion', obj.single_ion, 'twin', obj.twin, ...
+ 'unit', obj.unit, 'basisvector', obj.basisvector, 'nmagext', obj.nmagext);
+else
spectra.dateend = datestr(now);
spectra.obj = copy(obj);
end
@@ -1058,13 +1298,8 @@
end
if strcmp(singWarn,'MATLAB:nearlySingularMatrix')
- lineLink = 'line 846';
- if feature('HotLinks')
- lineLink = ['' lineLink ''];
- end
warning('spinw:spinwave:nearlySingularMatrix',['Matrix is close '...
- 'to singular or badly scaled. Results may be inaccurate.\n> In spinw/spinwave (' lineLink ')']);
- %fprintf(repmat('\b',[1 30]));
+ 'to singular or badly scaled. Results may be inaccurate.']);
end
-end
\ No newline at end of file
+end
diff --git a/swfiles/@spinw/symbolic.m b/swfiles/@spinw/symbolic.m
index 9a8f89352..fc4ef35fd 100644
--- a/swfiles/@spinw/symbolic.m
+++ b/swfiles/@spinw/symbolic.m
@@ -37,8 +37,7 @@
switch symb
case true
- v = ver;
- if ~any(strcmp('Symbolic Math Toolbox', {v.Name}))
+ if ~license('checkout','Symbolic_Toolbox')
error('spinw:symbolic:NoToolBox','You need Symbolic Math Toolbox installed to run symbolic calculations!');
end
diff --git a/swfiles/@swpref/private/datastruct.m b/swfiles/@swpref/private/datastruct.m
index 1a167228e..cdba05a98 100644
--- a/swfiles/@swpref/private/datastruct.m
+++ b/swfiles/@swpref/private/datastruct.m
@@ -33,7 +33,9 @@
'tid',...
'colormap',...
'usemex',...
- 'docurl'
+ 'docurl',...
+ 'nthread',...
+ 'nspinlarge'
};
Size = {
@@ -46,8 +48,10 @@
[1, 1],...
[1, 1],...
[1, 1],...
+ [ ],...
+ [ ],...
[1, 1],...
- [ ]
+ [1, 1]
};
d.Validation = {
@@ -59,9 +63,11 @@
{@isnumeric, @mustBeInteger, @mustBeNonnegative, @(x) check_size(x,Size{6}), @(x) mustBeGreaterThan(x,1), @(x) mustBeLessThan(x,256)},...
{@isnumeric, @mustBeInteger, @mustBeNonnegative, @(x) check_size(x,Size{7}), @(x) mustBeGreaterThan(x,4), @(x) mustBeLessThan(x,256)},...
{@isnumeric, @mustBeInteger, @mustBeNonnegative, @(x) check_size(x,Size{8}), @(x) mustBeLessThan(x,256)},...
- {@(x) check_size(x,Size{9}), @check_mex, @(x) isa(x,'function_handle')},...
- {@(x) check_size(x,Size{10}), @islogical},...
- {@ischar, @(x) strfind(x,'http')}
+ {@(x) check_size(x,Size{9}), @(x) isa(x,'function_handle')},...
+ {@(x) check_mex(x)},...
+ {@ischar, @(x) strfind(x,'http')},...
+ {@isnumeric, @(x) check_size(x,Size{12})},...
+ {@isnumeric, @(x) check_size(x,Size{12}), @mustBeNonnegative}
};
d.Value = {
@@ -73,9 +79,11 @@
20,...
12,...
1,...
- @cm_inferno,...
+ @(x)flipud(cm_inferno(x)),...
false,...
- 'https://tsdev.github.io/spinwdoc'
+ 'https://spinw.github.io/spinwdoc',...
+ -1,...
+ 120
};
d.Label = {
@@ -90,6 +98,8 @@
'default colormap'...
'if true, mex files are used in the spin wave calculation'...
'url to the documentation server'...
+ 'number of threads when running parallel mex computations (-1 to automatically define from number of cores)'...
+ 'number of spins in a unit cell to be considered a "large" system (mex calcs use different algorithsm for large and small systems)'
};
function out = check_size(obj,S)
@@ -98,12 +108,12 @@
% {{warning Internal function for the Spin preferences.}}
%
% ### Syntax
- %
+ %
% 'logical = check_size(toBeChecked,size)'
%
% ### Description
%
- % 'logical = check_size(toBeChecked,size)' checks to see if an
+ % 'logical = check_size(toBeChecked,size)' checks to see if an
% object 'obj 'is the expected size given by 'size'. An error is
% thrown if there is a difference.
%
@@ -116,26 +126,143 @@
end
end
- function out = check_mex(~)
+ function out = check_mex(val)
% checks to see if mex files are available.
%
% {{warning Internal function for the Spin preferences.}}
%
% ### Syntax
- %
+ %
% 'logical = check_mex(obj)'
%
% ### Description
%
- % 'logical = check_mex(obj)' checks to see if files 'chol_omp' and
- % 'eig_omp' are present in the MATLAB path.An error is thrown if
+ % 'logical = check_mex(obj)' checks to see if files 'chol_omp' and
+ % 'eig_omp' are present in the MATLAB path.An error is thrown if
% they do not exist.
%
+ % Do the checks only if we are trying to set usemex = true
+ if val == 0
+ out = 0;
+ return
+ end
+
if ~(exist('chol_omp','file')==3 && exist('eig_omp','file')==3)
- error('spref:MissingMex','Necessary mex files are missing, compile them!')
+ % There is a path error for < R2017a
+ if (exist('chol_omp','file')==2 && exist('eig_omp','file')==2)
+ p = which('chol_omp');
+ if ~(exist('chol_omp','file')==3 && exist('eig_omp','file')==3)
+ error('spref:MissingMex','Necessary mex files are missing, compile them!')
+ end
+ else
+ error('spref:MissingMex','Necessary mex files are missing, compile them!')
+ end
else
out = 1;
end
end
-end
\ No newline at end of file
+
+ function mustBeInteger(A)
+ % Validate that value is integer or issue error
+ %
+ % {{warning Internal function for the Spin preferences.}}
+ %
+ % MUSTBEINTEGER(A) issues an error if A contains non integer values.
+ % A value is integer if it is real, finite, and equal to the result
+ % of taking the floor of the value.
+ %
+ % Modified from mathworks verion for < R2017a
+
+ ME = MException('MATLAB:validators:mustBeNumericOrLogical','Value must be integer.');
+ if ~all(isnumeric(A) || islogical(A))
+ throwAsCaller(ME)
+ end
+
+ if ~isreal(A)
+ throwAsCaller(ME)
+ end
+
+ if ~all(isfinite(A(:))) || ~all(A(:) == floor(A(:)))
+ throwAsCaller(ME)
+ end
+ end
+
+ function mustBeNonnegative(A)
+ % Validate that value is nonnegative or issue error
+ %
+ % {{warning Internal function for the Spin preferences.}}
+ %
+ % MUSTBENONNEGATIVE(A) issues an error if A contains negaitive values.
+ % A value is nonnegative if it is greater than or equal to zero.
+ %
+ % Modified from mathworks verion for < R2017a
+
+ if ~all(A(:) >= 0)
+ ME = MException('MATLAB:validators:mustBeNonnegative','Value/s must be positive.');
+ throwAsCaller(ME)
+ end
+ end
+ function mustBeGreaterThan(A, B)
+ % Validate that value is greater than a specified value or issue error
+ %
+ % {{warning Internal function for the Spin preferences.}}
+ %
+ % MUSTBEGREATERTHAN(A,B) issues an error if A is not greater than B.
+ % MATLAB calls gt to determine if A is greater than B.
+ %
+ % Modified from mathworks verion for < R2017a
+
+ if ~all(A(:) > B)
+ ME = MException('MATLAB:validators:mustBeGreaterThan','All values must be greater than %f.',B);
+ throwAsCaller(ME)
+ end
+ end
+ function mustBeLessThan(A, B)
+ % Validate that value is less than a specified value or issue error
+ %
+ % {{warning Internal function for the Spin preferences.}}
+ %
+ % MUSTBELESSTHAN(A,B) issues an error if A is not less than B.
+ % MATLAB calls gt to determine if A is less than B.
+ %
+ % Modified from mathworks verion for < R2017a
+
+ if ~all(A(:) < B)
+ ME = MException('MATLAB:validators:mustBeLessThan','All values must be less than %f.',B);
+ throwAsCaller(ME)
+ end
+ end
+
+ function mustBeGreaterThanOrEqual(A, B)
+ % Validate that value is greater than or equal to a specified value or issue error
+ %
+ % {{warning Internal function for the Spin preferences.}}
+ %
+ % MUSTBEGREATERTHANOREQUAL(A,B) issues an error if A is not greater than or equal to B.
+ % MATLAB calls gt to determine if A is greater than or equal to B.
+ %
+ % Modified from mathworks verion for < R2017a
+
+ if ~all(A(:) >= B)
+ ME = MException('MATLAB:validators:mustBeGreaterThanOrEqual','All values must be greater than or equal to %f.',B);
+ throwAsCaller(ME)
+ end
+ end
+
+ function mustBeLessThanOrEqual(A, B)
+ % Validate that value is less than or equal to a specified value or issue error
+ %
+ % {{warning Internal function for the Spin preferences.}}
+ %
+ % MUSTBELESSTHANOREQUAL(A,B) issues an error if A is not less or equal than B.
+ % MATLAB calls gt to determine if A is less than oir equal B.
+ %
+ % Modified from mathworks verion for < R2017a
+
+ if ~all(A(:) <= B)
+ ME = MException('MATLAB:validators:mustBeLessThanOrEqual','All values must be less than or equal to %f.',B);
+ throwAsCaller(ME)
+ end
+ end
+end
diff --git a/swfiles/Contents.m b/swfiles/Contents.m
index 630563379..90cebc9a0 100644
--- a/swfiles/Contents.m
+++ b/swfiles/Contents.m
@@ -113,4 +113,4 @@
% sw_uniquetol
% sw_update
% sw_version
-% sw_mex
+% sw_mex
diff --git a/swfiles/functionSignatures.json b/swfiles/functionSignatures.json
deleted file mode 100644
index 4524b5362..000000000
--- a/swfiles/functionSignatures.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
-"spinw":
-{
- "inputs":
- [
- {"name":"filename", "kind":"optional", "type":[["filepath"], ["char"]]}
- ]
-},
-"spinwave":
-{
- "inputs":
- [
- {"name": "obj","kind": "required","type": "spinw"},
- {"name": "formFact","kind": "namevalue","type": "char"}
- ]
-},
-"genmagstr":
-{
- "inputs":
- [
- {"name": "obj","kind": "required","type": "spinw"},
- {"name": "mode","kind": "namevalue","type": "choices={'random','direct','tile','helical','rotate','func','fourier'}"},
- {"name": "phi","kind": "namevalue","type": ["numeric" "scalar"]},
- {"name": "phid","kind": "namevalue","type": ["numeric" "scalar"]},
- {"name": "nExt","kind": "namevalue","type": "numeric"},
- {"name": "k","kind": "namevalue","type": "double"},
- {"name": "phi","kind": "namevalue","type": "double"},
- {"name": "n","kind": "namevalue","type": "double"},
- {"name": "S","kind": "namevalue","type": "double"},
- {"name": "unitS","kind": "namevalue","type": "choices={'xyz','lu'}"},
- {"name": "epsilon","kind": "namevalue","type": "double"},
- {"name": "func","kind": "namevalue","type": "function_handle"},
- {"name": "x0","kind": "namevalue","type": "double"},
- {"name": "norm","kind": "namevalue","type": [["logical"],["double"],["int32"]]},
- {"name": "x0","kind": "namevalue","type": "double"}
- ]
-},
-"swdoc":
-{
- "inputs":
- [
- {"name":"name", "kind":"optional", "type":"identifier=variable,function,localfunction,package,classdef"}
- ]
-},
-"swdoc":
-{
- "inputs":
- [
- {"name":"filename", "kind":"optional", "type":"filepath"}
- ]
-}
-}
\ No newline at end of file
diff --git a/swfiles/private/sw_issymspec.m b/swfiles/private/sw_issymspec.m
new file mode 100644
index 000000000..521a12266
--- /dev/null
+++ b/swfiles/private/sw_issymspec.m
@@ -0,0 +1,39 @@
+function issym = sw_issymspec(spectra)
+% Checks if a given spectrum structure is symbolic or not
+%
+% ### Syntax
+%
+% `issym = sw_issymspec(spectra)`
+%
+% ### Description
+%
+% Calling spinwave() for a symbolic spinw object results in
+% a symbolic spectrum which cannot be used with sw_neutron etc.
+% This function checks if a given spectrum is symbolic or not.
+%
+% ### Input Arguments
+%
+% `spectra`
+% : Input spectra structure.
+%
+
+if nargin < 1
+ error('sw_issymspec:WrongInput', 'Missing input spectra structure');
+end
+
+% Spectrum must have `ham` or `omega` field
+if ~isfield(spectra, 'ham')
+ if isfield(spectra, 'omega')
+ symobj = spectra.omega;
+ elseif size(spectra.hklA, 1) == 1 % Is a powder spectra
+ issym = false;
+ return
+ else
+ error('sw_issymspec:WrongInput', 'Invalid input spectra structure');
+ end
+else
+ symobj = spectra.ham;
+end
+issym = isa(symobj, 'sym');
+
+end
diff --git a/swfiles/sw_cartesian.m b/swfiles/sw_cartesian.m
index 166ca6927..c34a3b97d 100644
--- a/swfiles/sw_cartesian.m
+++ b/swfiles/sw_cartesian.m
@@ -1,29 +1,29 @@
function [vyOut, vzOut, vxOut] = sw_cartesian(n)
% creates a right handed Cartesian coordinate system
-%
+%
% ### Syntax
-%
+%
% `[vy, vz, vx] = sw_cartesian(n)`
%
% `V = sw_cartesian(n)`
-%
+%
% ### Description
-%
+%
% `[vy, vz, vx] = sw_cartesian(n)` creates an $(x,y,z)$ right handed
% Cartesian coordinate system with $v_x$, $v_y$ and $v_z$ defining the
-% basis vectors.
+% basis vectors.
%
% `V = sw_cartesian(n)` the generated basis vectors are stored in the `V`
% matrix: `V = [vx vy vz]` as column vectors.
-%
+%
% ### Input Arguments
-%
+%
% `n`
% : Either a 3 element row/column vector or a $[3\times 3]$ matrix with
% columns defining 3 vectors.
-%
+%
% ### Output Arguments
-%
+%
% `vy,vz,vx`
% : Vectors defining the right handed coordinate system. They are
% either column of row vectors depending on the shape of the
@@ -43,8 +43,15 @@
z = [0; 0;-1];
y = [0;-1; 0];
- if any(cross(n,z))
- vy = cross(n,z);
+ c = cross(n,z);
+ if isa(n,'sym')
+ opt = any(sw_always(c));
+ else
+ opt = any(c);
+ end
+
+ if opt
+ vy = c;
else
vy = cross(n,y);
end
@@ -54,12 +61,12 @@
if det(n) == 0
error('sw_cartesian:WrongInput','The input vectors are not linearly independent!')
end
-
+
nShape = [3 1];
vz = cross(n(:,1),n(:,2));
vy = cross(vz,n(:,1));
n = n(:,1);
-
+
else
error('sw_cartesian:WrongInput','Wrong size of n!')
end
@@ -73,7 +80,6 @@
vyOut = reshape(vy/norm(vy),nShape);
vzOut = reshape(vz/norm(vz),nShape);
vxOut = reshape(n/norm(n),nShape);
-
end
end
\ No newline at end of file
diff --git a/swfiles/sw_egrid.m b/swfiles/sw_egrid.m
index 797f57d65..cc2c18b62 100644
--- a/swfiles/sw_egrid.m
+++ b/swfiles/sw_egrid.m
@@ -113,8 +113,9 @@
% include all modes.
%
% `'epsilon'`
-% : Error limit, used to determine whether a given energy bin is
-% uniform or not. Default value is $10^{-5}$.
+% : DEPRECATED (previously the error limit, used to determine whether a
+% given energy bin is uniform or not. Default value is $10^{-5}$). This
+% parameter is no longer relevant and is ignored.
%
% `'autoEmin'`
% : Due to the finite numerical precision, the spin wave energies
@@ -129,6 +130,10 @@
% `'imagChk'`
% : Checks whether the imaginary part of the spin wave dispersion is
% smaller than the energy bin size. Default value is true.
+%
+% `dE`
+% : Energy resolution (FWHM) can be function, or a numeric matrix that
+% has length 1 or the number of energy bin centers.
%
% {{note The Blume-Maleev coordinate system is a cartesian coordinate
% system with $x_{BM}$, $y_{BM}$ and $z_{BM}$ basis vectors defined as:
@@ -158,6 +163,10 @@
% `Evect`
% : Input energy bin vector, defines the energy bin **edge** positions
% (converted from the given bin centers if necessary).
+%
+% `zeroEnergyTol`
+% : Eigenvalues with magnitude of the real component less than zeroEnergyTol
+% will be not be included in the structure factor binning
%
% `param`
% : All the input parameters.
@@ -177,6 +186,10 @@
return
end
+if sw_issymspec(spectra)
+ error('sw_egrid:SymbolicInput', 'This function does not handle symbolic spectra');
+end
+
if isfield(spectra,'obj')
T0 = spectra.obj.single_ion.T;
else
@@ -191,17 +204,28 @@
end
inpForm.fname = {'Evect' 'T' 'component' 'sumtwin' 'modeIdx' 'epsilon'};
-inpForm.defval = {E0 T0 'Sperp' true zeros(1,0) 1e-5 };
+inpForm.defval = {E0 T0 'Sperp' true zeros(1,0) NaN };
inpForm.size = {[1 -1] [1 1] [1 -2] [1 1] [1 -4] [1 1] };
inpForm.soft = {true false false false false false };
-inpForm.fname = [inpForm.fname {'autoEmin' 'imagChk' 'binType'}];
-inpForm.defval = [inpForm.defval {false true 'ebin' }];
-inpForm.size = [inpForm.size {[1 1] [1 1] [1 -5] }];
-inpForm.soft = [inpForm.soft {false false false }];
+inpForm.fname = [inpForm.fname {'autoEmin' 'imagChk' 'binType' 'zeroEnergyTol'}];
+inpForm.defval = [inpForm.defval {false true 'ebin' 5e-4}];
+inpForm.size = [inpForm.size {[1 1] [1 1] [1 -5] [1 1]}];
+inpForm.soft = [inpForm.soft {false false false false}];
+
+inpForm.fname = [inpForm.fname {'maxDSF', 'dE'}];
+inpForm.defval = [inpForm.defval {1e6, []}];
+inpForm.size = [inpForm.size {[1 1], [-6 -7]}];
+inpForm.soft = [inpForm.soft {false, true}];
param = sw_readparam(inpForm, varargin{:});
+if ~isnan(param.epsilon)
+ % non default value of epsilon
+ warning('sw_egrid:DeprecationWarning', ...
+ 'epsilon is deprecated - it is not relevant and will be ignored.');
+end
+
switch param.binType
case 'ebin'
eBin = true;
@@ -309,7 +333,7 @@
% pack all cross section into a cell for easier looping
-if iscell(spectra.Sab)
+if isfield(spectra,'Sab') && iscell(spectra.Sab)
nTwin = numel(spectra.Sab);
if ~isfield(spectra,'omega')
omega = {};
@@ -318,10 +342,39 @@
end
Sab = spectra.Sab;
+% intP = {intP};
+ if ~iscell(intP); intP = {intP}; end
+ if ~iscell(Pab); Pab = {Pab}; end
+ if ~iscell(Mab); Mab = {Mab}; end
+ if ~iscell(Sperp); Sperp = {Sperp}; end
+% Pab = {Pab};
+% Mab = {Mab};
+% Sperp = {Sperp};
+
+elseif isfield(spectra,'Sperp') && iscell(spectra.Sperp)
+ nTwin = numel(spectra.Sperp);
+ if ~isfield(spectra,'omega')
+ omega = {};
+ else
+ omega = spectra.omega;
+ end
+ Sab = repmat({[]},1,nTwin);
intP = {intP};
Pab = {Pab};
Mab = {Mab};
- Sperp = {Sperp};
+ Sperp = spectra.Sperp;
+elseif ~isfield(spectra,'Sab')
+ nTwin = 1;
+ if ~isfield(spectra,'omega')
+ omega = {[]};
+ else
+ omega = {spectra.omega};
+ end
+ Sab = {[]};
+ intP = {intP};
+ Pab = {Pab};
+ Mab = {Mab};
+ Sperp = {spectra.Sperp};
else
nTwin = 1;
if ~isfield(spectra,'omega')
@@ -337,7 +390,11 @@
end
% number of modes and Q points
-nMode = size(spectra.Sab,3);
+if iscell(spectra.omega)
+ nMode = size(spectra.omega{1}, 1);
+else
+ nMode = size(spectra.omega, 1);
+end
nHkl = numel(spectra.hkl)/3;
sHkl = [size(spectra.hkl) 1];
@@ -348,7 +405,7 @@
end
if ~isempty(intP{1})
- intP{ii} = reshape(intP{ii},3,3,nMode,[]);
+ intP{ii} = reshape(intP{ii},3,nMode,[]);
end
if ~isempty(Pab{1})
Pab{ii} = reshape(Pab{ii},3,3,nMode,[]);
@@ -393,86 +450,57 @@
end
% save the edge bins
-Evect = sort(param.Evect);
+param.Evect = sort(param.Evect);
if eBin
- eEvect = Evect;
+ ebin_edges = param.Evect;
+ ebin_cens = (ebin_edges(2:end)+ebin_edges(1:(end-1)))/2;
else
- dE = diff(Evect);
- eEvect = [Evect-[dE(1) dE]/2 Evect(end)+dE(end)/2];
+ ebin_cens = param.Evect;
+ den = diff(ebin_cens)./2;
+ ebin_edges = [ebin_cens(1)-den(1), ebin_cens(1:end-1) + den, ebin_cens(end) + den(end)];
end
+bin_widths = diff(ebin_edges);
+nE = numel(ebin_cens);
-if isfield(spectra,'omega')
- % Create vector for energy values, and put extra value below minimum and
- % above maximum for easy indexing swConv.
- epsilon = 1e-8;
-
- if eBin
- Evect = (Evect(2:end)+Evect(1:(end-1)))/2;
+% evalulate reoslution at every bin-center if func
+if isa(param.dE,'function_handle')
+ dE_sigma = param.dE(ebin_cens(:))/2*sqrt(2*log(2)); % convert from FWHM
+elseif isnumeric(param.dE)
+ n_dE = numel(param.dE);
+ if n_dE > 1 && n_dE ~= nE
+ error('sw_egrid:WrongInput', ['numeric dE must be on length 1 ' ...
+ 'or number of energy bin centers'])
end
- % energy bin parameters
- nE = numel(Evect);
- dE = diff(Evect);
-
+ dE_sigma = param.dE(:)/2*sqrt(2*log(2));
+else
+ error('sw_egrid:WrongInput', ['dE must be a numeric matrix or a ' ...
+ 'callable function']);
+end
+
+
+if isfield(spectra,'omega')
if param.imagChk
% find the maximum of the imaginary part of the spin wave energies
% checks only the first twin!
ioMax = max(abs(imag(omega{1}(:))));
-
- if ioMax > max(abs(dE(:)))
+ if ioMax > max(abs(bin_widths(:)))
error('egrid:BadSolution',['The imaginary part of the spin '...
'wave energes is larger than the bin size! Improve '...
'your calculation or disable imagChk option!']);
+ elseif param.autoEmin
+ if abs(ebin_edges(1)) < ioMax
+ ebin_edges(1) = ebin_edges(1)+ioMax;
+ ebin_cens(1) = ebin_cens(1)+ioMax/2;
+ end
end
- end
-
- if param.autoEmin && abs(Evect(1)-dE(1)/2)=0);
else
- Evect = [-epsilon; epsilon];
- end
-
- dE = dE(1);
-
- % Create indices in the matrix by searching for the closest value, size
- % nMode x nHkl. Put all the modes to the positive side for magnon creation.
- % The negative side will be the same, however with different Bose factor
- % for non-zero temperature.
- idxE = cell(1,nTwin);
-
-
- for tt = 1:nTwin
- % put the modes that are not in the modeIdx parameter above the
- % energy bin vector
- omega{tt}(~ismember(1:nMode,param.modeIdx),:) = Evect(end);
-
- % faster binning
- if isequalE
- idxE{tt} = floor((real(omega{tt})-E0)/dE)+2;
- idxE{tt}(idxE{tt}<2) = 1;
- idxE{tt}(idxE{tt}>nE+2) = nE+2;
- else
- % memory intensive binnin, bad for large numel(omega)
- [~, idxE{tt}] = min(abs(repmat(real(omega{tt}),[1 1 nE+2])-repmat(permute(Evect,[2 3 1]),[nMode nHkl 1])),[],3);
- end
-
- % Creates indices in the swConv matrix.
- idxE{tt} = idxE{tt} + repmat((0:nHkl-1).*(nE+2),[nMode 1]);
- idxE{tt} = idxE{tt}(:);
+ kB = 0.086173324;
+ nBose = 1./(exp(abs(ebin_cens)./(kB*param.T))-1)+double(ebin_cens>=0);
end
% Sums up the intensities in DSF into swConv.
@@ -480,25 +508,43 @@
for tt = 1:nTwin
for ii = 1:nConv
- swConv{ii,tt} = reshape(accumarray(idxE{tt},DSF{ii,tt}(:),[(nE+2)*nHkl 1]),[(nE+2) nHkl]);
- end
- end
-
- % Calculate Bose temperature factor for magnons
- if param.T==0
- nBose = double(Evect(2:(end-1))>=0);
- else
- nBose = 1./(exp(abs(Evect(2:(end-1)))./(spectra.obj.unit.kB*param.T))-1)+double(Evect(2:(end-1))>=0);
- end
-
- % Multiply the intensities with the Bose factor.
- for tt = 1:nTwin
- for ii = 1:nConv
- swConv{ii,tt} = bsxfun(@times,swConv{ii,tt}(2:(end-1),:),nBose);
+ swConv{ii, tt} = zeros([nE, nHkl]);
+ real_eigvals = real(omega{tt}(param.modeIdx, :));
+ % sum intensities and pad energies above max eigval with 0
+ DSF_valid = DSF{ii,tt}(param.modeIdx, :);
+ DSF_valid(DSF_valid > param.maxDSF) = 0;
+ if isempty(param.dE)
+ % find energy bin (cen) index coinciding with evals in omega
+ ien = discretize(real_eigvals, ebin_edges);
+ % set eigvals < zeroEnergyTol to naN (will be ignored)
+ izero_eigval = abs(real_eigvals(:)) < param.zeroEnergyTol;
+ ien(izero_eigval) = NaN;
+ % NaN in ien implies eigvals not in extent of Evect
+ ien_valid = ~isnan(ien(:));
+ % get hkl index of each ien bin (column index in real_eigvals)
+ [~, ihkl] = ind2sub(size(ien), 1:numel(ien));
+ % get index of bins in final sxConv field
+ % normally ien(ien_valid) is a colulmn vector but not in case
+ % of one modeIDx specified so need to reshape
+ sw_conv_idx = [reshape(ien(ien_valid), [], 1), ihkl(ien_valid)'];
+ if ~isempty(sw_conv_idx)
+ swConv{ii,tt} = accumarray(sw_conv_idx, DSF_valid(ien_valid), [nE, nHkl]);
+ end
+ else
+ swConv{ii, tt} = zeros([nE, nHkl]);
+ for ieval = 1:size(real_eigvals,1)
+ eval = real_eigvals(ieval, :);
+ % make use of array broadcasting, each q point
+ % corresponds to a col in ebin_cens(:) - eval
+ swConv{ii, tt} = swConv{ii, tt} + ...
+ DSF_valid(ieval,:).*(bin_widths(:)./(sqrt(2*pi)*dE_sigma)).*exp(-0.5 * ((ebin_cens(:) - eval) ./ dE_sigma).^2);
+ end
+ end
+ % Multiply the intensities with the Bose factor.
+ swConv{ii,tt} = bsxfun(@times,swConv{ii,tt},nBose');
swConv{ii,tt}(isnan(swConv{ii,tt})) = 0;
end
end
-
else
swConv = DSF;
nE = 1;
@@ -510,7 +556,7 @@
if isfield(spectra,'obj')
vol = spectra.obj.twin.vol/sum(spectra.obj.twin.vol);
else
- vol = 1;
+ vol = ones(1,nTwin);
end
swConvT = cell(nConv,1);
DSFT = cell(nConv,1);
@@ -528,7 +574,7 @@
spectra.T = param.T;
-spectra.Evect = eEvect;
+spectra.Evect = ebin_edges;
if isfield(spectra,'swRaw')
spectra = rmfield(spectra,'swRaw');
@@ -550,4 +596,4 @@
spectra.param.sumtwin = param.sumtwin;
-end
\ No newline at end of file
+end
diff --git a/swfiles/sw_extendlattice.m b/swfiles/sw_extendlattice.m
index 52aebabb2..5edd61421 100644
--- a/swfiles/sw_extendlattice.m
+++ b/swfiles/sw_extendlattice.m
@@ -93,13 +93,13 @@
SSext.(fName) = repmat(SS.(fName),[1 nCell]);
if ~isempty(SS.(fName))
% first atom index within the uspercell
- SSext.(fName)(4,:) = reshape(bsxfun(@plus,addIdx,SS.(fName)(4,:)),1,[]);
+ SSext.(fName)(4,:) = reshape(bsxfunsym(@plus,addIdx,SS.(fName)(4,:)),1,[]);
% end of bond vector still in original cell dimensions
- bVect = reshape(permute(bsxfun(@plus,cIdx,permute(SS.(fName)(1:3,:),[3:5 1 2])),[4 5 1:3]),3,[]);
+ bVect = reshape(permute(bsxfunsym(@plus,cIdx,permute(SS.(fName)(1:3,:),[3:5 1 2])),[4 5 1:3]),3,[]);
% normalize bond vector to supercell dimensions
- SSext.(fName)(1:3,:) = floor(bsxfun(@rdivide,bVect,nExt));
+ SSext.(fName)(1:3,:) = floor(bsxfunsym(@rdivide,bVect,nExt));
% indices are between (0:nCell-1)*nAtom
- SSext.(fName)(5,:) = sum(bsxfun(@times,bVect-bsxfun(@times,SSext.(fName)(1:3,:),nExt),[1;nExt(1);prod(nExt(1:2))]),1)*nAtom+SSext.(fName)(5,:);
+ SSext.(fName)(5,:) = sum(bsxfunsym(@times,bVect-bsxfunsym(@times,SSext.(fName)(1:3,:),nExt),[1;nExt(1);prod(nExt(1:2))]),1)*nAtom+SSext.(fName)(5,:);
end
end
diff --git a/swfiles/sw_fitpowder.m b/swfiles/sw_fitpowder.m
new file mode 100644
index 000000000..ddc3ab5e9
--- /dev/null
+++ b/swfiles/sw_fitpowder.m
@@ -0,0 +1,1091 @@
+classdef sw_fitpowder < handle & matlab.mixin.SetGet
+% Class to fit powder averaged spectra to inelastic neutron scattering data
+%
+% ### Syntax
+%
+% `fitpow = sw_fitpowder(swobj, data, fit_func, model_params, Name,Value)`
+%
+% ### Description
+%
+% Fits powder averaged spectra to constant-|Q| cuts or 2D |Q| vs en slices
+% of inelastic neutron scattering data accounting for the instrument
+% resolution
+%
+% ### Input Arguments
+%
+% `swobj`
+% : spinwave object with magnetic structure defined
+%
+% `data`
+% : Possible inputs depend on dimensionality of the data:
+% * 2D data
+% Either a struct containing fields `x`, `y` and `e` or a 2D HORACE
+% object (sqw or d2d)
+% where `y` is a matrix of intensities with shape (N|Q|, NEnergy)
+% `e` is a matrix of errorsbars with shape (N|Q|, NEnergy)
+% `x` is a cell array of size (1,2):
+% x{1} contains a vector of energy bin centers
+% x{2} contains a vector of |Q| bin centers.
+% * Vector of 1d datasets at constant |Q|
+% Either a struct containing fields `x`, `y` and `e` `qmin` and `qmax`
+% or a 1D HORACE object (sqw or d1d)
+% where `y` is a vector of intensities with shape (1, NEnergy)
+% `e` is a vector of errorsbars with shape (1, NEnergy)
+% `x` is a vector of energy bin centers
+% x{2} contains a vector of |Q| bin centers.
+% `qmin` is a scalar denoting the lower Q value of the cut
+% `qmax` is a scalar denoting the upper Q value of the cut
+%
+% `fit_func`
+% : Function handle changing the interactions in the spinwave model.
+% For example if the spinw model had matrices `J1`, `J2` and `D` then a
+% `fit_func` could be
+% ```
+% fit_func = @(obj, p) matparser(obj, 'param', p, 'mat',
+% {'J1', 'J2', 'D(3,3)'}, 'init', true);
+% ```
+%
+% `model_params`
+% : Vector of initial paramers to pass to fit_func
+%
+% `background_strategy` (optional)
+% : A string determining the type of background:
+% * `planar` (default) - 2D planar background in |Q| and energy transfer
+% * `independent` - 1D linear background as function of energy transfer
+%
+% `nQ` (optional)
+% : Scalar int correpsonding to number of Q-points to subdivide cuts into
+% for averaging
+%
+% ### Output Arguments
+%
+% `'result'`
+% : cell array containing output of sw_fitpowder.optimizer
+% For ndbase optimizers in the spinw package then the reuslt can be
+% unpacked as follows
+% ```
+% [fitted_params, cost_val, stat] = result{:}
+% ```
+% See docs for e.g. ndbase.simplex (default) for details
+%
+% ### Examples
+%
+% ```
+% >> % init spinw object
+% >> J1 = -0.05;
+% >> J2 = 0.3;
+% >> D = 0.05;
+% >> mnf2 = spinw;
+% >> mnf2.genlattice('lat_const', [4.87 4.87 3.31], 'angle', [90 90 90]*pi/180, 'sym', 'P 42/m n m');
+% >> mnf2.addatom('r', [0 0 0], 'S', 2.5, 'label', 'MMn2', 'color', 'b')
+% >> mnf2.gencoupling('maxDistance', 5)
+% >> mnf2.addmatrix('label', 'J1', 'value', J1, 'color', 'red');
+% >> mnf2.addmatrix('label', 'J2', 'value', J2, 'color', 'green');
+% >> mnf2.addcoupling('mat', 'J1', 'bond', 1)
+% >> mnf2.addcoupling('mat', 'J2', 'bond', 2)
+% >> mnf2.addmatrix('label', 'D', 'value', diag([0 0 D]), 'color', 'black');
+% >> mnf2.addaniso('D')
+% >> mnf2.genmagstr('mode', 'direct', 'S', [0 0; 0 0; 1 -1])
+% >>
+% >> % define fit_func
+% >> fit_func = @(obj, p) matparser(obj, 'param', p, 'mat', {'J1', 'J2', 'D(3,3)'}, 'init', true);
+% >>
+% >> % define resolution (from PyChop)
+% >> Ei = 20;
+% >> eres = @(en) 512.17*sqrt((Ei-en).^3 .* ( 8.26326e-10*(0.169+0.4*(Ei./(Ei-en)).^1.5).^2 + 2.81618e-11*(1.169+0.4*(Ei./(Ei-en)).^1.5).^2));
+% >> % Q-resolution (parameters for MARI)
+% >> e2k = @(en) sqrt( en .* (2*1.67492728e-27*1.60217653e-22) )./1.05457168e-34./1e10;
+% >> L1 = 11.8; % Moderator to Sample distance in m
+% >> L2 = 2.5; % Sample to detector distance in m
+% >> ws = 0.05; % Width of sample in m
+% >> wm = 0.12; % Width of moderator in m
+% >> wd = 0.025; % Width of detector in m
+% >> ki = e2k(Ei);
+% >> a1 = ws/L1; % Angular width of sample seen from moderator
+% >> a2 = wm/L1; % Angular width of moderator seen from sample
+% >> a3 = wd/L2; % Angular width of detector seen from sample
+% >> a4 = ws/L2; % Angular width of sample seen from detector
+% >> dQ = 2.35 * sqrt( (ki*a1)^2/12 + (ki*a2)^2/12 + (ki*a3)^2/12 + (ki*a4)^2/12 );
+% >>
+% >> % fit powder
+% >> backgroundStrategy = 'planar'; % 'planar' or 'independent'
+% >> nQ = 10; % Number of Q-points to subdivide cuts into for averaging
+% >>
+% >> fitpow = sw_fitpowder(mnf2, mnf2dat, fit_func, [J1 J2 D], backgroundStrategy, nQ);
+% >> fitpow.crop_energy_range(2.0, 8.0);
+% >> fitpow.powspec_args.dE = eres;
+% >> fitpow.sw_instrument_args = struct('dQ', dQ, 'ThetaMin', 3.5, 'Ei', Ei);
+% >> fitpow.estimate_constant_background();
+% >> fitpow.estimate_scale_factor();
+% >> fitpow.set_model_parameter_bounds(1:3, [-1 0 -0.2], [0 1 0.2]) % Force J1 to be ferromagnetic to agree with structure
+% >> fitpow.set_bg_parameter_bounds(3, 0.0, []) % Set lb of constant bg = 0
+% >> fitpow.fix_bg_parameters(1:2); % fix slopes of background to 0
+% >> fitpow.cost_function = "Rsq"; % or "chisq"
+% >> fitpow.optimizer = @ndbase.simplex;
+% >> [pfit,cost_val,stat] = fitpow.fit('MaxIter', 1); % passes varargin to optimizer
+% >>
+% >> fitpow.plot_result(pfit, 26, 'EdgeAlpha', 0.9, 'LineWidth', 2)
+% ```
+%
+% [spinw.spinwave] \| [sw_fitspec]
+%
+
+ properties (SetObservable)
+ % data
+ swobj;
+ y
+ e
+ ebin_cens
+ modQ_cens
+ nQ = 10 % number |Q| bins to calc. in integration limits of 1D cut
+ modQ_icuts
+ ndim
+ ncuts = 0;
+ % spinw funtion inputs
+ sw_instrument_args = struct()
+ powspec_args = struct('nRand', 1e3, 'T', 0, 'formfact', true, ...
+ 'hermit', false, 'imagChk', false, ...
+ 'fibo', true, 'binType', 'cbin', ...
+ 'component', 'Sperp', 'neutron_output', true, ...
+ 'fastmode', true);
+ % functions
+ fit_func
+ cost_function = "Rsq"; % "Rsq" or "chisq"
+ fbg
+ % fit and parameters
+ optimizer = @ndbase.simplex
+ nparams_model
+ nparams_bg
+ params
+ bounds
+ liveplot_interval = 0
+ end
+
+ properties (SetAccess = private)
+ background_strategy = "planar" % "planar" or "independent" (1D only - fbg = @(en, p)
+ fbg_planar = @(en, modQ, p, npoly_modQ) polyval(p(npoly_modQ+1:end), en(:)) + ...
+ polyval([reshape(p(1:npoly_modQ),1,[]), 0], modQ(:)'); % q^n,..q^1, en^n,...en^1, const
+ fbg_indep = @(en, p) polyval(p, en(:));
+ do_cache = true
+ ycalc_cached = []
+ model_params_cached = []
+ liveplot_counter = 0
+ ibg = []
+ bg_errors = []
+ npoly_modQ = 1;
+ npoly_en = 1;
+ bg_param_labels = [];
+ end
+
+ properties (Constant)
+ zero_abs_tol = 10*eps
+ end
+
+ methods
+ function obj = sw_fitpowder(swobj, data, fit_func, model_params, background_strategy, nQ)
+ % constructor
+ obj.swobj = swobj;
+ obj.fit_func = fit_func;
+ if nargin < 5
+ background_strategy = "planar";
+ elseif nargin==6
+ obj.nQ = nQ; % set this before add data
+ end
+ obj.add_data(data)
+ obj.set_background_strategy(background_strategy);
+ obj.initialise_parameters_and_bounds(model_params)
+ end
+
+ function initialise_parameters_and_bounds(obj, model_params)
+ obj.nparams_model = numel(model_params);
+ % last parameter is scale factor (default to 1)
+ obj.params = [model_params(:); 1];
+ obj.bounds = [-inf, inf].*ones(numel(obj.params), 1);
+ obj.bounds(end,1) = 0; % set lower bound of scale to be 0
+ obj.initialise_background_parameters_and_bounds();
+ end
+
+ function initialise_background_parameters_and_bounds(obj)
+ % zero intialise background parameters
+ obj.bg_param_labels = repmat("E", obj.npoly_en+1, 1) + [obj.npoly_en:-1:0]';
+ if obj.background_strategy == "independent"
+ nparams_bg_total = obj.ncuts * obj.nparams_bg;
+ else
+ nparams_bg_total = obj.nparams_bg;
+ % add modQ polynopmial label
+ obj.bg_param_labels = [repmat("Q", obj.npoly_modQ, 1) + [obj.npoly_modQ:-1:1]';
+ obj.bg_param_labels;];
+ end
+ bg_params = zeros(nparams_bg_total, 1);
+ bg_bounds = [-inf, inf].*ones(nparams_bg_total, 1);
+ % insert in middle of params and bound
+ obj.params = [obj.params(1:obj.nparams_model); bg_params; obj.params(end)];
+ obj.bounds = [obj.bounds(1:obj.nparams_model,:); bg_bounds; obj.bounds(end, :)];
+
+ end
+
+ function set_bg_npoly_modQ(obj, npoly_modQ)
+ if obj.background_strategy == "independent"
+ error('sw_fitpowder:invalidinput', ...
+ 'set_bg_npoly_modQ only applies to planar background');
+ end
+ if abs(mod(npoly_modQ, 1)) > eps || npoly_modQ < 0
+ error('sw_fitpowder:invalidinput', ...
+ 'npoly_modQ must be a positive integer');
+ end
+ if obj.ndim==1 && npoly_modQ > obj.ncuts - 1
+ error('sw_fitpowder:invalidinput', ...
+ 'Not enough 1D cuts to fit polynomial of order npoly_modQ.');
+ end
+
+ obj.npoly_modQ = npoly_modQ;
+ obj.nparams_bg = obj.get_nparams_in_background_func();
+ obj.initialise_background_parameters_and_bounds();
+ end
+
+ function set_bg_npoly_en(obj, npoly_en)
+ if abs(mod(npoly_en, 1)) > eps || npoly_en < 0
+ error('sw_fitpowder:invalidinput', ...
+ 'npoly_en must be a positive integer');
+ end
+ obj.npoly_en = npoly_en;
+ obj.nparams_bg = obj.get_nparams_in_background_func();
+ obj.initialise_background_parameters_and_bounds();
+ end
+
+ function set_background_strategy(obj, strategy)
+ if strategy == "planar"
+ obj.fbg = @(en, modQ, p) obj.fbg_planar(en, modQ, p, obj.npoly_modQ);
+ 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 bounds etc.
+ obj.background_strategy = strategy;
+ obj.nparams_bg = obj.get_nparams_in_background_func();
+ if ~isempty(obj.params)
+ obj.initialise_background_parameters_and_bounds();
+ if any(abs(bg_pars) > obj.zero_abs_tol) && obj.ndim == 1
+ % attempt good guess for background pars
+ modQs = obj.get_modQ_cens_of_cuts();
+ if obj.background_strategy == "independent"
+ % keep same energy bg (excl const bg)
+ obj.set_bg_parameters(1:obj.npoly_en, bg_pars(obj.npoly_modQ+1:end-1));
+ % set intercept (zero energy) for reach cut separately
+ intercepts = obj.fbg_planar(0, modQs, bg_pars, obj.npoly_modQ);
+ for icut = 1:obj.ncuts
+ obj.set_bg_parameters(obj.npoly_en+1, intercepts(icut), icut);
+ end
+ elseif obj.background_strategy == "planar"
+ % reshape each col is the params for a cut
+ bg_pars = reshape(bg_pars, [], obj.ncuts);
+ % fit constant/intercepts to poly in Q
+ pval = polyfit(modQs, bg_pars(end,:), obj.npoly_modQ);
+ obj.set_bg_parameters(1:obj.npoly_modQ, pval(1:end-1));
+ obj.set_bg_parameters(obj.nparams_bg, pval(end)); % const bg
+ % average energy dep. params (only very rough)
+ bg_pars_en = mean(bg_pars(1:obj.npoly_en,:), 2);
+ obj.set_bg_parameters(obj.npoly_modQ+1:obj.nparams_bg-1, bg_pars_en);
+ end
+ end
+ end
+ end
+
+ function fix_model_parameters(obj, iparams)
+ if any(iparams > obj.nparams_model)
+ error('sw_fitpowder', 'Parameter indices supplied must be within number of model parameters');
+ end
+ obj.fix_parameters(iparams)
+ end
+
+ function fix_bg_parameters(obj, iparams_bg, icuts)
+ if nargin < 3
+ icuts = 0;
+ end
+ obj.fix_parameters(obj.get_index_of_background_parameters(iparams_bg, icuts));
+ end
+
+ function fix_scale(obj)
+ obj.fix_parameters(numel(obj.params))
+ end
+
+ function set_model_parameters(obj, iparams, values)
+ if any(iparams > obj.nparams_model)
+ error('sw_fitpowder', 'Parameter indices supplied must be within number of model parameters');
+ end
+ obj.params(iparams) = values;
+ end
+
+ function set_bg_parameters(obj, iparams_bg, values, icuts)
+ if nargin < 4
+ icuts = 0;
+ end
+ if ischar(iparams_bg) || iscell(iparams_bg) % Handles character arrays
+ iparams_bg = string(iparams_bg);
+ end
+ for ival = 1:numel(values)
+ iparams = obj.get_index_of_background_parameters(iparams_bg(ival), icuts);
+ obj.params(iparams) = values(ival);
+ end
+ end
+
+ function set_scale(obj, scale)
+ obj.params(end) = scale;
+ end
+
+ function set_model_parameter_bounds(obj, iparams, lb, ub)
+ if any(iparams > obj.nparams_model)
+ error('sw_fitpowder', 'Parameter indices supplied must be within number of model parameters');
+ end
+ for ibnd = 1:numel(iparams)
+ obj.set_bounds(iparams(ibnd), lb, ub, ibnd);
+ end
+ end
+
+ function set_bg_parameter_bounds(obj, iparams_bg, lb, ub, icuts)
+ if nargin < 5
+ icuts = 0;
+ end
+ for ibnd = 1:numel(iparams_bg)
+ iparams = obj.get_index_of_background_parameters(iparams_bg(ibnd), icuts);
+ obj.set_bounds(iparams, lb, ub, ibnd);
+ end
+ end
+
+ function set_scale_bounds(obj, lb, ub)
+ obj.set_bounds(size(obj.bounds,1), lb, ub);
+ end
+
+ function data = export_data(obj, filename)
+ if obj.ndim == 2
+ data.y = obj.y';
+ data.e = obj.e';
+ data.x = {obj.ebin_cens, obj.modQ_cens};
+ else
+ % 1D - array of structs returned
+ data = repelem(struct('x', obj.ebin_cens), obj.ncuts);
+ for icut = 1:obj.ncuts
+ data(icut).y = obj.y(:, icut)';
+ data(icut).e = obj.e(:, icut)';
+ % find q limits (bin edges)
+ qs = obj.modQ_cens(obj.modQ_icuts == icut);
+ data(icut).qmin = qs(1) - 0.5*(qs(2)-qs(1));
+ data(icut).qmax = qs(end) + 0.5*(qs(end)-qs(end-1));
+ end
+ end
+ if nargin > 1
+ save(filename, 'data');
+ end
+ end
+
+ function add_data(obj, data)
+ if ~isa(data, "struct")
+ data = arrayfun(@obj.convert_horace_to_struct, data);
+ end
+ obj.clear_cache();
+ if numel(data) == 1 && isa(data.x, "cell")
+ % 2D
+ assert(all(isfield(data, {'x', 'y', 'e'})), ...
+ 'sw_fitpowder:invalidinput', ...
+ 'Input cell does not have correct fields');
+ obj.ndim = 2;
+ obj.y = data.y'; % nE x n|Q|
+ obj.e = data.e';
+ obj.ebin_cens = data.x{1};
+ obj.modQ_cens = data.x{2};
+ else
+ % 1D
+ if obj.ndim == 2
+ % clear previously added 2D data
+ obj.modQ_cens = [];
+ obj.y = [];
+ obj.e = [];
+ warning('spinw:sw_fitpowder:add_data', ...
+ 'Clearing 2D data previously added.');
+ end
+ obj.ndim = 1;
+ obj.ebin_cens = data(1).x;
+ obj.ncuts = numel(data);
+ for icut = 1:obj.ncuts
+ cut = data(icut);
+ has_xye = all(isfield(cut, {'x', 'y', 'e'}));
+ has_qfields = all(isfield(cut, {'qmin', 'qmax'})) || isfield(cut, 'qs');
+ assert(has_xye && has_qfields, ...
+ 'sw_fitpowder:invalidinput', ...
+ 'Input cell does not have correct fields');
+ obj.y = [obj.y cut.y(:)];
+ obj.e = [obj.e cut.e(:)];
+ if isfield(cut, 'qs')
+ obj.modQ_cens = [obj.modQ_cens, cut.qs(:)'];
+ obj.modQ_icuts = [obj.modQ_icuts, icut*ones(1, numel(cut.qs))];
+ else
+ dQ = (cut.qmax - cut.qmin)/obj.nQ;
+ obj.modQ_cens = [obj.modQ_cens, ((cut.qmin+dQ/2):dQ:cut.qmax)];
+ obj.modQ_icuts = [obj.modQ_icuts, icut*ones(1, obj.nQ)];
+ end
+ end
+ end
+ obj.powspec_args.Evect = obj.ebin_cens(:)'; % row vect
+ end
+
+ function replace_2D_data_with_1D_cuts(obj, qmins, qmaxs, background_strategy)
+ assert(numel(qmins)==numel(qmaxs), 'sw_fitpowder:invalidinput', ...
+ 'Must pass same number of mins and maxs to make cuts.');
+ cuts = [];
+ for icut = 1:numel(qmins)
+ cuts = [cuts obj.cut_2d_data(qmins(icut), qmaxs(icut))];
+ end
+ obj.add_data(cuts);
+ if nargin > 3 && background_strategy ~= obj.background_strategy
+ obj.set_background_strategy(background_strategy)
+ end
+ end
+
+ function crop_energy_range(obj, emin, emax)
+ % crop data
+ ikeep = obj.ebin_cens >= emin & obj.ebin_cens <= emax;
+ obj.apply_energy_mask(ikeep);
+ end
+
+ function exclude_energy_range(obj, elo, ehi)
+ ikeep = obj.ebin_cens < elo | obj.ebin_cens > ehi;
+ obj.apply_energy_mask(ikeep);
+ end
+
+ function crop_q_range(obj, qmin, qmax)
+ assert(obj.ndim==2, 'sw_fitpowder:invalidinput', ...
+ '|Q| range can only be cropped on 2D datasets');
+ % crop data
+ ikeep = obj.modQ_cens >= qmin & obj.modQ_cens <= qmax;
+ obj.modQ_cens = obj.modQ_cens(ikeep);
+ obj.y = obj.y(:, ikeep);
+ obj.e = obj.e(:, ikeep);
+ obj.clear_cache();
+ end
+
+ function bg = calc_background(obj, bg_params)
+ % add background
+ if obj.background_strategy == "planar"
+ bg = obj.fbg(obj.ebin_cens, obj.modQ_cens, bg_params);
+ elseif obj.ndim == 1
+ % add energy dependent background to each cut
+ bg = zeros(size(obj.y));
+ istart = 1;
+ for icut = 1:obj.ncuts
+ iend = istart + obj.nparams_bg - 1;
+ bg(:,icut) = obj.fbg(obj.ebin_cens(:), bg_params(istart:iend));
+ istart = iend + 1;
+ end
+ end
+ end
+
+ function set_caching(obj, do_cache)
+ obj.do_cache = do_cache;
+ if ~obj.do_cache
+ obj.clear_cache()
+ end
+ end
+
+ 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();
+ 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)
+ model_params = reshape(params(1:obj.nparams_model), 1, []);
+ if obj.do_cache && ~isempty(obj.model_params_cached) && all(abs(model_params - obj.model_params_cached) < 1e-10)
+ ycalc = obj.ycalc_cached; % do not repeat calc
+ else
+ % set spinw model interactions
+ % pass params as row-vector as expected by matparser
+ obj.fit_func(obj.swobj, model_params);
+ % simulate powder spectrum
+ spec = obj.swobj.powspec(obj.modQ_cens, obj.powspec_args);
+ spec = sw_instrument(spec, obj.sw_instrument_args);
+ ycalc = spec.swConv;
+ % cache if required
+ if obj.do_cache
+ obj.ycalc_cached = spec.swConv;
+ obj.model_params_cached = model_params;
+ end
+ end
+ % scale
+ ycalc = params(end)*ycalc;
+ % calc background
+ bg = calc_background(obj, params(obj.nparams_model + 1:end-1));
+ if obj.background_strategy == "planar"
+ ycalc = ycalc + bg; % add planar bg before any rebinning
+ if obj.ndim == 1
+ % integrate nQ |Q| points for each cut
+ ycalc = obj.rebin_powspec_to_1D_cuts(ycalc);
+ end
+ else
+ % integrate nQ |Q| points for each cut
+ ycalc = obj.rebin_powspec_to_1D_cuts(ycalc);
+ % add background to individual cuts after rebin
+ ycalc = ycalc + bg;
+ end
+
+ end
+
+ function resid = calc_residuals(obj, params)
+ % evaluate fit function
+ [ycalc, ~] = obj.calc_spinwave_spec(params);
+ if obj.liveplot_interval > 0
+ obj.liveplot_counter = obj.liveplot_counter + 1;
+ if obj.liveplot_counter == obj.liveplot_interval
+ obj.plot_1d_or_2d(ycalc);
+ drawnow;
+ obj.liveplot_counter = 0;
+ end
+ end
+ resid = obj.eval_residuals(obj.y, obj.e, ycalc);
+ end
+
+ function resid_sq_sum = calc_cost_func(obj, params)
+ resid = obj.calc_residuals(params);
+ resid_sq_sum = resid'*resid;
+ end
+
+ function resid = calc_residuals_of_background(obj, bg_params)
+ bg = obj.calc_background(bg_params);
+ if obj.ndim == 1 && obj.background_strategy=="planar"
+ % integrate nQ |Q| points for each cut
+ bg = obj.rebin_powspec_to_1D_cuts(bg);
+ end
+ resid = obj.eval_residuals(obj.y(obj.ibg), obj.e(obj.ibg), bg(obj.ibg));
+ end
+
+ function resid_sq_sum = calc_cost_func_of_background(obj, bg_params)
+ resid = obj.calc_residuals_of_background(bg_params);
+ resid_sq_sum = resid'*resid;
+ end
+
+ function varargout = fit(obj, varargin)
+ if obj.liveplot_interval > 0
+ figure("color","white");
+ obj.liveplot_counter = 0;
+ end
+ % setup cell for output of ndbase optimizer/minimizer
+ varargout = cell(1,nargout(obj.optimizer));
+ % pass params and bounds as rows for ndbase optimisers
+ if any(cellfun(@(elem) elem =="resid_handle", varargin(1:2:end)))
+ fobj = @obj.calc_residuals; % minimise using residual array
+ else
+ fobj = @obj.calc_cost_func; % minimise scalar
+ end
+ [varargout{:}] = obj.optimizer([], fobj, obj.params(:)', ...
+ 'lb', obj.bounds(:,1)', ...
+ 'ub', obj.bounds(:,2)', ...
+ 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
+ % which are free to vary in the fit. The index of the
+ % parameters is optionally output. The second optional output
+ % is the full covariance matrix.
+ %
+ % ### Usage
+ %
+ % errors = sw_fitpowder_obj.calc_uncertainty(params)
+ % [errors, cov] = sw_fitpowder_obj.calc_uncertainty(params)
+ %
+ % ### See Also
+ %
+ % [ndbase.estimate_hessian]
+ %
+ if ~any(cellfun(@(elem) elem =="ivary", varargin(1:2:end)))
+ % set parameters varied in fit from bounds
+ ivary = find(obj.bounds(:,2) - obj.bounds(:,1) > obj.zero_abs_tol);
+ varargin(end+1:end+2) = {"ivary", ivary};
+ end
+ [hess, stats] = ndbase.estimate_hessian(@obj.calc_cost_func, params, varargin{:});
+ % determine num DoF
+ nbins = sum(isfinite(obj.y) & obj.e > obj.zero_abs_tol, 'all');
+ ndof = nbins - size(hess,1);
+ % calc errors
+ cov = inv(hess) * 2.0 * stats.cost_val/ ndof;
+ param_errors = zeros(size(params));
+ param_errors(ivary) = sqrt(diag(cov));
+ varargout = {cov};
+ end
+
+ function varargout = fit_background(obj, varargin)
+ if isempty(obj.ibg)
+ obj.find_indices_and_mean_of_bg_bins();
+ end
+ % check if enough bins for parameters
+ ibg_par = (obj.nparams_model+1):(numel(obj.params)-1);
+ lb = obj.bounds(ibg_par,1)';
+ ub = obj.bounds(ibg_par,2)';
+ ifixed = sum(abs(lb - ub) < obj.zero_abs_tol);
+ if numel(obj.ibg) < numel(ibg_par) - ifixed
+ error('spinw:sw_fitpowder:fit_background', ...
+ 'Not enough points to fit the function.');
+ end
+ if any(cellfun(@(elem) elem =="resid_handle", varargin(1:2:end)))
+ fobj = @obj.calc_residuals_of_background; % minimise using residual array
+ else
+ fobj = @obj.calc_cost_func_of_background; % minimise scalar
+ 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);
+ 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
+
+ function estimate_constant_background(obj)
+ if isempty(obj.ibg)
+ bg = obj.find_indices_and_mean_of_bg_bins();
+ else
+ bg = mean(obj.y(obj.ibg));
+ end
+ % set constant background assuming last bg parameter
+ obj.set_bg_parameters("E0", bg); % set constant background
+ end
+
+ function set_errors_of_bg_bins(obj, val)
+ if isempty(obj.ibg)
+ obj.find_indices_and_mean_of_bg_bins();
+ end
+ if nargin < 2
+ val = max(obj.e(obj.ibg));
+ end
+ obj.bg_errors = obj.e(obj.ibg); % save values so can reset
+ obj.e(obj.ibg) = val;
+ end
+
+ function reset_errors_of_bg_bins(obj)
+ if ~isempty(obj.ibg) && ~isempty(obj.bg_errors)
+ obj.e(obj.ibg) = obj.bg_errors;
+ end
+ obj.bg_errors = [];
+ end
+
+ function plot_result(obj, params, varargin)
+ [ycalc, ~] = obj.calc_spinwave_spec(params);
+ obj.plot_1d_or_2d(ycalc, varargin{:});
+ end
+
+ function plot_1d_cuts_on_data(obj, ycalc, varargin)
+ modQs = obj.get_modQ_cens_of_cuts();
+ for icut = 1:obj.ncuts
+ ax = subplot(1, obj.ncuts, icut);
+ hold on; box on;
+ plot(ax, obj.ebin_cens, obj.y(:,icut), 'ok');
+ plot(ax, obj.ebin_cens, ycalc(:,icut), '-r', varargin{:});
+ xlim(ax, [obj.ebin_cens(1), obj.ebin_cens(end)]);
+ ymin = min(min(obj.y(:,icut)), min(ycalc(:,icut)));
+ ymax = max(max(obj.y(:,icut)), max(ycalc(:,icut)));
+ ylim(ax, [ymin, ymax]);
+ xlabel(ax, 'Energy (meV)')
+ ylabel(ax, 'Intensity');
+ title(ax, num2str(modQs(icut), "%.2f") + " $\AA^{-1}$", 'interpreter','latex')
+ end
+ end
+
+ function plot_2d_contour_on_data(obj, ycalc, varargin)
+ ax = obj.plot_2d_data(obj.y);
+ contour(ax, obj.modQ_cens, obj.ebin_cens, ycalc, varargin{:});
+ legend('Calculated');
+ end
+
+ function ax = plot_2d_data(obj, y)
+ ax = gca;
+ box on; hold on;
+ h = imagesc(ax, obj.modQ_cens, obj.ebin_cens, y);
+ h.AlphaData = double(obj.e > 0); % make empty bins transparent
+ cbar = colorbar(ax);
+ cbar.Label.String = "Intensity";
+ xlabel(ax, "$\left|Q\right| (\AA^{-1})$", 'interpreter','latex');
+ ylabel(ax, "Energy (meV)");
+ ylim(ax, [obj.ebin_cens(1), obj.ebin_cens(end)]);
+ 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
+ assert(obj.ndim ==2, ...
+ 'sw_fitpowder:invalidinput', ...
+ 'This function is only valid for 2D data');
+ if nargin > 3
+ [ycalc, ~] = obj.calc_spinwave_spec(params);
+ else
+ ycalc = [];
+ end
+ figure("color","white");
+ ncuts = numel(qmins);
+ for icut = 1:ncuts
+ ax = subplot(1, ncuts, icut);
+ hold on; box on;
+ if ~isempty(ycalc)
+ cut = obj.cut_2d_data(qmins(icut), qmaxs(icut), ycalc);
+ plot(ax, cut.x, cut.ycalc, '-r', 'DisplayName', 'Fit');
+ else
+ cut = obj.cut_2d_data(qmins(icut), qmaxs(icut));
+ end
+ plot(ax, cut.x, cut.y, 'ok', 'DisplayName', 'Data');
+ % calc xlims
+ ifinite = isfinite(cut.y);
+ istart = find(ifinite, 1, 'first');
+ iend = find(ifinite, 1, 'last');
+ xlim(ax, [cut.x(istart), cut.x(iend)]);
+ xlabel(ax, 'Energy (meV)')
+ ylabel(ax, 'Intensity');
+ title(ax, num2str(0.5*(cut.qmin +cut.qmax), "%.2f") + " $\AA^{-1}$", 'interpreter','latex')
+ end
+ end
+
+ function disp_params(obj,pfit, perr)
+ col_headers = ["Index", "Initial"];
+ pars = obj.params(:);
+ par_fmt = "%8g\t";
+ fmt_str = "%8.0f\t" + par_fmt;
+ if nargin > 1
+ assert(numel(pfit) == numel(obj.params), ...
+ 'sw_fitpowder:invalidinput', ...
+ 'Fit params vector has invlaid length.');
+ col_headers = [col_headers, "Best Fit"];
+ pars = [pars, pfit(:)];
+ fmt_str = fmt_str + par_fmt;
+ end
+ if nargin == 3
+ assert(numel(perr) == numel(obj.params), ...
+ 'sw_fitpowder:invalidinput', ...
+ 'Fit error vector has invlaid length.');
+ col_headers = [col_headers, "Error"];
+ pars = [pars, perr(:)];
+ fmt_str = fmt_str + par_fmt;
+ end
+ fmt_str = fmt_str + "\n";
+ % print model params
+ fprintf("---\nMODEL\n---\n")
+ fprintf([repmat('%s\t\t', 1,numel(col_headers)), '\n'], col_headers);
+ imodel = [1:obj.nparams_model]';
+ fprintf(fmt_str, [imodel, pars(imodel,:)]');
+ % print scale
+ scales = sprintf(par_fmt, pars(end,:));
+ fprintf("%8s\t%s\n", "SCALE", scales);
+ % print background
+ fprintf("---\nBACKGROUND\n---\n")
+ bg_pars = pars(obj.nparams_model+1:size(pars, 1)-1, :);
+ labels = obj.bg_param_labels;
+ col_headers = col_headers(2:end); % excl. Index
+ bg_par_index = [1:obj.nparams_bg]';
+ if obj.ndim == 1 && obj.background_strategy == "independent"
+ % add icut column
+ col_headers = ["Cut Index", col_headers];
+ fmt_str = "%.0f\t" + fmt_str;
+ cut_index = repelem([1:obj.ncuts]', obj.nparams_bg,1);
+ bg_pars = [cut_index , bg_pars];
+ bg_par_index = repmat(bg_par_index, obj.ncuts, 1);
+ labels = repmat(labels, obj.ncuts, 1);
+ end
+ bg_pars = [bg_par_index, bg_pars];
+ col_headers = ["Label", "Index", col_headers];
+ fprintf([repmat('%s\t', 1,numel(col_headers)), '\n'], col_headers);
+ for irow = 1:size(bg_pars, 1)
+ row = sprintf(fmt_str, bg_pars(irow,:));
+ fprintf("%5s\t%s", labels(irow), row);
+ end
+ end
+
+ end
+
+ % private
+ methods (Hidden=true, Access = private)
+ function resid = eval_residuals(obj, y, e, ycalc)
+ resid = (y - ycalc);
+ if obj.cost_function == "chisq"
+ resid = resid./e;
+ end
+ % exclude nans in both ycalc and input data
+ resid = resid(isfinite(resid) & e > obj.zero_abs_tol);
+ end
+
+
+ function modQ_cens = get_modQ_cens_of_cuts(obj)
+ modQ_cens = zeros(1, obj.ncuts);
+ for icut = 1:obj.ncuts
+ modQ_cens(icut) = mean(obj.modQ_cens(obj.modQ_icuts == icut));
+ end
+ end
+
+ function cut = cut_2d_data(obj, qmin, qmax, ycalc)
+ assert(obj.ndim ==2, ...
+ 'sw_fitpowder:invalidinput', ...
+ 'This function is only valid for 2D data');
+ ikeep = obj.modQ_cens > qmin & obj.modQ_cens <= qmax;
+ cut = struct('x', obj.ebin_cens, 'qmin', qmin, 'qmax', qmax, ...
+ 'qs', obj.modQ_cens(ikeep));
+ ifinite = isfinite(obj.y(:, ikeep));
+ cut.y = mean(obj.y(:, ikeep), 2, 'omitnan');
+ cut.e = sqrt(sum(obj.e(:, ikeep).^2, 2))./sum(ifinite, 2);
+ if nargin == 4 && ~isempty(ycalc)
+ % also cut ycalc
+ cut.ycalc = mean(ycalc(:, ikeep), 2, 'omitnan');
+ end
+ end
+ function ycalc_1d = rebin_powspec_to_1D_cuts(obj, ycalc)
+ % check if regular nQ points for each cut
+ counts = groupcounts(obj.modQ_icuts(:));
+ unique_counts = unique(counts);
+ if numel(unique_counts) == 1 && unique_counts == obj.nQ
+ % avg successive nQ points along |Q| axis (dim=2)
+ ycalc_1d = reshape(ycalc, size(ycalc,1), obj.nQ, []);
+ ycalc_1d = squeeze(mean(ycalc_1d, 2, 'omitnan'));
+ else
+ % different num Q points per cut, loop over cuts
+ ycalc_1d = zeros(size(ycalc, 1), obj.ncuts);
+ for icut = 1:obj.ncuts
+ ikeep = obj.modQ_icuts == icut;
+ ycalc_1d(:, icut) = mean(ycalc(:, ikeep), 2, 'omitnan');
+ end
+ end
+ end
+
+ function nbg_pars = get_nparams_in_background_func(obj)
+ % get background parameters
+ if obj.background_strategy == "planar"
+ nbg_pars = obj.npoly_en + obj.npoly_modQ + 1; % dependent variables energy, modQ
+ else
+ nbg_pars = obj.npoly_en + 1; % dependent variable energy
+ end
+ end
+
+ function iparams_bg = get_index_of_background_parameter_from_string(obj, bg_param_labels)
+ nlabels = numel(bg_param_labels);
+ iparams_bg = zeros(1, nlabels);
+ for ilabel = 1:nlabels
+ iparam_bg = find(obj.bg_param_labels == bg_param_labels(ilabel));
+ assert(~isempty(iparam_bg), ...
+ 'sw_fitpowder:invalidinput', ...
+ ['Background parameter ' char(bg_param_labels(ilabel)) ' not found.']);
+ iparams_bg(ilabel) = iparam_bg;
+ end
+ end
+
+ function iparams = get_index_of_background_parameters(obj, iparams_bg, icuts)
+ % get index if parameter string passed
+ if isstring(iparams_bg)
+ iparams_bg = obj.get_index_of_background_parameter_from_string(iparams_bg);
+ end
+ if any(iparams_bg > obj.nparams_bg)
+ error('sw_fitpowder:invalidinput', 'Parameter indices supplied must be within number of background parameters');
+ end
+ if any(icuts == 0)
+ if obj.background_strategy == "independent"
+ icuts = 1:obj.ncuts; % apply to all cuts
+ else
+ icuts = 1; % will work for planar bg
+ end
+ end
+ % find index in vector of all parameters
+ iparams = [];
+ for icut = icuts
+ iparams = [iparams, iparams_bg + obj.nparams_model + (icut-1)*obj.nparams_bg];
+ end
+ end
+
+ function plot_1d_or_2d(obj, ycalc, varargin)
+ if obj.liveplot_interval == 0
+ figure("color","white");
+ else
+ clf;
+ end
+ if obj.ndim == 1
+ obj.plot_1d_cuts_on_data(ycalc, varargin{:})
+ else
+ obj.plot_2d_contour_on_data(ycalc, varargin{:})
+ end
+ end
+ function apply_energy_mask(obj, ikeep)
+ if obj.ndim == 1
+ obj.ebin_cens = obj.ebin_cens(ikeep);
+ obj.powspec_args.Evect = obj.ebin_cens(:)';
+ obj.y = obj.y(ikeep, :);
+ obj.e = obj.e(ikeep, :);
+ else
+ obj.y(~ikeep, :) = NaN;
+ obj.e(~ikeep, :) = NaN;
+ end
+ obj.clear_cache();
+ end
+ function set_bounds(obj, iparams, lb, ub, ibnd)
+ if ~isempty(lb)
+ if nargin == 5
+ lb = lb(ibnd);
+ end
+ obj.bounds(iparams, 1) = lb;
+ end
+ if ~isempty(ub)
+ if nargin == 5
+ ub = ub(ibnd);
+ end
+ obj.bounds(iparams, 2) = ub;
+ end
+ end
+ function fix_parameters(obj, iparams)
+ for iparam = iparams
+ obj.bounds(iparam, :) = obj.params(iparam);
+ end
+ end
+ function bg = find_indices_and_mean_of_bg_bins(obj)
+ % start with a seed of indices of likely non-signal bins
+ iseed = find(isfinite(obj.y) & obj.y < mean(obj.y(:), 'omitnan'));
+ [ysort, isort] = sort(obj.y(iseed), 'descend');
+ bg = NaN;
+ prev_skew = inf;
+ for ipt = 1:numel(isort)
+ this_mean = mean(ysort(ipt:end));
+ this_skew = mean((ysort(ipt:end) - this_mean).^3);
+ if this_skew < 0 || this_skew > prev_skew
+ bg = this_mean;
+ break
+ else
+ prev_skew = this_skew;
+ end
+ end
+ % store indices of background bins
+ if ~isfinite(bg)
+ error('spinw:find_indices_and_mean_of_bg_bins', ...
+ 'Could not estimate background.');
+ else
+ 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)
+ if isa(data, "sqw")
+ ndim = data.dimensions;
+ assert(ndim < 3, 'spinw:fitpow:invalidinput', 'Input SQW object must be 1D or 2D');
+ data_obj = data.data;
+ elseif isa(data, "d1d")
+ ndim = 1;
+ data_obj = data;
+ elseif isa(data, "d2d")
+ ndim = 2;
+ data_obj = data;
+ else
+ error('spinw:fitpow:invalidinput', 'Input must be a Horace object (sqw, d1d or d2d)')
+ end
+ assert(strcmp(data_obj.ulabel{1}, '|Q|'), 'spinw:fitpow:invalidinput', 'Input Horace object is not a powder cut');
+ % convert edges to bin centers (need energy first if 2D)
+ cens = cellfun(@(edges) (edges(1:end-1) + edges(2:end))/2, data_obj.p(ndim:-1:1), 'UniformOutput', false);
+ if ndim == 2
+ cens = {cens}; % otherwise MATLAB makes multiple structs
+ end
+ data_struct = struct('x', cens, 'y', data_obj.s, 'e', data_obj.e);
+ if ndim == 1
+ % add q integration range
+ data_struct.qmin = data_obj.iint(1,1);
+ data_struct.qmax = data_obj.iint(2,1);
+ end
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/swfiles/sw_freemem.m b/swfiles/sw_freemem.m
index 8e1be2269..d2327a7a5 100644
--- a/swfiles/sw_freemem.m
+++ b/swfiles/sw_freemem.m
@@ -16,7 +16,21 @@
% `mem`
% : Size of free memory in bytes.
%
+persistent t0 m0;
+if isempty(t0) || isempty(m0)
+ m0 = get_free();
+ t0 = tic;
+else
+ if isempty(t0) || toc(t0) > 10 % poll only every 10 seconds
+ m0 = get_free();
+ t0 = tic;
+ end
+end
+mem = m0;
+
+end
+function mem = get_free()
mem = 0;
try %#ok
diff --git a/swfiles/sw_hassymtoolbox.m b/swfiles/sw_hassymtoolbox.m
new file mode 100644
index 000000000..0470efe53
--- /dev/null
+++ b/swfiles/sw_hassymtoolbox.m
@@ -0,0 +1,24 @@
+function has_toolbox = sw_hassymtoolbox()
+% Checks if the running Matlab instance has the symbolic toolbox
+%
+% ### Syntax
+%
+% `has_toolbox = sw_hassymtoolbox()`
+%
+% ### Description
+%
+% This function checks if the symbolic toolbox is available
+%
+
+has_toolbox = license('test', 'symbolic_toolbox') == 1;
+if has_toolbox
+ try
+ x = sym('x');
+ catch ME
+ if strcmp(ME.identifier, 'MATLAB:UndefinedFunction')
+ has_toolbox = false
+ else
+ rethrow(ME)
+ end
+ end
+end
diff --git a/swfiles/sw_import.m b/swfiles/sw_import.m
index 72feb4b82..736e11b0e 100644
--- a/swfiles/sw_import.m
+++ b/swfiles/sw_import.m
@@ -98,7 +98,7 @@
end
% create the SpinW model
- obj0.genlattice('lat_const',dat.cell(1:3),'angled',dat.cell(4:6),'spgr',dat.spgr);
+ obj0.genlattice('lat_const',dat.cell(1:3),'angled',dat.cell(4:6),'sym',dat.spgr);
if ~isempty(dat.atom)
obj0.addatom('r',[dat.atom(:).r],'label',{dat.atom(:).label});
elseif ~isempty(dat.matom)
@@ -156,7 +156,6 @@
end
xyz0 = sprintf('%s; ',xyz0{:}); xyz0 = xyz0(1:end-2);
- %name0 = cif0.atom_site_type_symbol';
cell0 = [cif0.atom_site_label cif0.atom_site_type_symbol];
name0 = cellfun(@(x,y)strjoin({x y}),cell0(:,1),cell0(:,2),'UniformOutput',false)';
r0 = mod([cif0.atom_site_fract_x cif0.atom_site_fract_y cif0.atom_site_fract_z]',1);
diff --git a/swfiles/sw_instrument.m b/swfiles/sw_instrument.m
index eac9b659c..a8aa872e0 100644
--- a/swfiles/sw_instrument.m
+++ b/swfiles/sw_instrument.m
@@ -57,6 +57,10 @@
% : Minimum scattering angle in \\deg, default value is 0. Can be only
% applied if one of the `ki`, `Ei`, `kf` or `Ef` parameters is defined.
%
+% `'thetaMax'`
+% : Maximum scattering angle in \\deg, default value is 135. Can be only
+% applied if one of the `ki`, `Ei`, `kf` or `Ef` parameters is defined.
+%
% `'plot'`
% : If the resolution is read from file and plot option is
% true, the energy dependent resolution values together with the
@@ -123,12 +127,16 @@
return
end
+if sw_issymspec(spectra)
+ error('sw_instrument:SymbolicInput', 'This function does not handle symbolic spectra');
+end
+
func0 = @swfunc.gaussfwhm;
-inpForm.fname = {'dE' 'ki' 'Ei' 'kf' 'Ef' 'plot' 'polDeg' 'thetaMin'};
-inpForm.defval = {[] 0 0 0 0 false 5 0 };
-inpForm.size = {[-1 -2] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] };
-inpForm.soft = {true false false false false false false false };
+inpForm.fname = {'dE' 'ki' 'Ei' 'kf' 'Ef' 'plot' 'polDeg' 'thetaMin' 'thetaMax'};
+inpForm.defval = {[] 0 0 0 0 false 5 5 135};
+inpForm.size = {[-1 -2] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1]};
+inpForm.soft = {true false false false false false false false false};
inpForm.fname = [inpForm.fname {'formFact' 'dQ' 'norm' 'useRaw' 'func' 'fid'}];
inpForm.defval = [inpForm.defval { 'auto' 0 false true func0 -1 }];
@@ -221,16 +229,19 @@
stdG = param.dQ/2.35482;
for jj = 1:nPlot
- swConv = spectra.swConv{jj};
- swConvTemp = swConv * 0;
- for ii = 1:numel(Qconv)
- % Gaussian with intensity normalised to 1, centered on E(ii)
- fG = exp(-((Qconv-Qconv(ii))/stdG).^2/2);
- fG = fG/sum(fG);
- swConvTemp = swConvTemp + swConv(:,ii) * fG;
-
+ if pref.usemex
+ spectra.swConv{jj} = sw_qconv(spectra.swConv{jj}, Qconv, stdG, pref.nthread);
+ else
+ swConv = spectra.swConv{jj};
+ swConvTemp = swConv * 0;
+ for ii = 1:numel(Qconv)
+ % Gaussian with intensity normalised to 1, centered on E(ii)
+ fG = exp(-((Qconv-Qconv(ii))/stdG).^2/2);
+ fG = fG/sum(fG);
+ swConvTemp = swConvTemp + swConv(:,ii) * fG;
+ end
+ spectra.swConv{jj} = swConvTemp;
end
- spectra.swConv{jj} = swConvTemp;
end
fprintf0(fid,'Finite instrumental momentum resolution of %5.3f A-1 is applied.\n',param.dQ);
@@ -269,28 +280,34 @@
end
if FX > 0
- k0 = param.k;
- cosT = cosd(param.thetaMin);
- sinT = sind(param.thetaMin);
-
+ kfix = param.k;
+ cosTMin = cosd(param.thetaMin);
+ sinTMin = sind(param.thetaMin);
+ cosTMax = cosd(param.thetaMax);
+ sinTMax = sind(param.thetaMax);
for jj = 1:nPlot
switch FX
case 1
% fix ki
- Emax = (k0^2-(k0*cosT-sqrt(Q.^2-k0^2*sinT^2)).^2) * sw_converter(1,'k','meV');
- Emin = (k0^2-(k0*cosT+sqrt(Q.^2-k0^2*sinT^2)).^2) * sw_converter(1,'k','meV');
+ % Ross Stewart, 9/2/23 - get proper trajectories based on thetamin *and* thetamax
+ Emax = Q*0;
+ Emin = Q*0;
+ ii = find(Q < kfix);
+ Emax(ii) = (kfix^2-(kfix*cosTMin-sqrt(Q(ii).^2-kfix^2*sinTMin^2)).^2) * sw_converter(1,'k','meV');
+ Emin(ii) = (kfix^2-(kfix*cosTMin+sqrt(Q(ii).^2-kfix^2*sinTMin^2)).^2) * sw_converter(1,'k','meV');
+ ii = find(Q > kfix);
+ Emax(ii) = (kfix^2-(kfix*cosTMax+sqrt(Q(ii).^2-kfix^2*sinTMax^2)).^2) * sw_converter(1,'k','meV');
+ Emin(ii) = (kfix^2-(kfix*cosTMax-sqrt(Q(ii).^2-kfix^2*sinTMax^2)).^2) * sw_converter(1,'k','meV');
case 2
% fix kf
- Emax = -(k0^2-(k0*cosT+sqrt(Q.^2-k0^2*sinT^2)).^2) * sw_converter(1,'k','meV');
- Emin = -(k0^2-(k0*cosT-sqrt(Q.^2-k0^2*sinT^2)).^2) * sw_converter(1,'k','meV');
+ Emax = -(kfix^2-(kfix*cosT+sqrt(Q.^2-kfix^2*sinT^2)).^2) * sw_converter(1,'k','meV');
+ Emin = -(kfix^2-(kfix*cosT-sqrt(Q.^2-kfix^2*sinT^2)).^2) * sw_converter(1,'k','meV');
end
- %Emax = (ki^2-(ki*cosT-sqrt(Q.^2-ki^2*sinT^2)).^2) * sw_converter(1,'k','meV');
- %Emin = (ki^2-(ki*cosT+sqrt(Q.^2-ki^2*sinT^2)).^2) * sw_converter(1,'k','meV');
-
+
Emax(abs(imag(Emax))>0) = 0;
Emin(abs(imag(Emin))>0) = 0;
-
+
Elist = repmat(cEvect',[1 size(spectra.swConv{jj},2)]);
Emin = repmat(Emin,[size(spectra.swConv{jj},1) 1]);
Emax = repmat(Emax,[size(spectra.swConv{jj},1) 1]);
@@ -301,7 +318,7 @@
swConv(idx) = NaN;
spectra.swConv{jj} = swConv;
end
- fprintf0(fid,'Energy transfer is limited to instrument, using %s=%5.3f A-1.\n',kstr,k0);
+ fprintf0(fid,'Energy transfer is limited to instrument, using %s=%5.3f A-1.\n',kstr,kfix);
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
diff --git a/swfiles/sw_markdown.m b/swfiles/sw_markdown.m
index 329255a98..92ac4e216 100644
--- a/swfiles/sw_markdown.m
+++ b/swfiles/sw_markdown.m
@@ -15,6 +15,11 @@
%
% [swdoc], [matlab.help]
%
+if verLessThan('Matlab','8.7')
+ nwl = sprintf('\n');
+else
+ nwl = newline;
+end
if nargin<2
hotlinks = feature('HotLinks');
@@ -66,8 +71,8 @@
end
% remove the note marking
-str = regexprep(str,'{{warning (.+?)}}',[newline repmat([symbol('skull') ' '],1,37) newline '$1' newline repmat([symbol('skull') ' '],1,37)]);
-str = regexprep(str,'{{note (.+?)}}',[newline repmat('~',1,75) newline '$1' newline repmat('~',1,75)]);
+str = regexprep(str,'{{warning (.+?)}}',[nwl repmat([symbol('skull') ' '],1,37) nwl '$1' nwl repmat([symbol('skull') ' '],1,37)]);
+str = regexprep(str,'{{note (.+?)}}',[nwl repmat('~',1,75) nwl '$1' nwl repmat('~',1,75)]);
if hotlinks
str = regexprep(str,'`([\s\,\.\)\:])','$1');
@@ -100,6 +105,6 @@
str = regexprep(str,'([\^\_]{1,1})\-([0-9]{1,1})','${symbol([''\\\\'' $1 ''-\\\\'' $1 $2])}');
str = regexprep(str,'([\^\_]{1,1}[0-9]{1,1})','${symbol($1)}');
-%helpStr = regexprep(helpStr,'\n\s*?\n\s*?\n',[newLine newLine]);
+%helpStr = regexprep(helpStr,'\n\s*?\n\s*?\n',[nwl nwl]);
end
\ No newline at end of file
diff --git a/swfiles/sw_mex.m b/swfiles/sw_mex.m
old mode 100644
new mode 100755
index 5dd03bb71..68ee46689
--- a/swfiles/sw_mex.m
+++ b/swfiles/sw_mex.m
@@ -14,6 +14,10 @@ function sw_mex(varargin)
% switch on using mex files in [spinw.spinwave].
%
% ### Name-Value Pair Arguments
+%
+% `'compile'`
+% : If `false`, mex files will not be compiled. Default is
+% `true`.
%
% `'test'`
% : If `true`, the compiled .mex files will be tested. Default is
@@ -37,29 +41,77 @@ function sw_mex(varargin)
if param.compile
% save current folder
aDir = pwd;
+ eig_omp_dir = [sw_rootdir filesep 'external' filesep 'eig_omp'];
+ chol_omp_dir = [sw_rootdir filesep 'external' filesep 'chol_omp'];
+ mtimesx_dir = [sw_rootdir filesep 'external' filesep 'mtimesx'];
+ swloop_dir = [sw_rootdir filesep 'external' filesep 'swloop'];
+ swqconv_dir = [sw_rootdir filesep 'external' filesep 'sw_qconv'];
+ eigen_ver = '3.4.0';
+ if ~exist([sw_rootdir filesep 'eigen-' eigen_ver], 'dir')
+ cd(sw_rootdir);
+ disp('Downloading Eigen')
+ mkdir(['eigen-' eigen_ver]);
+ cd(['eigen-' eigen_ver]);
+ urlwrite(['https://eigen.googlesource.com/mirror/+archive/refs/tags/' eigen_ver '.tar.gz'], 'eigen.tar.gz');
+ gunzip('eigen.tar.gz');
+ untar('eigen.tar');
+ cd(aDir);
+ end
% compile the mex files
if ispc
- cd([sw_rootdir filesep 'external' filesep 'eig_omp']);
+ cd(eig_omp_dir);
mex('-v','-largeArrayDims','eig_omp.cpp','-lmwlapack',...
'COMPFLAGS=$COMPFLAGS /openmp','LINKFLAGS=$LINKFLAGS /nodefaultlib:vcomp "$MATLABROOT\bin\win64\libiomp5md.lib"')
- cd([sw_rootdir filesep 'external' filesep 'chol_omp']);
+ cd(chol_omp_dir);
mex('-v','-largeArrayDims','chol_omp.cpp','-lmwlapack',...
'-lmwblas','COMPFLAGS=$COMPFLAGS /openmp',...
'LINKFLAGS=$LINKFLAGS /nodefaultlib:vcomp "$MATLABROOT\bin\win64\libiomp5md.lib"')
+ cd(mtimesx_dir);
+ mex('-v','-largeArrayDims','sw_mtimesx.c','-lmwblas','COMPFLAGS=$COMPFLAGS /openmp',...
+ 'LINKFLAGS=$LINKFLAGS /nodefaultlib:vcomp "$MATLABROOT\bin\win64\libiomp5md.lib"')
+ cd(swloop_dir);
+ mex('-R2018a',['COMPFLAGS= /I ' sw_rootdir filesep 'eigen-' eigen_ver],'swloop.cpp')
elseif ismac
- cd([sw_rootdir filesep 'external' filesep 'eig_omp']);
- mex('-v','-largeArrayDims','eig_omp.cpp','-lmwlapack','COMPFLAGS="/openmp $COMPFLAGS"','CXXFLAGS=$CXXFLAGS -fopenmp -pthread');
- cd([sw_rootdir filesep 'external' filesep 'chol_omp']);
- mex('-v','-largeArrayDims','chol_omp.cpp','-lmwlapack','-lmwblas','COMPFLAGS="/openmp $COMPFLAGS"','CXXFLAGS=$CXXFLAGS -fopenmp -pthread');
- %cd([sw_rootdir filesep 'external' filesep 'mtimesx']);
- %mex('-DDEFINEUNIX','-largeArrayDims','mtimesx.c','-lmwblas');
+ % add =libiomp5 after -fopenmp?
+ if strcmp(mexext, 'mexmaca64')
+ mwlinklib = '-L$MATLABROOT/bin/maca64';
+ else
+ mwlinklib = '-L$MATLABROOT/sys/os/maci64';
+ end
+ if exist('/opt/homebrew/opt/libomp/include') == 7
+ omp_inc = '-I/opt/homebrew/opt/libomp/include';
+ omp_lib = '-lomp';
+ else
+ omp_inc = '-I/usr/local/opt/libomp/include';
+ omp_lib = '-liomp5';
+ end
+ cd(eig_omp_dir);
+ mex('-v','-largeArrayDims','eig_omp.cpp', 'CXX_FLAGS="-Xclang -fopenmp -pthread"', ...
+ ['LDFLAGS="$LDFLAGS ' mwlinklib ' ' omp_lib ' -lmwblas -lmwlapack"'], ...
+ 'CXXOPTIMFLAGS="$CXXOPTIMFLAGS -Xclang -fopenmp"', omp_inc);
+ cd(chol_omp_dir);
+ mex('-v','-largeArrayDims','chol_omp.cpp', 'CXX_FLAGS="-Xclang -fopenmp -pthread"', ...
+ ['LDFLAGS="$LDFLAGS ' mwlinklib ' ' omp_lib ' -lmwblas -lmwlapack"'], ...
+ 'CXXOPTIMFLAGS="$CXXOPTIMFLAGS -Xclang -fopenmp"', omp_inc);
+ cd(mtimesx_dir);
+ mex('-v','-largeArrayDims','sw_mtimesx.c', 'CXX_FLAGS="-Xclang -fopenmp -pthread"', ...
+ ['LDFLAGS="$LDFLAGS ' mwlinklib ' ' omp_lib ' -lmwblas"'], ...
+ 'CXXOPTIMFLAGS="$CXXOPTIMFLAGS -Xclang -fopenmp"', omp_inc);
+ cd(swloop_dir);
+ mex('-v','-R2018a',['-I' sw_rootdir filesep 'eigen-' eigen_ver],'swloop.cpp')
else
% linux?
- cd([sw_rootdir filesep 'external' filesep 'eig_omp']);
- mex('-v','-largeArrayDims','eig_omp.cpp','-lmwlapack','CXXFLAGS=$CXXFLAGS -fopenmp -pthread','LDFLAGS=$LDFLAGS -liomp5')
- cd([sw_rootdir filesep 'external' filesep 'chol_omp']);
- mex('-v','-largeArrayDims','chol_omp.cpp','-lmwlapack','-lmwblas','CXXFLAGS=$CXXFLAGS -fopenmp -pthread','LDFLAGS=$LDFLAGS -liomp5')
+ cd(eig_omp_dir);
+ mex('-v','-largeArrayDims','eig_omp.cpp','-lmwlapack','CXXFLAGS=$CXXFLAGS -fopenmp -pthread','LDFLAGS=$LDFLAGS -fopenmp')
+ cd(chol_omp_dir);
+ mex('-v','-largeArrayDims','chol_omp.cpp','-lmwlapack','-lmwblas','CXXFLAGS=$CXXFLAGS -fopenmp -pthread','LDFLAGS=$LDFLAGS -fopenmp')
+ cd(mtimesx_dir);
+ mex('-v','-largeArrayDims','sw_mtimesx.c','-lmwblas','CXXFLAGS=$CXXFLAGS -fopenmp -pthread','LDFLAGS=$LDFLAGS -fopenmp')
+ cd(swloop_dir);
+ mex('-v','-R2018a',['-I' sw_rootdir filesep 'eigen-' eigen_ver],'swloop.cpp')
end
+ cd(swqconv_dir);
+ mex('-R2018a','sw_qconv.cpp')
% return back to original folder
cd(aDir);
end
@@ -78,17 +130,17 @@ function sw_mex(varargin)
% Functionality tests - should not give any errors, just want to check that correct number of outputs given.
% {
- [V,E]=eig_omp(zhe);
- D=eig_omp(zhe);
+ [V, E]=eig_omp(zhe);
+ D = eig_omp(zhe);
eig_omp(zhe);
- [V,E]=eig_omp(dsy);
- D=eig_omp(dsy);
+ [V, E]=eig_omp(dsy);
+ D = eig_omp(dsy);
eig_omp(dsy);
- [V,E]=eig_omp(zge);
- D=eig_omp(zge);
+ [V, E]=eig_omp(zge);
+ D = eig_omp(zge);
eig_omp(zge);
- [V,E]=eig_omp(dge);
- D=eig_omp(dge);
+ [V, E]=eig_omp(dge);
+ D = eig_omp(dge);
eig_omp(dge);
%}
@@ -107,10 +159,10 @@ function sw_mex(varargin)
[V1,E1]=eig_omp(zge); [V2,E2]=eig(zge); fprintf('Complex general \t% 10.5g\t\t% 10.5g\n',abs(sum(diag(E1)-diag(E2))),sum(sum(abs(V1)-abs(V2))));
fprintf('\nConsistency tests:\tV*E*inv(V)-A\tV*E*V''-A\tV*V''-I\t\tV''*V-I\n');
fprintf('------------------------------------------------------------------------------\n');
- [V,E]=eig_omp(dsy); fprintf('Real symmetric \t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E*inv(V)-dsy)),sum(sum(V*E*V'-dsy)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
- [V,E]=eig_omp(zhe); fprintf('Complex hermitian\t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E*inv(V)-zhe)),sum(sum(V*E*V'-zhe)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
- [V,E]=eig_omp(dge); fprintf('Real general \t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E*inv(V)-dge)),sum(sum(V*E*V'-dge)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
- [V,E]=eig_omp(zge); fprintf('Complex general \t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E*inv(V)-zge)),sum(sum(V*E*V'-zge)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
+ [V,E]=eig_omp(dsy); fprintf('Real symmetric \t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E/V-dsy)),sum(sum(V*E*V'-dsy)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
+ [V,E]=eig_omp(zhe); fprintf('Complex hermitian\t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E/V-zhe)),sum(sum(V*E*V'-zhe)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
+ [V,E]=eig_omp(dge); fprintf('Real general \t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E/V-dge)),sum(sum(V*E*V'-dge)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
+ [V,E]=eig_omp(zge); fprintf('Complex general \t% 10.4g\t% 10.4g\t% 10.4g\t% 10.4g\n',sum(sum(V*E/V-zge)),sum(sum(V*E*V'-zge)),sum(sum(V*V'-eye(n))),sum(sum(V'*V-eye(n))));
%}
matfn{1} = @(a,b) triu(a)+triu(a,1)';
@@ -124,10 +176,10 @@ function sw_mex(varargin)
nt = 10; if(nt>nb); nt=nb; end
for tp=1:4
for ii=1:nt; B(:,:,ii) = matfn{tp}(rand(n),rand(n)); end
- for ii=1:nt;
+ for ii=1:nt
[vv,D]=eig(B(:,:,ii)); [~,isr]=sort(real(diag(D)),'descend');
V1(:,:,ii)=vv(:,isr); E1(:,ii)=diag(D); E1(:,ii)=E1(isr,ii);
- end;
+ end
[V2,E2]=eig_omp(B,'sort',-1);
sortres(:,tp) = [sum(sum(sum(abs(V1)-abs(V2)))) sum(sum(abs(E1-E2)))];
end
@@ -149,7 +201,7 @@ function sw_mex(varargin)
% V1(:,:,ii)=vv(:,isr); E1(:,ii) = D(isr);
%end;
tic; [V1,E1]=eigorth(B,eps); t1(tp)=toc;
- tic; [V2,E2]=eig_omp(B,'orth','sort\n'); t2(tp)=toc;
+ tic; [V2,E2]=eig_omp(B,'orth','sort'); t2(tp)=toc;
for ii=1:nb; [~,isr]=sort(real(E1(:,ii))); V1(:,:,ii)=V1(:,isr,ii); E1(:,ii) = E1(isr,ii); end
orthres(:,tp) = [sum(sum(sum(abs(V1)-abs(V2)))) sum(sum(abs(E1-E2)))];
end
@@ -167,11 +219,11 @@ function sw_mex(varargin)
for tp=1:4
for ii=1:nb; B(:,:,ii) = matfn{tp}(rand(n),rand(n)); end
tic; [V,E]=eig_omp(B); t1(tp)=toc;
- for ii=1:nb;
+ for ii=1:nb
ch(ii)=sum(sum(V(:,:,ii)*diag(E(:,ii))*V(:,:,ii)'-B(:,:,ii))); ch2(ii)=sum(sum(V(:,:,ii)'*V(:,:,ii)-eye(size(V(:,:,ii)))));
end; check1(tp,:)=[sum(ch) sum(ch2)];
tic; for ii=1:size(E,2); [V(:,:,ii),ee]=eig(B(:,:,ii)); E2(:,ii)=diag(ee); end; t2(tp)=toc;
- for ii=1:nb;
+ for ii=1:nb
ch(ii)=sum(sum(V(:,:,ii)*diag(E(:,ii))*V(:,:,ii)'-B(:,:,ii))); ch2(ii)=sum(sum(V(:,:,ii)'*V(:,:,ii)-eye(size(V(:,:,ii)))));
end; check2(tp,:)=[sum(ch) sum(ch2)];
check3(tp)=sum(sum(E-E2));
@@ -217,14 +269,14 @@ function sw_mex(varargin)
fprintf('Complex upper \t% 12.5g\t% 12.5g\t% 12.5g\t% 12.5g\t% 12.5g\n',t2(3)+t2(7),t1(3)+t1(7),(t1(3)+t1(7))/(t2(3)+t2(7)),ch(3),ch(7));
fprintf('Complex lower \t% 12.5g\t% 12.5g\t% 12.5g\t% 12.5g\t% 12.5g\n',t2(4)+t2(8),t1(4)+t1(8),(t1(4)+t1(8))/(t2(4)+t2(8)),ch(4),ch(8));
- if(mod(n,2)==1); n=n+1; end;
+ if(mod(n,2)==1); n=n+1; end
clear mm; clear K1; clear inv1;
g=diag([ones(1,n/2) -ones(1,n/2)]);
for tp=1:4
% These matrices should be positive definite (with large diagonal elements)
for ii=1:nb; mm(:,:,ii) = chlfn{tp}(rand(n),rand(n)); end
- tic; for ii=1:nb;
- if(tp>2); K = chol(mm(:,:,ii),'lower'); else; K = chol(mm(:,:,ii)); end;
+ tic; for ii=1:nb
+ if(tp>2); K = chol(mm(:,:,ii),'lower'); else; K = chol(mm(:,:,ii)); end
inv1(:,:,ii)=inv(K); K1(:,:,ii)=K*g*K';
end; t1(tp)=toc;
tic; if(tp>2); [K2,inv2] = chol_omp(mm,'Colpa','lower'); else; [K2,inv2] = chol_omp(mm,'Colpa'); end; t2(tp)=toc;
@@ -241,63 +293,112 @@ function sw_mex(varargin)
end
if param.swtest
+ pref = swpref;
+ import matlab.unittest.TestCase
+ import matlab.unittest.constraints.IsEqualTo
+ import matlab.unittest.constraints.RelativeTolerance
+ import matlab.unittest.constraints.AbsoluteTolerance
+ theseBounds = RelativeTolerance(0.05) | AbsoluteTolerance(5e-6);
+ testCase = TestCase.forInteractiveUse;
% Antiferromagnetic square lattice (tutorial 4, Cu+1 S=1) - small system [3 spins, n=6]
- AFsq = sw;
+ AFsq = spinw;
AFsq.genlattice('lat_const',[3 3 10],'angled',[90 90 90],'sym',0)
AFsq.addatom('r',[0 0 0],'S', 1,'label','Cu1','color','b');
AFsq.gencoupling('maxDistance',9)
- AFsq.addmatrix('label','J1','value',1,'color','red'); AFsq.addcoupling('J1',1)
- AFsq.addmatrix('label','J2','value',-0.1,'color','green'); AFsq.addcoupling('J2',2)
- AFsq.genmagstr('mode','helical','k',[1/2 1/2 0],'n',[0 0 1], 'S',[1; 0; 0],'nExt',[1 1 1]);
+ AFsq.addmatrix('label','J1','value',1,'color','red'); AFsq.addcoupling('mat','J1','bond',1)
+ AFsq.addmatrix('label','J2','value',-0.1,'color','green'); AFsq.addcoupling('mat','J2','bond',2)
+ AFsq.addmatrix('label','D','value',diag([-0.2 0 0])); AFsq.addaniso('D');
+ AFsq.genmagstr('mode','helical','k',[1/2 1/2 0],'n',[0 0 1], 'S',[1; 0; 0],'nExt',[2 2 1]);
+ AFsq.optmagsteep
%plot(AFsq,'range',[2 2 0.5],'zoom',-1)
% Runs test
hkl = {[1/4 3/4 0] [1/2 1/2 0] [1/2 0 0] [3/4 1/4 0] [1 0 0] [3/2 0 0] 50000};
nm = 15; % Ensure same number of slices for all tests
- tic; linespec_herm_mex = AFsq.spinwave(hkl,'hermit',true,'useMex',true,'optmem',nm,'fid',0); t1=toc;
- tic; linespec_herm_nomex = AFsq.spinwave(hkl,'hermit',true,'useMex',false,'optmem',nm,'fid',0); t2=toc;
- tic; linespec_nonherm_mex = AFsq.spinwave(hkl,'hermit',false,'useMex',true,'optmem',nm,'fid',0); t3=toc;
- tic; linespec_nonherm_nomex = AFsq.spinwave(hkl,'hermit',false,'useMex',false,'optmem',nm,'fid',0);t4=toc;
+ pref.usemex = true;
+ tic; linespec_herm_mex = AFsq.spinwave(hkl,'hermit',true,'optmem',nm,'fid',0); t1=toc;
+ pref.usemex = false;
+ tic; linespec_herm_nomex = AFsq.spinwave(hkl,'hermit',true,'optmem',nm,'fid',0); t2=toc;
+ pref.usemex = true;
+ tic; linespec_nonherm_mex = AFsq.spinwave(hkl,'hermit',false,'optmem',nm,'fid',0); t3=toc;
+ pref.usemex = false;
+ tic; linespec_nonherm_nomex = AFsq.spinwave(hkl,'hermit',false,'optmem',nm,'fid',0); t4=toc;
+
+ emax = ceil(max(linespec_herm_nomex.omega(:))/10)*10; evect = linspace(0, emax, 100);
+ sp_mex = sw_egrid(sw_neutron(linespec_herm_mex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ sp_nomex = sw_egrid(sw_neutron(linespec_herm_nomex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ testCase.verifyThat(sort(real(sp_mex.omega)), IsEqualTo(sort(real(sp_nomex.omega)), 'Within', theseBounds));
+ testCase.verifyThat(sp_mex.swConv, IsEqualTo(sp_nomex.swConv, 'Within', theseBounds));
+ %figure; sw_plotspec(sp_mex); figure; sw_plotspec(sp_nomex);
+ %dsW = sp_mex.swConv - sp_nomex.swConv; dsW(find(abs(dsW)<1e-5)) = NaN; figure; pcolor(dsW); shading flat;
+ sp_mex = sw_egrid(sw_neutron(linespec_nonherm_mex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ sp_nomex = sw_egrid(sw_neutron(linespec_nonherm_nomex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ testCase.verifyThat(sort(real(sp_mex.omega)), IsEqualTo(sort(real(sp_nomex.omega)), 'Within', theseBounds));
+ testCase.verifyThat(sp_mex.swConv, IsEqualTo(sp_nomex.swConv, 'Within', theseBounds));
+ %figure; sw_plotspec(sp_mex); figure; sw_plotspec(sp_nomex);
+ %dsW = sp_mex.swConv - sp_nomex.swConv; dsW(find(abs(dsW)<1e-5)) = NaN; figure; pcolor(dsW); shading flat;
- fprintf(' %16s %16s %16s %16s\n','Hermitian Mex','Hermitian NoMex','NonHermitian Mex','NonHermitian NoMex');
- fprintf('Run Time(s) % 16.6f % 16.6f % 16.6f % 16.6f\n',t1*5,t2*5,t3*5,t4*5);
+ hdr = sprintf(' %16s %16s %16s %16s','Hermitian Mex','Hermitian NoMex','NonHermitian Mex','NonHermitian NoMex');
+ rr1 = sprintf('Run Time(s) % 16.6f % 16.6f % 16.6f % 16.6f',t1*5,t2*5,t3*5,t4*5);
+ fprintf('%s\n%s\n', hdr, rr1);
% Times
% ndlt811: 43.7453 72.0950 406.1366 611.1799
% eryenyo: 24.1470 85.5668 382.7930 563.6103
% ndl01wkc26243: 36.2106 107.8983 527.3301 796.2985
+ % simon_i9_32Gb: 113.8068 149.5165 159.9780 260.2178
+ % simonWork: 173.9701 231.1420 216.0472 455.3942
% KCu3As2O7(OD)3 kagome (Nilsen PRB 89 140412) - Tutorial 18, medium sized [18 spins, n=36]
J = -2; Jp = -1; Jab = 0.75; Ja = -J/.66 - Jab; Jip = 0.01;
- hK = sw;
+ hK = spinw;
hK.genlattice('lat_const',[10.2 5.94 7.81],'angled',[90 117.7 90],'sym','C 2/m');
hK.addatom('r',[0 0 0],'S',1/2,'label','MCu2','color','b');
hK.addatom('r',[1/4 1/4 0],'S',1/2,'label','MCu2','color','k');
hK.gencoupling;
- hK.addmatrix('label','J', 'color','r', 'value',J); hK.addcoupling('J',1);
- hK.addmatrix('label','J''','color','g', 'value',Jp); hK.addcoupling('J''',2);
- hK.addmatrix('label','Ja', 'color','b', 'value',Ja); hK.addcoupling('Ja',3);
- hK.addmatrix('label','Jab','color','cyan','value',Jab); hK.addcoupling('Jab',5);
- hK.addmatrix('label','Jip','color','gray','value',Jip); hK.addcoupling('Jip',10);
+ hK.addmatrix('label','J', 'color','r', 'value',J); hK.addcoupling('mat','J','bond',1);
+ hK.addmatrix('label','J''','color','g', 'value',Jp); hK.addcoupling('mat','J''','bond',2);
+ hK.addmatrix('label','Ja', 'color','b', 'value',Ja); hK.addcoupling('mat','Ja','bond',3);
+ hK.addmatrix('label','Jab','color','cyan','value',Jab); hK.addcoupling('mat','Jab','bond',5);
+ hK.addmatrix('label','Jip','color','gray','value',Jip); hK.addcoupling('mat','Jip','bond',10);
hK.genmagstr('mode','helical','n',[0 0 1],'S',[1 0 0]','k',[0.77 0 0.115],'next',[1 1 1]);
optpar.func = @gm_planar;
optpar.nRun = 5;
optpar.xmin = [ zeros(1,6), 0.5 0 0.0, 0 0];
optpar.xmax = [2*pi*ones(1,6), 1.0 0 0.5, 0 0];
magoptOut = hK.optmagstr(optpar);
- kOpt = hK.mag_str.k;
+ kOpt = hK.mag_str.k(:)';
hK.genmagstr('mode','helical','n',[0 0 1],'S',[1 0 0]','k',kOpt,'next',[1 1 1]);
%plot(hK,'range',[2 2 0.3],'sSpin',2)
% Runs test
hkl = {[0 0 0] [1 0 0] 50000};
nm = 10; % Ensure same number of slices for all tests
- tic; linespec_herm_mex = hK.spinwave(hkl,'hermit',true,'useMex',true,'optmem',nm); t1=toc;
- tic; linespec_herm_nomex = hK.spinwave(hkl,'hermit',true,'useMex',false,'optmem',nm); t2=toc;
- tic; linespec_nonherm_mex = hK.spinwave(hkl,'hermit',false,'useMex',true,'optmem',nm); t3=toc;
- tic; linespec_nonherm_nomex = hK.spinwave(hkl,'hermit',false,'useMex',false,'optmem',nm); t4=toc;
- fprintf(' %16s %16s %16s %16s\n','Hermitian Mex','Hermitian NoMex','NonHermitian Mex','NonHermitian NoMex');
- fprintf('Run Time(s) % 16.6f % 16.6f % 16.6f % 16.6f\n',t1,t2,t3,t4);
+ pref.usemex = true;
+ tic; linespec_herm_mex = hK.spinwave(hkl,'hermit',true,'optmem',nm); t1=toc;
+ pref.usemex = false;
+ tic; linespec_herm_nomex = hK.spinwave(hkl,'hermit',true,'optmem',nm); t2=toc;
+ pref.usemex = true;
+ tic; linespec_nonherm_mex = hK.spinwave(hkl,'hermit',false,'optmem',nm); t3=toc;
+ pref.usemex = false;
+ tic; linespec_nonherm_nomex = hK.spinwave(hkl,'hermit',false,'optmem',nm); t4=toc;
+
+ evect = linspace(0, ceil(max(linespec_herm_nomex.omega(:))/10)*10, 100);
+ sp_mex = sw_egrid(sw_neutron(linespec_herm_mex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ sp_nomex = sw_egrid(sw_neutron(linespec_herm_nomex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ testCase.verifyThat(sort(real(sp_mex.omega)), IsEqualTo(sort(real(sp_nomex.omega)), 'Within', theseBounds));
+ testCase.verifyThat(sp_mex.swConv, IsEqualTo(sp_nomex.swConv, 'Within', theseBounds));
+ %figure; sw_plotspec(sp_mex); figure; sw_plotspec(sp_nomex);
+ %dsW = sp_mex.swConv - sp_nomex.swConv; dsW(find(abs(dsW)<1e-5)) = NaN; figure; pcolor(dsW); shading flat;
+ sp_mex = sw_egrid(sw_neutron(linespec_nonherm_mex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ sp_nomex = sw_egrid(sw_neutron(linespec_nonherm_nomex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ testCase.verifyThat(sort(real(sp_mex.omega)), IsEqualTo(sort(real(sp_nomex.omega)), 'Within', theseBounds));
+ testCase.verifyThat(sp_mex.swConv, IsEqualTo(sp_nomex.swConv, 'Within', theseBounds));
+ %figure; sw_plotspec(sp_mex); figure; sw_plotspec(sp_nomex);
+ %dsW = sp_mex.swConv - sp_nomex.swConv; dsW(find(abs(dsW)<1e-5)) = NaN; figure; pcolor(dsW); shading flat;
+
+ rr2 = sprintf('Run Time(s) % 16.6f % 16.6f % 16.6f % 16.6f',t1,t2,t3,t4);
+ fprintf('%s\n%s\n', hdr, rr2);
%{
[~,nSuperlat] = rat(hK.mag_str.k,0.01);
@@ -318,20 +419,22 @@ function sw_mex(varargin)
% ndlt811: 26.4661 40.5760 60.6112 63.3341
% eryenyo: 7.6710 36.0402 56.3213 73.5568
% ndl01wkc26243: 17.2135 42.8147 74.9062 106.8541
+ % simon_i9_32Gb: 49.5562 55.9125 54.1221 65.5144
+ % simonWork: 68.3912 84.6127 75.3424 100.9470
% Bi4Fe5O13F - large(ish) system [80 spins, n=160]
ff=11.6*2.5; Jc1 = 34/ff; Jc2 = 20/ff; Jab1 = 45/ff; Jab2 = 74/ff; Jd = 191/ff;
- bfof = sw();
+ bfof = spinw();
bfof.genlattice('lat_const',[8.29950 8.29950 18.05730],'angled',[90 90 90],'sym','P 42/m b c');
bfof.addatom('r',[0.5 0. 0.0800],'S',2.5,'color',[0 0 255],'label','Fe1');
bfof.addatom('r',[0.8515 0.8388 0], 'S',2.5,'color',[255 0 0],'label','Fe2');
bfof.addatom('r',[0.5 0. 0.25 ],'S',2.5,'color',[0 0 128],'label','Fe1_3');
bfof.gencoupling('maxBond',99,'maxDistance',10);
- bfof.addmatrix('value',Jc1,'label','Jc1','color','r'); bfof.addcoupling('Jc1',1);
- bfof.addmatrix('value',Jc2,'label','Jc2','color',[128 0 0]); bfof.addcoupling('Jc2',2);
- bfof.addmatrix('value',Jab1,'label','Jab1','color','b'); bfof.addcoupling('Jab1',3);
- bfof.addmatrix('value',Jab2,'label','Jab2','color',[0 255 0]); bfof.addcoupling('Jab2',4);
- bfof.addmatrix('value',Jd,'label','Jd','color','k'); bfof.addcoupling('Jd',5);
+ bfof.addmatrix('value',Jc1,'label','Jc1','color','r'); bfof.addcoupling('mat','Jc1','bond',1);
+ bfof.addmatrix('value',Jc2,'label','Jc2','color',[128 0 0]); bfof.addcoupling('mat','Jc2','bond',2);
+ bfof.addmatrix('value',Jab1,'label','Jab1','color','b'); bfof.addcoupling('mat','Jab1','bond',3);
+ bfof.addmatrix('value',Jab2,'label','Jab2','color',[0 255 0]); bfof.addcoupling('mat','Jab2','bond',4);
+ bfof.addmatrix('value',Jd,'label','Jd','color','k'); bfof.addcoupling('mat','Jd','bond',5);
bfof.addmatrix('value',diag([0 0 0.2]),'label','D'); bfof.addaniso('D');
S2a = -[4.05 -0.35 0]; S2b = -[-0.35 4.05 0]; S1a = [2.18 -2.53 0]; S1b = [2.53 2.18 0];
S = [S1b; S1a; S1a; S1b; S1b; S1a; S1a; S1b; S2a; -S2a; -S2b; S2b; S2b; -S2b; S2a; -S2a; -S1b; -S1a; -S1b; -S1a];
@@ -346,17 +449,39 @@ function sw_mex(varargin)
nm = 60; % For laptops with 16GB memory, to have the same number of slices
%nm = 6; % For the workstation with 50GB.
hkl={[0 0 0] [1 1 0] [1 1 1] [0 0 1] 2000};
- tic; linespec_herm_mex = bfof.spinwave(hkl,'hermit',true,'useMex',true,'optmem',nm); t1=toc;
- tic; linespec_herm_nomex = bfof.spinwave(hkl,'hermit',true,'useMex',false,'optmem',nm); t2=toc;
- tic; linespec_nonherm_mex = bfof.spinwave(hkl,'hermit',false,'useMex',true,'optmem',nm); t3=toc;
- tic; linespec_nonherm_nomex = bfof.spinwave(hkl,'hermit',false,'useMex',false,'optmem',nm); t4=toc;
+ pref.usemex = true;
+ tic; linespec_herm_mex = bfof.spinwave(hkl,'hermit',true,'optmem',nm); t1=toc;
+ pref.usemex = false;
+ tic; linespec_herm_nomex = bfof.spinwave(hkl,'hermit',true,'optmem',nm); t2=toc;
+ pref.usemex = true;
+ tic; linespec_nonherm_mex = bfof.spinwave(hkl,'hermit',false,'optmem',nm); t3=toc;
+ pref.usemex = false;
+ tic; linespec_nonherm_nomex = bfof.spinwave(hkl,'hermit',false,'optmem',nm); t4=toc;
- fprintf(' %16s %16s %16s %16s\n','Hermitian Mex','Hermitian NoMex','NonHermitian Mex','NonHermitian NoMex');
- fprintf('Run Time(s) % 16.6f % 16.6f % 16.6f % 16.6f\n',t1,t2,t3,t4);
+ evect = linspace(0, ceil(max(linespec_herm_nomex.omega(:))/10)*10, 100);
+ sp_mex = sw_egrid(sw_neutron(linespec_herm_mex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ sp_nomex = sw_egrid(sw_neutron(linespec_herm_nomex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ testCase.verifyThat(sort(real(sp_mex.omega)), IsEqualTo(sort(real(sp_nomex.omega)), 'Within', theseBounds));
+ testCase.verifyThat(sp_mex.swConv, IsEqualTo(sp_nomex.swConv, 'Within', theseBounds));
+ %figure; sw_plotspec(sp_mex); figure; sw_plotspec(sp_nomex);
+ %dsW = sp_mex.swConv - sp_nomex.swConv; dsW(find(abs(dsW)<1e-5)) = NaN; figure; pcolor(dsW); shading flat;
+ sp_mex = sw_egrid(sw_neutron(linespec_nonherm_mex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ sp_nomex = sw_egrid(sw_neutron(linespec_nonherm_nomex),'component','Sperp', 'Evect', evect, 'imagChk', false);
+ testCase.verifyThat(sort(real(sp_mex.omega)), IsEqualTo(sort(real(sp_nomex.omega)), 'Within', theseBounds));
+ testCase.verifyThat(sp_mex.swConv, IsEqualTo(sp_nomex.swConv, 'Within', theseBounds));
+ %figure; sw_plotspec(sp_mex); figure; sw_plotspec(sp_nomex);
+ %dsW = sp_mex.swConv - sp_nomex.swConv; dsW(find(abs(dsW)<1e-5)) = NaN; figure; pcolor(dsW); shading flat;
+
+ rr3 = sprintf('Run Time(s) % 16.6f % 16.6f % 16.6f % 16.6f',t1,t2,t3,t4);
+ fprintf('%s\n%s\n', hdr, rr3);
% Times
% ndlt811: 123.6812 139.6476 197.1951 363.2818
% eryenyo: 127.4535 135.8621 220.9112 389.4686
% ndl01wkc26243: 67.8451 117.7316 146.2180 474.1894
-
+ % simon_i9_32Gb: 74.31700 84.7511 253.0013 342.0254
+ % simonWork: 75.40301 107.8941 216.4226 467.2630
+
+ fprintf('%s\n%s (small model)\n%s (medium model)\n%s (large model)\n', hdr, rr1, rr2, rr3);
+
+end
end
-end
\ No newline at end of file
diff --git a/swfiles/sw_model.m b/swfiles/sw_model.m
index 4bcdc0fe5..b020690d8 100644
--- a/swfiles/sw_model.m
+++ b/swfiles/sw_model.m
@@ -21,7 +21,10 @@
% 120\\deg angle and optimised magnetic structure.
% * `'squareAF'` Square lattice antiferromagnet.
% * `'chain'` Chain with further neighbor interactions.
-%
+% * `swm_*` Custom models which are in the matlab path can be
+% evaluated. Checkout:
+% https://www.github.com/spinw/Models for pre-made models.
+%
% `param`
% : Input parameters of the model, row vector which gives the values of the
% Heisenberg exchange for first, second, thirs etc. neighbor bonds stored
@@ -49,6 +52,7 @@
end
fprintf0(fid,'Preparing ''%s'' model ...\n',model);
+modelSearch = 'swm_';
obj = spinw;
@@ -108,7 +112,26 @@
end
end
otherwise
- error('sw_model:WrongINput','Model does not exists!')
+ % All paths
+ allPaths = strsplit(path,':');
+ % Remove the builtin functions
+ allPaths = allPaths(~strncmp(allPaths,fullfile(matlabroot, 'toolbox'), length(fullfile(matlabroot, 'toolbox'))));
+ relPaths = allPaths(~strncmp(allPaths,fullfile(matlabroot, 'example'), length(fullfile(matlabroot, 'example'))));
+ % Get all files
+ allFiles = cellfun(@(x) dir(x), relPaths, 'UniformOutput', false);
+ allFiles = cellfun(@(x) {x.name}, allFiles, 'UniformOutput', false);
+ allFiles = [allFiles{:}];
+ % Search for files which are models
+ relFiles = cellfun(@(x) x(1:end-2) ,allFiles(strncmp(allFiles, modelSearch, 3)),'UniformOutput',false);
+ if ~any(strcmp(model, relFiles))
+ error('sw_model:WrongInput','Model does not exists!')
+ end
+ % Evaluate the model
+ try
+ obj = feval(model, param);
+ catch ME
+ error('sw_model:ModelError','This model has an error!')
+ end
end
fprintf0(fid,'... ready!\n');
diff --git a/swfiles/sw_neutron.m b/swfiles/sw_neutron.m
index 33ae213e6..7715d1e0f 100644
--- a/swfiles/sw_neutron.m
+++ b/swfiles/sw_neutron.m
@@ -95,6 +95,10 @@
return
end
+if sw_issymspec(spectra)
+ error('sw_neutron:SymbolicInput', 'This function does not handle symbolic spectra');
+end
+
inpForm.fname = {'n' 'pol' 'uv' };
inpForm.defval = {[0 0 1] false {} };
inpForm.size = {[1 3] [1 1] [1 2] };
@@ -282,4 +286,4 @@
spectra.param.uv = param.uv;
spectra.param.pol = param.pol;
-end
\ No newline at end of file
+end
diff --git a/swfiles/sw_omegasum.m b/swfiles/sw_omegasum.m
index 0d1167339..62e45910b 100644
--- a/swfiles/sw_omegasum.m
+++ b/swfiles/sw_omegasum.m
@@ -37,6 +37,10 @@
% [spinw.spinwave] \| [sw_egrid]
%
+if sw_issymspec(spectra)
+ error('sw_omegasum:SymbolicInput', 'This function does not handle symbolic spectra');
+end
+
if iscell(spectra.omega)
error('sw_omegasum:NoTwin','The sw_omegasum() function doesn''t work for spectra calculated for multiple twins!');
end
@@ -76,4 +80,4 @@
spectra.omega = omegaCol;
spectra.swInt = intCol;
-end
\ No newline at end of file
+end
diff --git a/swfiles/sw_plotspec.m b/swfiles/sw_plotspec.m
index de34d9759..1c1ec9a44 100644
--- a/swfiles/sw_plotspec.m
+++ b/swfiles/sw_plotspec.m
@@ -72,8 +72,8 @@
% `'auto'`.
%
% `'sortMode'`
-% : Sorting the modes before plotting. Default is `false`. Can improve the
-% quality of the dispersion line plots if modes are crossing.
+% : Sorting the modes by energy before plotting. Default is `false`. Can
+% improve the quality of the dispersion line plots if modes are crossing.
%
% `'axLim'`
% : Upper limit for energy axis (for `mode` 1,2) or color axis (for `mode`
@@ -172,6 +172,10 @@
return
end
+if sw_issymspec(spectra)
+ error('sw_plotspec:SymbolicInput', 'This function does not handle symbolic spectra');
+end
+
if isfield(spectra,'norm')
norm0 = spectra.norm;
else
@@ -199,7 +203,6 @@
inpForm.size = [inpForm.size {[1 1] [1 1] [1 1] [1 2] [1 -7] }];
param = sw_readparam(inpForm, varargin{:});
-pref = swpref;
% plotmode string
if numel(param.mode)>1
@@ -217,6 +220,78 @@
end
end
+if ~isfield(spectra,'omega')
+ param.mode = 3;
+end
+
+if param.mode == 4
+ % PLOT EASY PEASY
+ fHandle = [];
+ pHandle = [];
+ pColor = isfield(spectra,'swConv');
+
+ % Change to "actual" mode and override some properties
+ if pColor
+ if param.dE == 0
+ param.dE = (spectra.Evect(end) - spectra.Evect(1))/50;
+ end
+ param.mode = 3;
+ param.dashed = true;
+ param.colorbar = false;
+ [fHandle, pHandle] = plotspec_internal(spectra, param);
+ end
+ % Fallthrough - plots dispersion line on top of colormap if applicable
+ if numel(spectra.hklA) ~= length(spectra.hklA)
+ hold on
+ if pColor
+ cMap0 = [0 0 0];
+ else
+ cMap0 = 'auto';
+ end
+
+ if iscell(spectra.omega)
+ omegaTemp = cell2mat(spectra.omega);
+ Emax = max(real(omegaTemp(:)));
+ clear('omegaTemp');
+ else
+ Emax = max(real(spectra.omega(:)));
+ end
+
+ param.mode = 1;
+ param.colorbar = ~pColor;
+ param.dashed = false;
+ param.title = ~pColor;
+ param.legend = ~pColor;
+ param.imag = ~pColor & param.imag;
+ param.colormap = cMap0;
+ param.axLim = [0, 1.1*Emax];
+ [fHandle, pHandle] = plotspec_internal(spectra, param);
+ else
+ error('sw_plotspec:WrongInput', ['Input looks like a powder spectrum but has no '...
+ '''swConv'' field, something has gone wrong somewhere']);
+ end
+else
+ [fHandle, pHandle] = plotspec_internal(spectra, param);
+end
+
+if ~isfield(spectra,'swConv') && param.mode>1 && param.mode<4
+ error('sw_plotspec:WrongInput',['Reference to non-existent field ''swConv'','...
+ 'use ''sw_egrid'' to produce the convoluted spectra before plotting!'])
+end
+
+if nargout >0
+ fHandle0 = fHandle;
+end
+if nargout>1
+ pHandle0 = pHandle;
+end
+
+end
+
+function [fHandle0, pHandle0] = plotspec_internal(spectra, param)
+
+pref = swpref;
+
% length, energy and temperature units
unitL = spectra.obj.unit.label{1};
unitE = spectra.obj.unit.label{2};
@@ -240,15 +315,6 @@
param.twin = 1;
end
-if ~isfield(spectra,'omega')
- param.mode = 3;
-end
-
-if ~isfield(spectra,'swConv') && param.mode>1 && param.mode<4
- error('sw_plotspec:WrongInput',['Reference to non-existent field ''swConv'','...
- 'use ''sw_egrid'' to produce the convoluted spectra before plotting!'])
-end
-
% select twins for convoluted plots
if param.mode>1 && param.mode<4 && iscell(spectra.swConv)
% number of convoluted spectras to plot
@@ -265,61 +331,7 @@
end
% Determine powder mode
-powmode = false;
-if numel(spectra.hklA)==length(spectra.hklA)
- powmode = true;
-end
-
-if param.mode == 4
- % PLOT EASY PEASY
-
- fHandle = [];
- pHandle = [];
- pColor = isfield(spectra,'swConv');
-
- if pColor
- if param.dE == 0
- Eres = (spectra.Evect(end) - spectra.Evect(1))/50;
- else
- Eres = param.dE;
- end
-
- [fHandle, pHandle] = sw_plotspec(spectra,'mode',3,'dE',Eres,...
- 'dashed',true,'colorbar',false,'axLim',param.axLim,...
- 'lineStyle',param.lineStyle,'maxPatch',...
- param.maxPatch,'qLabel',param.qlabel,'dat',param.dat,...
- 'ddat',param.ddat,'datFormat',param.datFormat);
- end
- if ~powmode
- hold on
- if pColor
- cMap0 = [0 0 0];
- else
- cMap0 = 'auto';
- end
-
- if iscell(spectra.omega)
- omegaTemp = cell2mat(spectra.omega);
- Emax = max(real(omegaTemp(:)));
- clear('omegaTemp');
- else
- Emax = max(real(spectra.omega(:)));
- end
-
- [fHandle, pHandle] = sw_plotspec(spectra,'mode','disp','colorbar',~pColor,...
- 'dashed',false,'title',~pColor,'legend',~pColor,'imag',~pColor,...
- 'lineStyle',param.lineStyle,'colormap',cMap0,'axLim',[0 1.1*Emax],...
- 'qLabel',param.qlabel);
- end
-
- if nargout >0
- fHandle0 = fHandle;
- end
- if nargout>1
- pHandle0 = pHandle;
- end
- return
-end
+powmode = numel(spectra.hklA) == length(spectra.hklA);
% Label of the x-axis
if powmode
@@ -423,10 +435,10 @@
% Defines colors for plotting modes.
%colors = flipud(fireprint(nMode+2));
if isa(param.colormap,'function_handle')
- colors = flipud(param.colormap(nMode+2));
+ colors = param.colormap(nMode+2);
else
if strcmpi(param.colormap,'auto')
- colors = flipud(cm_fireprint(nMode+2));
+ colors = cm_fireprint(nMode+2);
else
if numel(param.colormap) == 3
param.colormap = param.colormap(:);
@@ -443,14 +455,14 @@
colors = colors(2:(end-1),:);
end
- modeList = nMode/(2*nMagExt);
- if modeList == 1
+ % Accounts for "fastmode" calculations where only +ve modes are present
+ if nMode == nMagExt || nMode == 2*nMagExt
if param.imag
lLabel = {'Real' 'Imaginary'};
else
lLabel = {'Real'};
end
- elseif modeList == 3
+ elseif nMode == 3*nMagExt || nMode == 6*nMagExt
if param.imag
lLabel = {'Q+k_m' 'Q' 'Q-k_m' 'Imaginary'};
else
@@ -472,7 +484,7 @@
param.mode = 3;
end
-hPlot = [];
+hPlot = gobjects(0);
hold on
switch param.mode
@@ -495,8 +507,8 @@
'Color', colors(ii,:),'LineWidth',param.lineWidth); %#ok<*AGROW>
hLegend(incIdx) = hPlot(end);
if param.imag
- hPlot(end+1) = plot(xAxis,ploti(ii,:),'ro-');
- hLegend(modeList+1) = hPlot(end);
+ hPlot(end+1) = plot(xAxis,ploti(ii,:),'ro-');
+ hLegend(incIdx+1) = hPlot(end);
end
end
end
@@ -601,18 +613,21 @@
% for 'auto' mode an equally space hue values are created for
% use with multiple sectra plot
if nPlot>1
- param.colormap = hsv2rgb([(1:nPlot)'/nPlot ones(nPlot,2)])'*255;
+ param.colormap = flipud(hsv2rgb([(1:nPlot)'/nPlot ones(nPlot,2)])'*255);
else
param.colormap = {pref.colormap};
end
end
if ~iscell(param.colormap)
+ if numel(param.colormap) == 3
+ param.colormap = param.colormap(:);
+ end
if (size(param.colormap,1) ~= 3) || (size(param.colormap,2)=xCut(ii-1) & xAxis<=xCut(ii);
hPlot(ii-1) = image(xAxis(selIdx),yAxis,cMat(:,selIdx,:));
@@ -796,7 +812,10 @@
iName = strsplit(strtrim(sprintf('I%d ',1:nMode)),' ');
eName = strsplit(strtrim(sprintf('EN%d ',1:nMode)),' ');
sName = strsplit(strtrim(sprintf('s%d ',1:nMode)),' ');
-
+ if ~isfield(T, sName)
+ sName = strsplit(strtrim(sprintf('sigma%d ',1:nMode)),' ');
+ end
+
dat.I = zeros(nMode,nQ);
dat.E = zeros(nMode,nQ);
dat.s = zeros(nMode,nQ);
@@ -810,7 +829,7 @@
dat.s(dat.I==0) = nan;
dat.E(dat.I==0) = nan;
% reciprocal lattice
- RL = spectra.obj.rl;
+ RL = 2*pi*inv(spectra.obj.basisvector);
% distance of experimental data points from plotted data points
D = sqrt(sum(bsxfun(@minus,permute(Qexp'*RL,[1 3 2]),permute(spectra.hkl'*RL,[3 1 2])).^2,3));
diff --git a/swfiles/sw_readspec.m b/swfiles/sw_readspec.m
index e0fd52c47..122bb7f76 100644
--- a/swfiles/sw_readspec.m
+++ b/swfiles/sw_readspec.m
@@ -133,8 +133,9 @@
% sort intensity, put zero intensities to the end
[data{polIdx}.I,idx] = sort(data{polIdx}.I,1,'descend');
- data{polIdx}.E = data{polIdx}.E(sub2ind(size(data{polIdx}.E),idx,repmat(1:size(data{polIdx}.E,2),[2 1])));
- data{polIdx}.sigma = data{polIdx}.sigma(sub2ind(size(data{polIdx}.E),idx,repmat(1:size(data{polIdx}.E,2),[2 1])));
+ nModes = size(data{polIdx}.I, 1);
+ data{polIdx}.E = data{polIdx}.E(sub2ind(size(data{polIdx}.E), idx, repmat(1:size(data{polIdx}.E,2), [nModes 1])));
+ data{polIdx}.sigma = data{polIdx}.sigma(sub2ind(size(data{polIdx}.E), idx, repmat(1:size(data{polIdx}.E,2), [nModes 1])));
data{polIdx}.nMode = sum(data{polIdx}.I~=0,1);
data{polIdx}.corr = sw_parstr(modeStr{polIdx});
@@ -151,4 +152,4 @@
fclose(fid);
-end
\ No newline at end of file
+end
diff --git a/swfiles/sw_rot.m b/swfiles/sw_rot.m
index 8e40d5bf1..8a06e58bb 100644
--- a/swfiles/sw_rot.m
+++ b/swfiles/sw_rot.m
@@ -1,5 +1,5 @@
function [V, rotM] = sw_rot(rotAxis, rotAngle, V)
-% rotates vectorsin 3D
+% rotates vectors in 3D
%
% ### Syntax
%
diff --git a/swfiles/sw_spec2MDHisto.m b/swfiles/sw_spec2MDHisto.m
new file mode 100644
index 000000000..f1a0c7ce0
--- /dev/null
+++ b/swfiles/sw_spec2MDHisto.m
@@ -0,0 +1,294 @@
+function sw_spec2MDHisto(spectra,proj, dproj, filename)
+% saves spectrum to MDHisto
+%
+% ### Syntax
+%
+% sw_spec2MDHisto(spectra,proj,dproj,filename)`
+%
+% ### Description
+%
+% `sw_spec2MDHisto(spectra,proj,dproj,filename)` saves a
+% spectrum that is calculated by sw_egrid
+%
+% ### Input Arguments
+% spectra: a structure calculated by sw_egrid
+%
+% proj: a 3x3 matrix defining an orthogonal coordinate system
+% where each column is a vector defining the orientation
+% of the view. One of the vectors must be along the Q axis
+% defined by the direction of the calculation. It is also used to define the units along the x axis.
+%
+% dproj: is a 3 vector that is the bin size in each of the
+% directions defined in proj. For the direction of the
+% calculation, the value used is internally calcualted from the spectrum.
+% It is wise to enter the step size for clarity.
+%
+% filename: is the name of the nexus file. It will overwrite the existing
+% file if one already exists
+%
+% Example:
+% q0 = [0 0 0];
+% qmax = [2 0 0];
+% nsteps = 100;
+% spec = sw_egrid(spinwave(sw_model('triAF', 1), {q0 qmax nsteps}))
+% proj = [[1 0 0]' [0 1 0]' [0 0 1]'];
+% dproj = [1, 1e-6, 1e-6];
+% sw_spec2MDHisto(spec, proj, dproj, 'testmdh.nxs');
+% Note that:
+% (1) In the call to `spinwave`, only one q-direction may be specified
+% e.g. the HKL specifier must be of the form {q0 q0+qdir nsteps}
+% (2) one column in the `proj` matrix must be the q-direction used in
+% `spinwave` (e.g. `qdir`).
+
+
+if nargin==0
+ swhelp sw_spec2MDHisto
+ return
+end
+[unit_cell,Bmat,proj_out,D,dat,proj,name] = read_struct(spectra,proj,dproj);
+%check if hdf file exists and delete if it does.
+if exist(filename,'file')
+ delete(filename)
+end
+
+h5createnwrite(filename,'/MDHistoWorkspace/coordinate_system',3,0); % None = 0, QLab = 1, QSample = 2, HKL = 3
+h5createnwrite(filename,'/MDHistoWorkspace/visual_normalization',0,0);
+h5writeatt(filename,'/MDHistoWorkspace','NX_class','NXentry');
+h5writeatt(filename,'/MDHistoWorkspace','Qconvention','Inelastic');
+h5writeatt(filename,'/MDHistoWorkspace','SaveMDVersion', 2);
+% write data
+rtpth = NXScreategroup(filename,'/MDHistoWorkspace','data','NXdata');
+% write D dimensions
+Dszs=zeros(1,4);
+Dstrcell={};
+for idx=1:length(D)
+ szd=size(D{idx});
+ Dszs(idx)=szd(2)-1;
+ Dstrcell{idx} = strcat('D',num2str(idx-1));
+ Dpth = strcat(rtpth,'/',Dstrcell{idx});
+ h5createnwritevec(filename,rtpth,Dstrcell{idx},D{idx});
+ if idx ');
+h5createnwritevec(filename,smplpth,'num_oriented_lattice',int32(1))
+h5createnwritevec(filename,smplpth,'num_other_samples',int32(0))
+h5createnwritevec(filename,smplpth,'geom_height', 0)
+h5createnwritevec(filename,smplpth,'geom_id', int32(0))
+h5createnwritevec(filename,smplpth,'geom_thickness', 0)
+h5createnwritevec(filename,smplpth,'geom_width' ,0)
+%write material
+mtl_pth = NXScreategroup(filename,smplpth,'material','NXdata');
+h5writeatt(filename,mtl_pth,'formulaStyle','empty')
+h5writeatt(filename,mtl_pth,'name',' ')
+h5writeatt(filename,mtl_pth,'version',int32(2))
+h5createnwritevec(filename,mtl_pth,'packing_fraction',1)
+h5createnwritevec(filename,mtl_pth,'number_density',0)
+h5createnwritevec(filename,mtl_pth,'pressure',0)
+h5createnwritevec(filename,mtl_pth,'temperature',0)
+%write crystal lattice
+OL_pth = NXScreategroup(filename,smplpth,'oriented_lattice','NXcrystal');
+%write lattice parameters
+u_parm_names={'a','b','c','alpha','beta','gamma'};
+for idx=1:length(u_parm_names)
+ parm_name = strcat('unit_cell_',u_parm_names{idx});
+ h5createnwritevec(filename,OL_pth,parm_name,unit_cell(idx))
+end
+%write orientation matrix
+om_path =strcat(OL_pth,'/orientation_matrix');
+h5createnwrite(filename,om_path,Bmat,0);
+%write instrument
+instr_pth = NXScreategroup(filename,exppth,'instrument','NXinstrument');
+h5writeatt(filename,instr_pth,'version',int32(1))
+h5createnwritevec(filename,instr_pth,'name','SEQUOIA');
+end
+
+function h5createnwrite(filename,path,val,shp)
+dtyp = class(val);
+if shp ==0
+ shp = size(val);
+end
+h5create(filename,path,shp,'Datatype',dtyp);
+h5write(filename,path,val);
+end
+
+function h5createnwritevec(filename,group,ds,val)
+fh = H5F.open(filename,'H5F_ACC_RDWR','H5P_DEFAULT');
+sph = H5S.create_simple(1,length(val),length(val));
+gsh = H5G.open(fh,group);
+memtype = 'H5ML_DEFAULT';
+switch class(val)
+ case 'int32'
+ H5type = 'H5T_NATIVE_INT';
+
+ case 'int8'
+ H5type = 'H5T_NATIVE_CHAR';
+ case 'char'
+ dims= size(val);
+ sph = H5S.create_simple(1,fliplr(dims(1)),[]);
+ H5type = H5T.copy('H5T_FORTRAN_S1');
+ H5T.set_size(H5type,(dims(2)+1))
+ memtype = H5T.copy ('H5T_C_S1');
+ H5T.set_size(memtype,dims(2));
+ otherwise
+ H5type = 'H5T_NATIVE_DOUBLE';
+end
+dsh = H5D.create(gsh,ds,H5type,sph,'H5P_DEFAULT');
+H5D.write(dsh,memtype,'H5S_ALL','H5S_ALL','H5P_DEFAULT',val)
+H5S.close(sph)
+H5D.close(dsh)
+H5G.close(gsh)
+H5F.close(fh)
+end
+
+function pthout = NXScreategroup(filename,pth,group,NX_class)
+% ### Syntax
+
+% pthout = NXScreategroup(filename,pth,group,NX_class)
+
+% ### Description
+%
+% create a group with attributes of a nexus class
+%
+% ### Input Arguments
+% filename, name of hdf5 file
+% pth, path to where the group should be created
+% group the group name
+% NX_class a string containing a valid NX_class definition
+%
+% ### Output Arguments
+% returns a path to the group
+
+fh = H5F.open(filename,'H5F_ACC_RDWR','H5P_DEFAULT');
+pthh = H5G.open(fh,pth);
+gh = H5G.create(pthh,group,'H5P_DEFAULT','H5P_DEFAULT','H5P_DEFAULT');
+pthout =strcat([pth,'/',group]);
+H5G.close(gh)
+H5G.close(pthh)
+H5F.close(fh)
+h5writeatt(filename,pthout,'NX_class',NX_class)
+end
+
+function writeNXlog(filename,log_pth,log_nm,value,units)
+% ### Description
+%
+% create and write a Nexus log
+%
+% ### Input Arguments
+%filename, name of hdf5 file
+%log_pth, path to where the log should be created
+%log_nm the name of the log
+% value the vlaue of the log
+% the units if any
+pth = NXScreategroup(filename,log_pth,log_nm,'NXlog' );
+h5createnwritevec(filename,pth,'value',value)
+h5writeatt(filename,pth,'units',units)
+
+end
+function [latt_parms,Bmat,proj_out,D,signal,proj,name] = read_struct(dstruct,proj,dproj)
+
+% ### Input Arguments
+% dstruct: is the spinw structure
+% proj: is the viewing projection matrix each column is a different axis.
+% One axis must be parallel to the drection of propogation in the
+% dstruct
+% drproj: is the size of the bin in each direction (ignored for the direction
+% of the cut).
+%
+% ### Output Arguments
+% latt_parms: the lattice parameters from the spinw structure
+% Bmat: a matrix for converting from inverse angstroms to rlu
+% proj_out:
+% D: a cell array of the number of steps in each direction
+% signal: the signal array from the spinw spec strcuture
+% name : the chemical formula from the spinW file
+ check_ortho(proj)
+ fnames=fieldnames(dstruct);
+ objnum = find(strcmp(fnames,'obj'));
+ swobj = dstruct.(subsref(fnames,substruct('{}',{objnum})));
+ latt_parms = abc(swobj);
+ M = basisvector(swobj);
+ Bmat = inv(M);
+ name =formula(swobj).chemform;
+ %proj_out = proj(:);
+ hkls = dstruct.hkl;
+ hkls_sz = size(hkls);
+ % determine the direction where the hkl changes
+ dir_vec = hkls(:,hkls_sz(2))-hkls(:,1);
+ dir_vec = dir_vec/norm(dir_vec);
+ %qout = hkls'/dir_vec';
+ D={};
+ % loop through each of the projection directions
+ for qidx=1:3
+ procjv = proj(:,qidx)/norm(proj(:,qidx));
+ % if the projection direction is perpendicular to the propogation direction
+ % then set the values to +/- dproj
+ if abs(norm(cross(dir_vec,procjv)))> 1e-6
+ dtmp = dot(hkls(:,2),procjv);
+ D{qidx} = dtmp+dproj(qidx)/2.*[-1 1];
+ else
+ %assume it aslong the propogation direction
+ hkl_proj = hkls'/proj(:,qidx)';
+ dhkl = hkl_proj(2)-hkl_proj(1); % get the spacing along the q axis
+ %hkl_proj = dot(hkls(:,1),procjv);
+ D{qidx} = zeros([1,length(hkl_proj)+1]);
+ D{qidx}(1:length(hkl_proj)) = hkl_proj-dhkl/2;
+ D{qidx}(length(D{qidx})) = hkl_proj(length(hkl_proj))+dhkl/2;% change to bin boundaries
+ %proj(:,qidx) = dir_vec; %set varying projection vector to spectra object
+ end
+ end
+ D{4}=dstruct.Evect;
+ signal = dstruct.swConv;
+ proj_out = proj(:);
+end
+
+function check_ortho(mat)
+% check if the three column vectors in proj are orthogonal to each other
+sm = size(mat);
+ for idx = 1:sm(2)
+ for idx2 = 1:sm(2)
+ if idx~=idx2
+ if norm(dot(mat(:,idx),mat(:,idx2))) >1e-6
+ error("read_struct:nonorthogonal","the 3 vectors in proj must form an orthogonal basis set")
+ end
+ end
+ end
+ end
+end
diff --git a/swfiles/sw_timeit.m b/swfiles/sw_timeit.m
index 8188b7b50..736f3f0e1 100644
--- a/swfiles/sw_timeit.m
+++ b/swfiles/sw_timeit.m
@@ -7,7 +7,7 @@ function sw_timeit(percent,varargin)
%
% ### Description
%
-% `sw_timeit(percent, {mode},{fid},{title})` can display remaining time of
+% `sw_timeit(percent, {mode},{tid},{title})` can display remaining time of
% a calculation that is run for a fixed number of iterations. It can output
% the status both in the Command Window and in a pup up window using
% [waitbar].
@@ -40,20 +40,28 @@ function sw_timeit(percent,varargin)
%
global sw_time
-pref = swpref;
+persistent pref;
+if isempty(pref)
+ pref = swpref;
+end
if nargin == 0
swhelp sw_timeit
return
end
+if pref.fid == 0
+ % Users wants to suppress output
+ return
+end
+
if nargin > 2 && ~isempty(varargin{2}) && ~ischar(varargin{2})
- fid = varargin{2};
+ tid = varargin{2};
else
- fid = pref.tid;
+ tid = pref.tid;
end
-if fid == 0
+if tid == 0
% do nothing
return
end
@@ -64,21 +72,21 @@ function sw_timeit(percent,varargin)
title0 = 'sw_timeit';
end
-if ~ismember(fid,[1 2])
+if ~ismember(tid,[1 2])
return
end
if nargin > 1
- start = varargin{1};
+ mode = varargin{1};
else
- start = 0;
+ mode = 0;
end
-switch start
+switch mode
case 1
- % start the time estimation
+ % mode the time estimation
sw_time = tic;
- switch fid
+ switch tid
case 1
fprintf([repmat(' ',[1 40]) '\n']);
case 2
@@ -99,7 +107,7 @@ function sw_timeit(percent,varargin)
rtime = rtime-hou*60^2;
min = floor(rtime/60);
sec = floor(rtime - min*60);
- switch fid
+ switch tid
case 1
fprintf([repmat('\b',[1 41]) '%6.2f%%, remained: %03d:%02d:%02d (HH:MM:SS).\n'],...
percent,hou,min,sec);
@@ -121,7 +129,7 @@ function sw_timeit(percent,varargin)
sec = floor(etime);
%tho = floor((etime-sec)*1000);
%fprintf('Finished in %02d:%02d:%02d.%03d (HH:MM:SS.FFF).\n',hou,min,sec,tho);
- switch fid
+ switch tid
case 1
fprintf(repmat('\b',1,40+1));
fprintf('Calculation is finished in %02d:%02d:%02d (hh:mm:ss).\n',hou,min,sec);
@@ -151,4 +159,4 @@ function extended_waitbar()
'Position', pauseBtnPos,...
'Callback', 'dbstop in sw_timeit.m at 23');
-end
\ No newline at end of file
+end
diff --git a/swfiles/sw_tofres.m b/swfiles/sw_tofres.m
index 535fdbe37..c37e8f58e 100644
--- a/swfiles/sw_tofres.m
+++ b/swfiles/sw_tofres.m
@@ -76,6 +76,10 @@
return
end
+if sw_issymspec(spec)
+ error('sw_tofres:SymbolicInput', 'This function does not handle symbolic spectra');
+end
+
dQ0 = ones(1,3)*0.1;
nQ0 = ones(1,3)*5;
@@ -172,4 +176,4 @@
spec.swConv = conv0/prod(nQ);
fprintf0(fid,'Calculation finished.\n')
-end
\ No newline at end of file
+end
diff --git a/swfiles/sw_uniquetol.m b/swfiles/sw_uniquetol.m
index c7ff02b58..d132621ed 100644
--- a/swfiles/sw_uniquetol.m
+++ b/swfiles/sw_uniquetol.m
@@ -58,7 +58,6 @@
M(:,idxSame) = [];
idx = idx + 1;
end
-
else
idx = 1;
% storing the indices in M
diff --git a/swfiles/sw_update.m b/swfiles/sw_update.m
index d1a59c7ad..1874e359b 100644
--- a/swfiles/sw_update.m
+++ b/swfiles/sw_update.m
@@ -1,42 +1,55 @@
-function onlineRev = sw_update(installDir)
+function onlineRev = sw_update(installDir,varargin)
% updates the SpinW installation from the internet
-%
+%
% ### Syntax
-%
+%
% `sw_update`
-%
+%
% `onlineRev = sw_update(installDir)`
%
% ### Description
-%
+%
% `sw_update` creates a new folder with the latest release beside the
% current SpinW installation, downloads the newses SpinW version and adds
% the new version to the Matlab search path and also removes the old
% version from the search path. If the search path is defined in the
% `startup.m` file, it has to be changed manually.
-%
+%
% Each step of the update process can be controlled by the user via
% the interactive Command Line provided by the function.
%
% ### Input Arguments
-%
+%
% `installDir`
% : Folder name, where the new version is installed. Default is
% the parent folder of the current version of SpinW. If
% `installDir` is `'.'`, the update will be installed to current
% folder.
-%
+%
+% `'beta'`
+% : Retrieve the latest pre-release if it is newer than the latest release.
+% If it is older, download the latest release.
+%
% ### Output Arguments
-%
+%
% `onlineVer` If output is defined, the revision number of the online
% SpinW is given, optional.
%
+
+inpForm.fname = {'beta'};
+inpForm.defval = {false};
+inpForm.size = {[1 -1]};
+inpForm.soft = {false};
+
+param = sw_readparam(inpForm, varargin{:});
+
+
% check current version
swVer = sw_version;
% base url, where the sw_download_info file stored
-baseUrl = 'https://docs.google.com/uc?export=download&id=0BzFs7CQXhehSRXpjT0dndDNxNUE';
+baseUrl = 'https://api.github.com/repos/SpinW/spinw/releases';
if nargout == 0
if ~isempty(swVer.Version)
@@ -70,18 +83,34 @@
% release number
% message in the next few lines
try
- newInfo = urlread(baseUrl);
+ newInfo = webread(baseUrl);
+ % Remove pySpinW releases
+ idx = cellfun(@(x) ~any(strfind(x, 'py')), {newInfo.tag_name});
+ newInfo = newInfo(idx);
+ % Remove any beta releases
+ idx = ~[newInfo.prerelease];
+ if ~param.beta
+ newInfo = newInfo(idx);
+ else
+ [~, idx] = max(...
+ arrayfun(...
+ @(x) datenum(strrep(strrep(x.created_at, 'T', ' '), 'Z', ' '),'dd-mm-yyyy HH:MM:SS'),...
+ newInfo));
+ newInfo = newInfo(idx);
+ if newInfo.prerelease
+ warning('sw_update:UseBeta',...
+ 'You are about to install beta software.\nReport any errors to: %s', strtok(swVer.Contact, ','))
+ end
+ end
+ % The latest release is the first
+ newInfo = newInfo(1);
catch
- error('sw_update:NoNetwork','It looks like there is a problem with your network connection!');
+ error('sw_update:NoNetwork', 'It looks like there is a problem with your network connection!');
end
-newLine = sprintf('\n'); %#ok
-% separate lines of text
-newInfo = textscan(newInfo, '%s', 'delimiter',newLine);
-newInfo = newInfo{1};
-
-newLink = newInfo{1};
-newRev = str2double(newInfo{2});
+newLink = newInfo.assets.browser_download_url;
+newRev = strsplit(newInfo.assets.browser_download_url, '_');
+newRev = str2double(newRev{2}(2:end-4));
if nargout == 1
% Give the release number and exit.
@@ -89,15 +118,20 @@
return
end
-if numel(newInfo)>2
- newMsg = newInfo(3:end);
-else
- newMsg = {};
-end
+newMsg = newInfo.body;
% check whether the online version is newer (compare release numbers)
if ischar(swVer.Release)
swVer.Release = str2double(swVer.Release);
+ if isnan(swVer.Release)
+ % User has installed by the MATLAB add-ons
+ answer = getinput('This SpinW was installed by MATLAB addons.\nYou can remove and install it from the menu.\nDo you want to retrieve the latest version from Github? (y/n)','yn');
+ if answer == 'y'
+ swVer.Release = -1;
+ else
+ error('sw_update:userCanceled','Operation terminated by user.')
+ end
+ end
end
if swVer.Release == newRev
@@ -108,48 +142,46 @@
return
end
-fprintf('Current version has a release number: %d\n',swVer.Release);
-fprintf('New version has a release number: %d\n',newRev);
+fprintf('Current version has a release number: %d\n', swVer.Release);
+fprintf('New version has a release number: %d\n', newRev);
-answer = getinput('Do you want to continue? (y/n)','yn');
+answer = getinput('Do you want to continue? (y/n)', 'yn');
if answer == 'n'
- disp('SpinW update process cancelled!');
- return
+ error('sw_update:userCanceled', 'Operation terminated by user.')
end
-fprintf('New version will be installed to: %s\n',installDir);
-answer = getinput('Do you want to continue? (y/n)','yn');
+fprintf('New version will be installed to: %s\n', installDir);
+answer = getinput('Do you want to continue? (y/n)', 'yn');
if answer(1) == 'n'
- disp('SpinW update process cancelled!');
- return
+ error('sw_update:userCanceled', 'Operation terminated by user.')
end
% save new update as a zip file
updateName = 'spinw_update_files.zip';
-fprintf('Downloading update from %s... ',newLink);
+fprintf('Downloading update from %s... ', newLink);
urlwrite(newLink,[installDir updateName]);
fprintf('ready!\n');
% decompress zip file
-zipList = unzip([installDir updateName],installDir);
+zipList = unzip([installDir updateName], installDir);
% get folder name
-folName = [installDir strtok(zipList{1}(numel(installDir)+1:end),filesep)];
+folName = [installDir strtok(zipList{1}(numel(installDir)+1:end), filesep)];
% remove old SpinW installation from path
fprintf('\nRemoving path to old SpinW installation!\n')
rmpath(genpath(sw_rootdir));
% adding new path
-fprintf('Adding path to new SpinW installation: %s!\n',folName);
+fprintf('Adding path to new SpinW installation: %s!\n', folName);
ww = warning;
warning('off');
addpath(genpath(folName));
warning(ww);
-answer = getinput('Do you want to save the new path (savepath)? (y/n)','yn');
+answer = getinput('Do you want to save the new path (savepath)? (y/n)', 'yn');
if answer == 'y'
savepath
@@ -175,7 +207,7 @@
% end
% % gobjects
% fList = dir([folName filesep 'external' filesep 'gobjects*']);
-% for ii = 1:numel(fList)
+% for ii = 1:numel
% delete([folName filesep 'external' filesep fList(ii).name]);
% end
% else
@@ -196,7 +228,7 @@
% [folName filesep 'external' filesep 'gobjects.m']);
% end
% end
-%
+%
% % functions introduced in R2015a
% % if ~verLessThan('matlab', '8.5')
% % % uniquetol()
@@ -205,17 +237,17 @@
% % delete([folName filesep 'external' filesep fList(ii).name]);
% % end
% % end
-%
+%
% fprintf(['In order to reach SpinW after restarting Matlab, the following\n'...
% 'line has to be added to your startup.m file:\n']);
% fprintf(' addpath(genpath(''%s''));\n',folName);
-%
+%
% % location of Matlab startup file
% sfLoc = which('startup');
% uPath = userpath;
% % remove ':' and ';' characters from the userpath
% uPath = [uPath(~ismember(uPath,':;')) filesep 'startup.m'];
-%
+%
% % create new startup.m file
% if isempty(sfLoc)
% answer = getinput(sprintf(['You don''t have a Matlab startup.m file,\n'...
@@ -225,13 +257,13 @@
% sfLoc = uPath;
% end
% end
-%
+%
% if ~isempty(sfLoc)
-%
+%
% answer = getinput(sprintf(['Would you like to add the following line:\n'...
% sprintf('addpath(genpath(''%s''));',folName) '\nto the end of '...
% 'your Matlab startup file (%s)? (y/n)'],sfLoc),'yn');
-%
+%
% if answer == 'y'
% fid = fopen(sfLoc,'a');
% fprintf(fid,['\n%%###SW_UPDATE\n%% Path to the SpinW (rev. %d) '...
@@ -246,7 +278,7 @@
disp('Release information:')
disp(repmat('-',[1 60]))
for ii = 1:numel(newMsg)
- fprintf('\t%s\n',newMsg{ii});
+ fprintf('%s', newMsg);
end
disp(repmat('-',[1 60]))
end
@@ -259,7 +291,7 @@
% 'in the Matlab internal memory. Would you like the updater to issue\n'...
% 'the command now, otherwise you can do it manually later.\n'...
% 'Do you want to issue the command "clear classes" now? (y/n)'],'yn');
-%
+%
% if answer == 'y'
% clear('classes'); %#ok
% disp('Matlab class memory is refreshed!')
@@ -267,7 +299,7 @@
disp('Update was successful!')
-answer = getinput('Do you want to run the install_spinw command from the update? (y/n)','yn');
+answer = getinput('Do you want to run the install_spinw command from the update? (y/n)', 'yn');
switch answer
case 'n'
@@ -282,12 +314,11 @@
% get the necessary letter input
answer = ' ';
-while ~ismember(answer(1),good)
- answer = input(message,'s');
+while ~ismember(answer(1), good)
+ answer = input(message, 's');
if isempty(answer)
answer = 0;
end
end
answer = answer(1);
-
end
\ No newline at end of file
diff --git a/swfiles/sw_version.m b/swfiles/sw_version.m
index f607f3724..af11783b6 100644
--- a/swfiles/sw_version.m
+++ b/swfiles/sw_version.m
@@ -18,6 +18,12 @@
% release date and license.
%
+% Take into account deployed installs
+if isdeployed
+ outStr = struct;
+ return
+end
+
% read file header from sw_version.m file
fid = fopen('sw_version.m');
@@ -48,7 +54,6 @@
[~, verSel] = strtok(verSel,'$'); %#ok<*STTOK>
[partStr{end+1}, verSel] = strtok(verSel,'$');
end
-
end
nField = numel(partStr);
@@ -76,13 +81,10 @@
end
% Matlab version & Symbolic Toolbox
-v0 = ver;
-nSym = strcmp('Symbolic Math Toolbox', {v0.Name});
-nSym = find(nSym,1);
-if isempty(nSym)
+if ~license('checkout','Symbolic_Toolbox')
strSym = 'no Symbolic Math Toolbox installed';
else
- strSym = [v0(nSym).Name ' installed'];
+ strSym = 'Symbolic Math Toolbox installed';
end
@@ -112,7 +114,7 @@
ver0.Release = '';
ver0.Date = datestr(now,'dd-mmm-yyyy');
ver0.Author = 'S. Tóth and S. Ward';
- ver0.Contact = 'spinw4@gmail.com, @spinw4 on Twitter';
+ ver0.Contact = 'admin@spinw.org, @spinw4 on Twitter';
ver0.License = 'GNU GENERAL PUBLIC LICENSE';
if nField == 0
diff --git a/test/fourier_test.m b/test/fourier_test.m
deleted file mode 100644
index 9c399daa0..000000000
--- a/test/fourier_test.m
+++ /dev/null
@@ -1,21 +0,0 @@
-%% kagome lattice
-
-kag = spinw;
-kag.genlattice('lat_const',[6 6 4],'angled',[90 90 120])
-kag.addatom('r',[1/2 0 0],'S',1)
-kag.addatom('r',[1/2 1/2 0],'S',1)
-kag.addatom('r',[0 1/2 0],'S',1)
-
-%kag.newcell('bvect',{[2 0 0] [0 1 0] [0 0 1]})
-kag.quickham(1)
-%plot(kag)
-
-%% calculate fourier transformation
-
-for ii = 1:10:100
- Q = reshape(sw_qgrid('bin',{[0 0.01 2] [0 0.01 2] [1 ii]}),3,[]);
- tic;
- F2 = kag.fourier(Q,'isomode','auto');
- t1(ii) = toc;
-end
-
diff --git a/test/scga_test.m b/test/scga_test.m
deleted file mode 100644
index c7f690612..000000000
--- a/test/scga_test.m
+++ /dev/null
@@ -1,460 +0,0 @@
-%% define the pyrochlore lattice
-
-pyro = spinw;
-pyro.genlattice('lat_const',3*sqrt(8)*[1 1 1])
-
-% basis vectors of the FCC lattice
-a = [0 0 0;0 1/2 1/2;1/2 0 1/2;1/2 1/2 0];
-
-
-subIdx = repmat(1:4,1,4);
-% colors of the sublattices
-color = {'red' 'green' 'orange' 'yellow'};
-for ii = 1:4
- for jj = 1:4
- pyro.addatom('r',a(ii,:)+a(jj,:)/2,'S',1,'color',color{ii})
-
- end
-end
-
-pyro.quickham(1)
-
-plot(pyro)
-
-
-%% test fourier transformed Hamiltonian
-
-Q = [1/2 1/3 1/4];
-cxy = cos((Q(1)+Q(2))/4*2*pi);
-cxz = cos((Q(1)+Q(3))/4*2*pi);
-cyz = cos((Q(2)+Q(3))/4*2*pi);
-
-cxym = cos((Q(1)-Q(2))/4*2*pi);
-cxzm = cos((Q(1)-Q(3))/4*2*pi);
-cyzm = cos((Q(2)-Q(3))/4*2*pi);
-
-FT2 = [1 cyz cxz cxy;cyz 1 cxym cxzm;cxz cxym 1 cyzm;cxy cxzm cyzm 1];
-
-F = pyro.fourier(Q','sublat',repmat(1:4,1,4))
-FT = squeeze(F.ft(1,1,:,:));
-
-%% calculate lambda
-
-pyro.setunit('mode','1')
-T = 10.^(linspace(-2,2,51));
-spec4 = pyro.scga([],'T',T,'sublat',repmat(1:4,1,4),'plot',false,'nInt',1e3,'chi',true,'iso',false);
-
-clf
-semilogx(T,spec4.lambda)
-axis([1e-2 1e2 1 3.5])
-
-%% Calculate diffuse scattering
-
-Q1 = sw_qgrid('u',[1 0 0],'v',[0 1 0],'bin',{[0 0.01 4] [-1 0.01 4]});
-Q2 = sw_qgrid('u',[1 1 0],'v',[0 0 1],'bin',{[0 0.01 4] [-1 0.01 4]});
-
-spec1 = pyro.scga(Q1,'T',1/200,'sublat',repmat(1:4,1,4));
-spec2 = pyro.scga(Q2,'T',1/200,'sublat',repmat(1:4,1,4));
-
-%% make figure comparable to the published figure
-
-clf
-subplot(1,2,1)
-hAxis(1) = gca;
-hSurf = surf(squeeze(Q1(1,:,:)),squeeze(Q1(2,:,:)),squeeze(spec1.Sab(1,1,1,:,:))*4);
-hSurf.EdgeAlpha = 0;
-hold on
-contour3(squeeze(Q1(1,:,:)),squeeze(Q1(2,:,:)),squeeze(spec1.Sab(1,1,1,:,:))*4,0.5:0.5:2.5,'color','k');
-view(2)
-%axis([0 3 -1 4])
-box on
-colormap(cm_viridis)
-caxis([0 2.7])
-hCol = colorbar('northoutside');
-xlabel('($h$,0,0)','interpreter','latex')
-ylabel('(0,0,$l$)','interpreter','latex')
-text(0.1,0.94,100,'(a)','fontsize',22,'units','normalized','HorizontalAlignment','center','color','w')
-axis([0 3 -1 4])
-
-subplot(1,2,2)
-hAxis(2) = gca;
-hSurf = surf(squeeze(Q2(1,:,:)),squeeze(Q2(3,:,:)),squeeze(spec2.Sab(1,1,1,:,:))*4);
-hSurf.EdgeAlpha = 0;
-hold on
-contour3(squeeze(Q2(1,:,:)),squeeze(Q2(3,:,:)),squeeze(spec2.Sab(1,1,1,:,:))*4,0.5:0.5:2.5,'color','k');
-view(2)
-%axis([0 3 -1 4])
-box on
-colormap(cm_viridis)
-caxis([0 2.7])
-hAxis(2).YAxis.TickLabels = '';
-xlabel('($h$,$h$,0)','interpreter','latex')
-text(0.1,0.94,100,'(b)','fontsize',22,'units','normalized','HorizontalAlignment','center','color','w')
-axis([0 3 -1 4])
-
-hCol.Position = [0.10 0.90 0.87 0.04];
-hAxis(1).Position = [0.10 0.12 0.42 0.75];
-hAxis(2).Position = [0.55 0.12 0.42 0.75];
-drawnow;
-fPos = get(gcf,'Position');
-set(gcf,'Position',[fPos(1:2) 584 431])
-
-%% do the different cuts
-
-Q3 = sw_qgrid('u',[1 0 0],'v',[0 0 1],'bin',{[0 0.01 4] [0 0.2 2]});
-spec3 = pyro.scga(Q3,'T',1/20,'sublat',repmat(1:4,1,4),'plot',false);
-spec3.lambda
-
-figure
-for ii = 2:(size(spec3.Sab,5)-1)
- plot(squeeze(Q3(1,:,1,1)),squeeze(spec3.Sab(1,1,1,:,ii))*4)
- hold on
-end
-
-%% calculate susceptibility
-
-T = linspace(1e-2,5,51);
-spec4 = pyro.scga([],'T',T,'sublat',repmat(1:4,1,4),'plot',false,'nInt',1e3,'chi',true,'iso',false);
-
-figure;
-subplot(2,1,1)
-plot(T,spec4.lambda)
-subplot(2,1,2)
-plot(T,spec4.chi)
-
-
-
-%% Fourier transform of the interaction matrix
-
-nMag = numel(pyro.matom.idx);
-idx1 = repmat(subIdx',1,nMag);
-idx2 = repmat(subIdx,nMag,1);
-
-Q = [1/3 1/4 1/5];
-FT = pyro.fourier(Q');
-% sum up the FT per sublattice
-
-FT = reshape(FT.ft,9,[]);
-
-subs = [idx1(:) idx2(:)];
-
-FT2 = zeros(4,4,9);
-
-for ii = 1:9
- FT2(:,:,ii) = accumarray(subs,FT(ii,:));
-end
-
-FT2 = sum(FT2,3)/12+eye(4);
-
-cxy = cos((Q(1)+Q(2))/4*2*pi);
-cxz = cos((Q(1)+Q(3))/4*2*pi);
-cyz = cos((Q(2)+Q(3))/4*2*pi);
-
-cxym = cos((Q(1)-Q(2))/4*2*pi);
-cxzm = cos((Q(1)-Q(3))/4*2*pi);
-cyzm = cos((Q(2)-Q(3))/4*2*pi);
-
-FT3 = [1 cyz cxz cxy;cyz 1 cxym cxzm;cxz cxym 1 cyzm;cxy cxzm cyzm 1];
-
-FT2-FT3
-
-%% high temperature solution
-
-kbT = sw_converter(1,'K','meV');
-beta = 1/kbT;
-
-lambda = 3;
-
-%% solution of self-consistent equation
-
-nQ0 = 1e4;
-D = 3;
-% FT
-N = round(nQ0^(1/D));
-nQ = N^D;
-BZ = sw_qgrid('mat',eye(3),'bin',repmat({linspace(0,1,N)},1,D));
-
-FT = pyro.fourier(reshape(BZ,3,[]));
-FT = FT.ft;
-% include the spin value into the Fourier transform of the Js
-% thus convert the model into interacting S=1 spins
-FT = bsxfun(@times,FT,permute(bsxfun(@times,pyro.matom.S',pyro.matom.S),[3 4 1 2]));
-
-
-%%
-subIdx = 1:16;
-%subIdx = repmat(1:4,1,4);
-% reduce the lattice into sublattices
-nMag = numel(pyro.matom.idx);
-idx1 = repmat(subIdx',1,nMag);
-idx2 = repmat(subIdx,nMag,1);
-subs = [idx1(:) idx2(:)];
-
-nSub = max(subIdx);
-
-FT = reshape(FT,3,3,[],nQ);
-FT2 = zeros(3,3,nSub,nSub,nQ);
-
-for ii = 1:nSub
- for jj = 1:nSub
- FT2(:,:,ii,jj,:) = permute(sum(FT(:,:,ismember(subs,[ii jj],'rows'),:),3),[1 2 3 5 4])*nSub/nMag;
- if ii == jj
- FT2(:,:,ii,ii,:) = FT2(:,:,ii,ii,:) + 1;
- end
- end
-end
-
-[E,D] = eigorth(squeeze(FT2(1,1,:,:,:)));
-
-%% plot the fitted curve
-
-beta = 200;
-lv = linspace(1,4,101);
-for ii = 1:numel(lv)
- lambda = lv(ii);
- J = 1;
- Di(ii) = abs(sumn(1./(lambda+beta*J*D),[1 2])/nQ/4-1/3);
- Di2(ii) = sumn(1./(lambda+beta*J*D),[1 2])/nQ/4;
-end
-fminsearch(@(lambda)abs(sumn(1./(lambda+beta*J*D/4),[1 2])/nQ/4-1/3),3)
-figure
-plot(lv,Di2)
-hold on
-plot(lv,lv*0+1/3)
-
-%% find the values of lambda
-
-Jv = 10.^linspace(-2,2,51);
-beta = 1;
-
-for ii = 1:numel(Jv)
- J = Jv(ii);
- li(ii) = fminsearch(@(lambda)abs(sumn(1./(lambda+beta*J*D),[1 2])/nQ/4-1/3),3);
-end
-
-figure;
-semilogx(1./Jv,li,'o-')
-xlabel('T/J_1')
-ylabel('\lambda')
-axis([1e-2 1e2 1 3.5])
-
-%% calculate the spin correlations
-
-%subIdx = 1:16;
-subIdx = repmat(1:4,1,4);
-
-beta = 1;
-J = 20;
-
-lambda = fminsearch(@(lambda)abs(sumn(1./(lambda+beta*J*D),[1 2])/size(D,2)/4-1/3),3);
-
-Q = sw_qgrid('bin',{[0 0.02 4] 0 [-1 0.02 4]});
-%Q = sw_qgrid('u',[1 1 0],'v',[0 0 1],'bin',{[0 0.02 3] [-1 0.02 4]});
-nQ = numel(Q)/3;
-
-FT = pyro.fourier(reshape(Q,3,[]));
-FT = FT.ft;
-% include the spin value into the Fourier transform of the Js
-% thus convert the model into interacting S=1 spins
-FT = bsxfun(@times,FT,permute(bsxfun(@times,pyro.matom.S',pyro.matom.S),[3 4 1 2]));
-
-% reduce the lattice into sublattices
-nMag = numel(pyro.matom.idx);
-idx1 = repmat(subIdx',1,nMag);
-idx2 = repmat(subIdx,nMag,1);
-subs = [idx1(:) idx2(:)];
-
-nSub = max(subIdx);
-
-FT = reshape(FT,3,3,[],nQ);
-FT2 = zeros(3,3,nSub,nSub,nQ);
-
-for ii = 1:nSub
- for jj = 1:nSub
- FT2(:,:,ii,jj,:) = permute(sum(FT(:,:,ismember(subs,[ii jj],'rows'),:),3),[1 2 3 5 4])*nSub/nMag;
- if ii == jj
- FT2(:,:,ii,ii,:) = FT2(:,:,ii,ii,:) + 1;
- end
- end
-end
-V = FT2;
-
-V = bsxfun(@plus,permute(lambda*eye(nSub),[3 4 1 2]),beta*J*V);
-
-Sab = zeros(1,nQ);
-for ii = 1:nQ
- Sab(ii) = sumn(inv(squeeze(V(1,1,:,:,ii))),[1 2]);
-end
-
-dQ = num2cell(size(Q));
-% Correlations per site
-Sab = reshape(Sab,dQ{2:end})/nSub;
-
-%% plot correlations
-clf
-hSurf = surf(squeeze(Q(1,:,:)),squeeze(Q(3,:,:)),squeeze(Sab)*4);
-hold on
-contour3(squeeze(Q(1,:,:)),squeeze(Q(3,:,:)),squeeze(Sab)*4,0.5:0.5:2.5,'color','k');
-hSurf.EdgeAlpha = 0;
-view(2)
-%axis([0 3 -1 4])
-box on
-colormap(cm_viridis)
-caxis([0 2.7])
-colorbar
-
-%% plot cuts
-
-figure
-nCut = 5;
-idxv = round(linspace(1,251,nCut));
-for ii = 1:nCut
- plot(squeeze(Q(1,:,1,1)),Sab(:,1,idxv(ii))*4)
- hold on
-end
-
-%% test the new SCGA code
-
-pyro.setunit('mode','1')
-
-%Q = sw_qgrid('bin',{[0 0.02 4] 0 [-1 0.02 4]});
-Q = sw_qgrid('u',[1 1 0],'v',[0 0 1],'bin',{[0 0.02 3] [-1 0.02 4]});
-
-betaJ = 100;
-%betaJ = 10.^linspace(-5,5,31);
-subLat = repmat(1:4,1,4);
-%subLat = 1:16;
-tic
-spec2 = pyro.scga2(Q,'T',1/2./betaJ,'plot',true,'nInt',1e4,'subLat',subLat,'lambda',[],'isomode','off');
-toc
-ylim([0 5])
-
-%% plot lambda values
-
-figure;
-semilogx(spec.T*2,spec.lambda,'o-')
-%xlim([1e-2 1e2])
-
-%% plot correlations
-
-spec = spec2;
-Q = spec.hkl;
-Sab = squeeze(spec.Sab(1,1,:,:)*4);
-
-figure
-hSurf = surf(squeeze(Q(1,:,:)),squeeze(Q(3,:,:)),Sab);
-hold on
-contour3(squeeze(Q(1,:,:)),squeeze(Q(3,:,:)),Sab,0.5:0.5:2.5,'color','k');
-hSurf.EdgeAlpha = 0;
-view(2)
-%axis([0 3 -1 4])
-box on
-colormap(cm_viridis)
-caxis([0 2.7])
-colorbar
-
-%% test sublattice problem
-
-%subLat = 1:16;
-subLat = [];
-%subLat = repmat(1:4,1,4);
-
-beta = 200;
-
-nMag = numel(pyro.matom.idx);
-idx1 = repmat(subLat',1,nMag);
-idx2 = repmat(subLat,nMag,1);
-subs = [idx1(:) idx2(:)];
-
-if ~isempty(subLat)
- nSub = max(subLat);
-else
- nSub = nMag;
-end
-
-BZ = sw_qgrid('bin',{[0 0.1 2] [0 0.1 2] 0.3});
-sQBZ = num2cell(size(BZ));
-nQBZ = numel(BZ)/3;
-chi0 = pyro.fourier(reshape(BZ,3,[]),'extend',false);
-% include the spin value into the Fourier transform of the Js
-% thus convert the model into interacting S=1 spins
-%FT = bsxfun(@times,chi0.ft,permute(bsxfun(@times,S',S),[3 4 1 2]));
-FT = chi0.ft;
-
-% reduce the lattice into sublattices
-if ~isempty(subLat)
- FT = reshape(FT,3,3,[],nQBZ);
- FT2 = zeros(3,3,nSub,nSub,nQBZ);
-
- for ii = 1:nSub
- for jj = 1:nSub
- FT2(:,:,ii,jj,:) = permute(sum(FT(:,:,ismember(subs,[ii jj],'rows'),:),3),[1 2 3 5 4])*nSub/nMag;
- if ii == jj
- FT2(:,:,ii,ii,:) = FT2(:,:,ii,ii,:) + 1;
- end
- end
- end
- FT = FT2;
-else
- for ii = 1:nSub
- FT(:,:,ii,ii,:) = FT(:,:,ii,ii,:) + 1;
- end
-end
-% find the eigenvalues over the BZ
-FT = squeeze(FT(1,1,:,:,:));
-omega = zeros(nSub,nQBZ);
-for ii = 1:nQBZ
- omega(:,ii) = eig(FT(:,:,ii));
-end
-
-% find the optimum value of lambda
-lambda = fminsearch(@(lambda)abs(sumn(1./(lambda+beta*omega),[1 2])/nQBZ/nSub-1/3),3)
-
-% plot omega
-omega = reshape(omega',sQBZ{2:end},nSub);
-
-%% simple eig
-
-BZ = sw_qgrid('bin',{[1 0.1 3] [1 0.1 3] 0.25});
-sQBZ = num2cell(size(BZ));
-nQBZ = numel(BZ)/3;
-chi0 = pyro.fourier(reshape(BZ,3,[]),'extend',false);
-% include the spin value into the Fourier transform of the Js
-% thus convert the model into interacting S=1 spins
-%FT = bsxfun(@times,chi0.ft,permute(bsxfun(@times,S',S),[3 4 1 2]));
-FT = chi0.ft;
-FT = squeeze(FT(1,1,:,:,:));
-
-omega = zeros(nSub,nQBZ);
-for ii = 1:nQBZ
- omega(:,ii) = eig(FT(:,:,ii));
-end
-% plot omega
-omega = reshape(omega',sQBZ{2:end},nSub);
-
-%%
-clf
-for ii = 1:nSub
- surf(squeeze(BZ(1,:,:)),squeeze(BZ(2,:,:)),omega(:,:,ii))
- hold on
-end
-%axis([0 2 0 2 -4 4])
-ax=axis;
-axis([ax(1:4) -4 4])
-view(az,el)
-
-%% test the new SCGA code
-
-ybti = spinw('~/Documents/structures/Yb2Ti2O7/Yb2Ti2O7_cryst.cif');
-
-ybti.setunit('mode','1')
-ybti.quickham(1);
-%Q = sw_qgrid('bin',{[0 0.02 4] 0 [-1 0.02 4]});
-Q = sw_qgrid('u',[1 1 0],'v',[0 0 1],'bin',{[0 0.02 3] [-1 0.02 4]});
-
-betaJ = 100;
-%betaJ = 10.^linspace(-2,2,31);
-%subLat = repmat(1:4,1,4);
-subLat = 1:16;
-spec = ybti.scga(Q,'T',1/2./betaJ,'plot',true,'nInt',1e4,'subLat',subLat,'lambda',[]);
-ylim([0 5])
diff --git a/test/scga_test_kag.m b/test/scga_test_kag.m
deleted file mode 100644
index 131b37d72..000000000
--- a/test/scga_test_kag.m
+++ /dev/null
@@ -1,63 +0,0 @@
-%% kagome lattice
-
-kag = spinw;
-kag.genlattice('lat_const',[6 6 4],'angled',[90 90 120])
-kag.addatom('r',[1/2 0 0],'S',1)
-kag.addatom('r',[1/2 1/2 0],'S',1)
-kag.addatom('r',[0 1/2 0],'S',1)
-
-%kag.newcell('bvect',{[2 0 0] [0 1 0] [0 0 1]})
-kag.quickham(1)
-%plot(kag)
-
-%% eig
-
-Q = sw_qgrid('bin',{[0 0.05 2] [0 0.05 2]});
-
-chi = kag.fourier(reshape(Q,3,[]));
-
-ft = squeeze(chi.ft(1,1,:,:,:));
-
-clear om
-for ii = 1:size(ft,3)
- om(:,ii) = eig(ft(:,:,ii));
-end
-
-nMode = size(om,1);
-sQ = num2cell(size(Q));
-om = reshape(om',sQ{2:end},nMode);
-
-clf
-for ii = 1:nMode
- surf(squeeze(Q(1,:,:)),squeeze(Q(2,:,:)),om(:,:,ii));
- hold on
-end
-%% scga method determine lambda
-
-kag.setunit('mode','1')
-T = 10.^linspace(-2,2,41);
-Q = sw_qgrid('bin',{[0 0.05 2] [0 0.05 2]});
-spec = kag.scga(Q,'T',T,'plot',true,'nInt',1e4);
-
-figure
-semilogx(spec.T,spec.lambda,'o-')
-
-%% scga method diffuse scattering
-
-kag.setunit('mode','1')
-T = 1;
-Q = sw_qgrid('bin',{[0 0.05 4] [0 0.05 4]});
-spec = kag.scga2(Q,'T',T,'plot',true,'nInt',1e4);
-
-
-%%
-figure
-hSurf = surf(squeeze(Q(1,:,:,:)),squeeze(Q(2,:,:,:)),spec.Sab);
-hSurf.EdgeAlpha = 0;
-view(2)
-hold on
-contour3(squeeze(Q(1,:,:,:)),squeeze(Q(2,:,:,:)),spec.Sab,0:0.05:2,'color','k');
-
-
-
-
diff --git a/test/sw_genatpos2.m b/test/sw_genatpos2.m
deleted file mode 100644
index 920f2146b..000000000
--- a/test/sw_genatpos2.m
+++ /dev/null
@@ -1,71 +0,0 @@
-function [rSym, symName] = sw_genatpos2(sym, r, fid, tol)
-% [rSym, symName] = SW_GENATPOS(sym, r, {fid}, {tol}) generates all symmetry
-% equivalent atomic positions from a given symmetry number and coordinates
-% of the input atoms. If print is defined, the result is printed onto
-% the command window.
-%
-% Input:
-%
-% sym Line index in the symmetry.dat file or string of the
-% symmetry operators.
-% r Atomic position in lattice units, dimensions are [3 nAtom].
-% {fid} Optional input, the file identifier to print the result.
-% To print onto the Command Window, use fid = 1; default is
-% fid = 0, no print.
-%
-% Output:
-%
-% rSym Symmetry equivalent atomic positions, dimensions are
-% [3 nGenAtom] if the input was a single atom. If more atoms
-% are defined, the output is packaged into a cell, with
-% dimensions of [1 nAtom]. Every element of the cell contains
-% a matrix with the symmetry equivalent positions.
-% symName String, the name of the space group.
-% tol Tolerance, for the atomic overlap, default is 1e-5.
-%
-% See also SW, SW.ATOM, SW.MATOM, SW_GENCOUPLING, SW_GENCOORD, SW_GENSYM.
-%
-
-if nargin == 0
- swhelp sw_genatpos;
- return
-end
-
-if nargin == 2
- fid = 0;
-end
-
-if nargin < 4
- tol = 1e-5;
-end
-
-[symOp, symTr, symName] = sw_gencoord(sym, fid);
-
-nAtom = size(r,2);
-nSym = size(symOp,3);
-rSym = cell(1,0);
-
-% loop over all input atoms
-for ii = 1:nAtom
- % generate all equivalent atomic positions, some might overlap
- rTemp = mod(permute(sum(repmat(r(:,ii)',[3 1 nSym]).*symOp,2),[1 3 2])+symTr,1);
- % take out the overlapping positions
- %rSym{ii} = sw_uniquetol(rTemp,tol);
- rSym{ii} = consolidator(rTemp',[],[],tol)';
-end
-
-if fid
- for ii = 1:nAtom
- fprintf(fid,'\nAtomic coordinates generated for: (%5.3f %5.3f %5.3f)\n',r(:,ii));
- for jj = 1:size(rSym{ii},2)
- fprintf(fid,'r%i (%5.3f %5.3f %5.3f)\n',jj,rSym{ii}(:,jj));
- end
- end
- fprintf(fid,'\n');
-end
-
-if nAtom == 1
- rSym = rSym{1};
-end
-
-end
\ No newline at end of file
diff --git a/test/sw_symtest.m b/test/sw_symtest.m
deleted file mode 100644
index de34bab2e..000000000
--- a/test/sw_symtest.m
+++ /dev/null
@@ -1,59 +0,0 @@
-%% test symmetry string generator
-good = false(1,230);
-
-for ii = 1:230
- [R,T,~,strSym0] = sw_gensym(ii);
- strSym0 = strtrim(strSym0);
-
- strSym = sw_gensymstr(R,T);
-
- % check equality
- if numel(strSym0) == numel(strSym)
- good(ii) = all(strSym0==strSym);
- end
-
-end
-
-if ~all(good)
- error('Symmetry string generator test failed!')
-end
-
-%% test generator function
-
-good = false(1,230);
-
-for ii = 1:230
- [Rg,Tg] = sw_gensym(ii);
- [R,T] = sw_gencoord(ii);
- [R0, T0] = sw_symgetgen(R,T);
- % check that the number of generators are smaller or equal
- good(ii) = size(Rg,3) >= size(R0,3);
-
-end
-
-[R1,T1] = sw_gencoord({R0 T0});
-[R2,T2] = sw_gencoord({Rg(:,:,[1 3 4]) Tg(:,[1 3 4])});
-
-if ~all(good)
- error('Symmetry generator calculator test failed!')
-end
-
-% %%
-%
-% Rs =[reshape(R,9,[]); T];
-% R1s=[reshape(R1,9,[]);T1];
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test/sw_test.m b/test/sw_test.m
deleted file mode 100644
index 6ca8b0e28..000000000
--- a/test/sw_test.m
+++ /dev/null
@@ -1,80 +0,0 @@
-function errMsg = sw_test(varargin)
-% testing of sw functions
-% errMsg = sw_test('Option1,' Value1, ...)
-%
-% Options:
-%
-% fid Identifier where the text output goes.
-% 0 No output.
-% 1 Output onto the Command Window.
-% fid Output into file, opened with: fid = fopen(path)
-% tol Tolerance on the agreement of different calculated matrices,
-% default is 1e-5.
-%
-% Output:
-%
-% errMsg Cell, that contains all the error messages for every test
-% function.
-%
-
-currDir = cd;
-cd(sw_rootdir);
-
-% switch off warnings
-warnLevel = warning;
-warning('off','all');
-
-inpForm.fname = {'fid' 'tol' };
-inpForm.defval = {-1 1e-5 };
-inpForm.size = {[1 1] [1 1] };
-
-param = sw_readparam(inpForm, varargin{:});
-pref = swpref;
-
-if param.fid == -1
- param.fid = pref.fid;
-else
- fid = param.fid;
-end
-
-Res = [];
-errMsg = {};
-
-allTestPath = dir([sw_rootdir 'test' filesep 'sw_test_*.m']);
-allTestPath = {allTestPath.name};
-
-for ii = 1:numel(allTestPath)
- [Res(ii), errMsg{ii}] = eval([allTestPath{ii}(1:end-2) '(param.tol)']); %#ok
-end
-
-minRes = min(Res(Res>0));
-if isempty(minRes)
- minRes = 0;
-end
-
-switch minRes
- case 0
- % no error
- fprintf(fid,'All test ran succesfull!\n');
- case 1
- % error message
- fprintf(fid,'The code throw an error message!\n');
- case 2
- % wrong result
- fprintf(fid,'Some of the numerical results were wrong!\n');
- otherwise
- fprintf(fid,'Unkown error code!\n');
-end
-
-errMsg = [allTestPath; errMsg];
-
-% clear symmetry.dat file from newly added entries
-sw_initialize;
-
-% change back to the current directory
-cd(currDir);
-
-% switch on warnings
-warning(warnLevel);
-
-end
\ No newline at end of file
diff --git a/test/sw_test_sw1.m b/test/sw_test_sw1.m
deleted file mode 100644
index c143196b9..000000000
--- a/test/sw_test_sw1.m
+++ /dev/null
@@ -1,353 +0,0 @@
-function [Res, errMsg] = sw_test_sw1(tol)
-% check spin wave calculation (spinwave routine)
-
-Res = 0;
-errMsg = [];
-
-try
-
- %% define crystal structure of Na2IrO3
-
- % data from: Choi, S. K., Coldea, et al. (2012). PRL, 108(12), 127204
- % space group C2/m, crystallographic parameters at 300 K
-
- nairo = sw;
- nairo.genlattice('lat_const',[5.427 9.395 5.614],'angle',[90 109.037 90]*pi/180,'sym','C 2/m');
-
- % add magnetic Ir
- nairo.addatom('r',[1/2; 0.167; 0],'S',1/2,'label','Ir','color',[140; 28; 22]);
- nairo.addatom('r',[0 1/2 1/2;0 0 0.340; 0 1/2 1/2],'S',[0 0 0],'label',{'Na1' 'Na2' 'Na3'},'color',ones(3)*233);
- nairo.addatom('r',[0.748 0.711; 0.178 0; 0.789 0.204],'S',[0 0],'label',{'O1', 'O2'},'color',[45 45;118 118;125 125]);
-
- hFig = plot(nairo);
- close(hFig);
-
- % define prototype magnetic Hamiltonian
-
- % regenerate crystal with P1 symmetry since the Hamiltonian is incompatible
- % with symmetry
- nairo.newcell({[1 0 0] [0 1 0] [0 0 1]})
-
- % rotation needs to be added, see sw_rot
- nairo.addmatrix('color',[255; 0; 0],'label','JKxx');
- nairo.addmatrix('color',[0; 255; 0],'label','JKyy');
- nairo.addmatrix('color',[0; 0; 255],'label','JKzz');
-
- % Heisenberg terms
- nairo.addmatrix('color',[128; 128; 128],'label','J1');
- nairo.addmatrix('color',[200; 128; 128],'label','J2');
- nairo.addmatrix('color',[128; 200; 128],'label','J3');
-
- % generate couplings up to 8 Angstrom
- nairo.gencoupling('maxdistance',8);
-
- % add J1, J2 and J3 and JK couplings
- nairo.addcoupling('J1',[1 2]);
- nairo.addcoupling('J2',[3 4]);
- nairo.addcoupling('J3',[7 8]);
-
- % anisotropic Kitaev couplings, incompatible with crystal symmetry
- nairo.addcoupling('JKxx',1,[1 4]);
- nairo.addcoupling('JKyy',1,[2 3]);
- nairo.addcoupling('JKzz',2);
-
- hFig = plot(nairo,'range',[-0.1 3.1;-0.1 3.1;-0.1 0.1]);
- close(hFig);
-
- % define Q scans
- nQ = 200;
- nE = 800;
- dQ = 0.764e-6;
- Qp{1} = [ -1; 0; 0]+dQ;
- Qp{2} = [ 0; 0; 0]+dQ;
- Qp{3} = [ 0; 1; 0]+dQ;
- Qp{4} = [ 1; 1; 0]+dQ;
- Qp{5} = [1/2; 1/2; 0]+dQ;
- Qp{6} = [ 0; 0; 0]+dQ;
-
- % case d stripy order
- % energy per spin is -0.2912
- J1 = 1; J2 = 0; J3 = 0; JK = 1.33;
- Na2IrO3fun(nairo,[J1 J2 J3 JK]);
-
- opt_par.xmin = [0 0 0 0, 0 0 0, 0 0];
- opt_par.xmax = [[1 1 1 1]*2*pi, 0 0 0, pi 0];
- opt_par.func = @gm_planar;
- opt_par.nRun = 10;
-
- nairo.optmagstr(opt_par);
-
- % calculate spin waves with zig-zag order
- E = linspace(0,4,nE);
-
- specD = nairo.spinwave([Qp {nQ}]);
- specD = sw_neutron(specD,'pol',false);
- specD = sw_egrid(specD,'component','Sxx','evect',E);
-
- hFig = figure;
- sw_plotspec(specD,'mode',3,'ahandle',gca,'imag',false,'convE',0.05,'axLim',0.5);
- sw_plotspec(specD,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'legend',false,'title',false);
- close(hFig);
-
- %% case e-f stripy order
- % energy per spin is -0.375
-
- J1 = 1; J2 = 0; J3 = 0; JK = 2;
- Na2IrO3fun(nairo,[J1 J2 J3 JK]);
-
- opt_par.xmin = [0 0 0 0, 0 0 0, 0 0];
- opt_par.xmax = [[1 1 1 1]*2*pi, 0 0 0, pi 0];
- opt_par.func = @gm_planar;
- opt_par.nRun = 10;
-
- nairo.optmagstr(opt_par);
-
- % calculate spin waves with zig-zag order
- E = linspace(0,4,nE);
-
- specEF = nairo.spinwave([Qp {nQ}]);
- specEF = sw_neutron(specEF,'pol',false);
- specEF = sw_egrid(specEF,'component','Syy','evect',E);
-
- hFig = figure;
- sw_plotspec(specEF,'mode',3,'ahandle',gca,'imag',false,'convE',0.05,'axLim',0.5);
- sw_plotspec(specEF,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'legend',false,'title',false);
- close(hFig);
-
-
- %% case g stripy order
- % optimise magnetic structure assuming planar
- % --> finds tripy order
-
- J1 = 1; J2 = 0.26; J3 = -0.2; JK = 0;
- Na2IrO3fun(nairo,[J1 J2 J3 JK]);
-
- opt_par.xmin = [0 0 0 0, 0 0 0, 0 0];
- opt_par.xmax = [[0 1 1 1]*2*pi, 0 0 0, 0 0];
- opt_par.func = @gm_planar;
- opt_par.nRun = 10;
-
- nairo.optmagstr(opt_par);
-
- % calculate spin waves with stripy order
- E = linspace(0,2,nE);
-
- specG = nairo.spinwave([Qp {nQ}]);
- specG = sw_neutron(specG,'pol',false);
- specG = sw_egrid(specG,'component','Szz','evect',E);
-
- hFig = figure;
- sw_plotspec(specG,'mode',3,'ahandle',gca,'imag',false,'convE',0.05,'axLim',0.5);
- sw_plotspec(specG,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'legend',false,'title',false);
- close(hFig);
-
- %% case h zig-zag order
- % optimise magnetic structure assuming planar
- % --> finds tripy order
-
- J1 = 1; J2 = 0.78; J3 = 0.9; JK = 0;
- Na2IrO3fun(nairo,[J1 J2 J3 JK]);
-
- opt_par.xmin = [0 0 0 0, 0 0 0, 0 0];
- opt_par.xmax = [[0 1 1 1]*2*pi, 0 0 0, 0 0];
- opt_par.func = @gm_planar;
- opt_par.nRun = 10;
-
- nairo.optmagstr(opt_par);
-
- % calculate spin waves with zig-zag order
- E = linspace(0,4,nE);
-
- specH = nairo.spinwave([Qp {nQ}]);
- specH = sw_neutron(specH,'pol',false);
- specH = sw_egrid(specH,'component','Syy','evect',E);
-
- hFig = figure;
- subplot(2,1,1)
- sw_plotspec(specH,'mode',3,'ahandle',gca,'imag',false,'convE',0.05,'axLim',0.5);
- sw_plotspec(specH,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'legend',false,'title',false,'colormap',[0 0 0]);
-
- % plot exact solution below
- subplot(2,1,2);
- specSimH = specH;
- specSimH.omega = repmat(omegaH([Qp {nQ}],[J1 J2 J3 JK]),4,1);
- sw_plotspec(specSimH,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'colormap',[255 0 0]);
- title('Exact spin wave dispersion','fontsize',14);
-
- close(hFig);
-
- specH = sw_omegasum(specH);
- specSimH = sw_omegasum(specSimH);
-
- % calculate difference between exact and numerical solutions
- omegaDiff = abs(specH.omega-specSimH.omega);
- omegaDiff(isnan(omegaDiff)) = 0;
-
- omegaDiff = max(max(omegaDiff));
- ratioH = omegaDiff/max(max(abs(real(specH.omega))));
-
- %% powder spectra
- J1 = 4.17; J2 = 0.78*J1; J3 = 0.9*J1; JK = 0;
- Na2IrO3fun(nairo,[J1 J2 J3 JK]);
-
- hklA = linspace(0.2,1.5,30);
-
- specHpow = nairo.powspec(hklA,'evect',linspace(0,8,100),'nRand',1e2);
-
- hFig = figure;
- sw_plotspec(specHpow)
- close(hFig);
-
- %% case ij zig-zag order
- % optimise magnetic structure assuming planar
-
- % DISPERSION DEPENDS ON THE SPIN DIRECTION
- % THE M POINT IS NOT UNIQUE
-
- J1 = 1; J2 = 0.23; J3 = 0.51; JK = 1.33;
- Na2IrO3fun(nairo,[J1 J2 J3 JK]);
-
- opt_par.xmin = [0 0 0 0, 0 0 0, 0 0];
- opt_par.xmax = [[0 1 1 1]*2*pi, 0 0 0, pi 0];
- opt_par.func = @gm_planar;
- opt_par.nRun = 10;
-
- nairo.optmagstr(opt_par);
-
- % calculate spin waves with zig-zag order
- E = linspace(0,3,nE);
-
- specIJ = nairo.spinwave([Qp {nQ}]);
- specIJ = sw_neutron(specIJ,'pol',false);
-
- component = {'Sxx' 'Szz'};
-
- hFig = figure;
-
- for ii = 1:2
- subplot(3,1,ii);
- specIJ = sw_egrid(specIJ,'component',component{ii},'evect',E);
-
- sw_plotspec(specIJ,'mode',3,'ahandle',gca,'imag',false,'convE',0.05,'axLim',0.5);
- sw_plotspec(specIJ,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'legend',false,'title',false,'colormap',[0 0 0]);
- caxis([0 0.5])
- end
-
- % plot exact solution below
- % EXACT SOLUTION HAS WRONG EQUATIONS IN PUBLICATION
- subplot(3,1,3);
- specSimIJ = specIJ;
- omegaTemp = repmat(omegaIJ([Qp {nQ}],[J1 J2 J3 JK]),2,1);
- omegaTemp = bsxfun(@times,omegaTemp,[1 1 1 1 0 0 0 0]');
- specSimIJ.omega = omegaTemp;
-
- sw_plotspec(specSimIJ,'mode',1,'ahandle',gca,'imag',false,'dashed',true,'colorbar',false,'colormap',[255 0 0]);
- title('Exact spin wave dispersion','fontsize',14);
-
- close(hFig);
-
- % specIJ = sw_omegasum(specIJ);
- % specSimIJ = sw_omegasum(specSimIJ);
- %
- % omegaDiff = abs(specIJ.omega-specSimIJ.omega);
- % omegaDiff(isnan(omegaDiff)) = 0;
- %
- % omegaDiff = max(max(omegaDiff));
- % ratioIJ = omegaDiff/max(max(abs(real(specIJ.omega))));
-catch errMsg
- % code throws error
- Res = 1;
- return;
-end
-
-try
- if abs(ratioH) > tol
- error('sw_test_sw1:DataError','The calculated spin wave dispersion differs from the exact result!');
- end
-catch errMsg
- Res = 2;
- return;
-end
-
-end
-
-
-function Na2IrO3fun(obj, Jinp)
-% takes the values as [J1 J2 J3 JK] and put them into the Hamiltonian
-
-J1 = Jinp(1);
-J2 = Jinp(2);
-J3 = Jinp(3);
-JK = Jinp(4);
-
-JKxx=zeros(3); JKxx(1,1)=-JK;
-JKyy=zeros(3); JKyy(2,2)=-JK;
-JKzz=zeros(3); JKzz(3,3)=-JK;
-
-obj.matrix.mat = cat(3,JKxx,JKyy,JKzz,eye(3)*J1,eye(3)*J2,eye(3)*J3);
-
-end
-
-
-function omega = omegaH(q,x)
-
-q = sw_qscan(q);
-
-J1 = x(1);
-J2 = x(2);
-J3 = x(3);
-
-S = 1/2;
-
-h = q(1,:);
-k = q(2,:);
-
-eta = exp(k*pi*1i/3);
-
-A = S*(-J1 + 2*J2 + 3*J3 + 2*J2*cos(2*pi*h));
-B = 2*S*J1*eta.*cos(pi*h);
-C = 2*S*J2*(cos(pi*(h+k))+cos(pi*(h-k)));
-D = S*(J1*eta.^2 + J3*(eta.^(-4)+2*eta.^2.*cos(2*pi*h)));
-
-sum1 = A.^2 + B.*conj(B) - C.^2 - D.*conj(D);
-sum2 = sqrt(4*abs(A.*B-C.*conj(D)).^2-abs(conj(B).*conj(D)-B.*D).^2);
-
-omega = [sqrt(sum1 + sum2); sqrt(sum1 - sum2)];
-
-end
-
-function omega = omegaIJ(q,x)
-
-q = sw_qscan(q);
-
-J1 = x(1);
-J2 = x(2);
-J3 = x(3);
-JK = x(4);
-
-S = 1/2;
-
-h = q(1,:);
-k = q(2,:);
-
-eta = exp(k*pi*1i/3);
-xi = exp(h*pi*1i);
-
-A = S*(-J1 + 2*J2 + 3*J3 + 2*J2*cos(2*pi*h) + JK);
-B = -1/2*S*JK*eta.^(-2);
-C = S*(2*J1*cos(pi*h)-1/2*JK*xi).*eta;
-D = S*(J1*eta.^(-2) + J3*(eta.^4+2*eta.^(-2).*cos(2*pi*h))-JK/2*eta.^(-2));
-E = 2*S*J2*(cos(pi*(h+k))+cos(pi*(h-k)));
-F = 1/2*S*JK*xi.*eta;
-
-delta = abs((B-C).*conj(D-F) - conj(B-C).*(D-F));
-
-sum1a = A.^2 - E.^2 + abs(B-C).^2 - abs(D-F).^2;
-sum1b = A.^2 - E.^2 + abs(B+C).^2 - abs(D+F).^2;
-
-sum2a = sqrt(4*abs(A.*(B-C)+E.*(D-F)).^2-delta.^2);
-sum2b = sqrt(4*abs(A.*(B+C)-E.*(D+F)).^2-delta.^2);
-
-omega = [sqrt(sum1a + sum2a); sqrt(sum1a - sum2a); sqrt(sum1b + sum2b); sqrt(sum1b - sum2b)];
-
-end
\ No newline at end of file
diff --git a/test/sw_test_sw2.m b/test/sw_test_sw2.m
deleted file mode 100644
index 03b0798b3..000000000
--- a/test/sw_test_sw2.m
+++ /dev/null
@@ -1,225 +0,0 @@
-function [Res, errMsg] = sw_test_sw2(tol)
-
-Res = 0;
-errMsg = [];
-
-try
- %% LiNiPO4
- % Model taken from T. Jensen et al., Phys. Rev. B. 79, 092413(2009)
-
-
- % Fitted parameters at T = 1.5 K
-
- % Jbc = 1.04;
- % Jb = 0.670;
- % Jc = -0.05;
- % Jac = -0.11;
- % Jab = 0.30;
- % Da = 0.339;
- % Db = 1.82;
- % Dc = 0;
- Jbc = 1.036;
- Jb = 0.6701;
- Jc = -0.0469;
- Jac = -0.1121;
- Jab = 0.2977;
- Da = 0.1969;
- Db = 0.9097;
- Dc = 0;
-
- Jvect = [Jbc Jab Jb Jc Jac Da Db];
- %% Analytical dispersion from the paper
-
- nQ = 500;
- hFig = figure;
-
- subplot(2,2,1)
- Qa = [0 1 0];
- Qb = [2 1 0];
- omega = Jensen_LiNiPO4({Qa Qb nQ}, Jvect);
- plot(linspace(0,2,nQ),omega(1,:),'b-',linspace(0,2,nQ),abs(omega(2,:)),'r-.');
- axis([0 2 0 8.5]);
- xlabel('H (r.l.u)'); ylabel('\omega (meV)'); title('(H,1,0)');
-
- subplot(2,2,2)
- Qa = [0 0 0];
- Qb = [0 2 0];
- omega = Jensen_LiNiPO4({Qa Qb nQ}, Jvect);
- plot(linspace(0,2,nQ),omega(1,:),'b-',linspace(0,2,nQ),abs(omega(2,:)),'r-.');
- axis([0 2 0 8.5]);
- xlabel('K (r.l.u)'); ylabel('\omega (meV)'); title('(0,K,0)');
-
- subplot(2,2,3)
- Qa = [0 1 0];
- Qb = [0 1 2];
- omega = Jensen_LiNiPO4({Qa Qb nQ}, Jvect);
- plot(linspace(0,2,nQ),omega(1,:),'b-',linspace(0,2,nQ),abs(omega(2,:)),'r-.');
- axis([0 2 0 8.5]);
- xlabel('L (r.l.u)'); ylabel('\omega (meV)'); title('(0,1,L)');
- close(hFig);
-
- %% LiNiPO4 structure in SpinW (only magnetic atoms)
-
- linipo = sw;
- linipo.genlattice('lat_const',[10.02 5.86 4.68],'sym','P n m a')
-
- linipo.addatom('r',[1/4 1/4 0],'S',1,'label','MNi2','color',[0;0;255])
- %Ni2.r = [0.2756; 1/4; 0.9825];
-
- linipo.gencoupling
-
- % Define the interactions
-
- % Isotropic exchanges
- linipo.addmatrix('mat',eye(3)*Jbc,'color',[255 0 0],'label','Jbc')
- linipo.addmatrix('mat',eye(3)*Jb, 'color',[0 255 0],'label','Jb' )
- linipo.addmatrix('mat',eye(3)*Jc, 'color',[0 0 255],'label','Jc' )
- linipo.addmatrix('mat',eye(3)*Jab,'color',[0 125 125],'label','Jab' )
- linipo.addmatrix('mat',eye(3)*Jac,'color',[125 125 0],'label','Jac' )
-
- % anisotropy matrix
- linipo.addmatrix('mat',diag([Da Db Dc]), 'color',[125 0 125],'label','D' )
-
-
- linipo.addcoupling('Jbc',1)
- linipo.addcoupling('Jc' ,2)
- linipo.addcoupling('Jab',[5 6])
- linipo.addcoupling('Jac',[3 4])
- linipo.addcoupling('Jb' ,7)
-
- linipo.addaniso('D')
-
- linipo.genmagstr('mode','direct','S',[0 0 0 0;0 0 0 0;1 -1 -1 1]);
-
- %% plot structure
-
- hFig = plot(linipo,'range',[-0.3 0.8;-0.3 0.8;-0.1 1.1]);
- close(hFig);
-
- %% simulated annealing
-
- % par_anneal.initT = 100;
- % par_anneal.endT = 1e-2;
- % par_anneal.nMC = 1000;
- % par_anneal.cool = @(T)0.8*T;
- % par_anneal.nStat = 1;
- %
- % [linipo, aStat] = sw_anneal(linipo,par_anneal);
-
-
- %% spin wave dispersion
-
- nQ = 400;
- Qa = [0 1 0];
- Qb = [2 1 0];
-
- specLi = linipo.spinwave({Qa Qb nQ});
- specLi = sw_neutron(specLi,'pol',false);
- specLi = sw_egrid(specLi,'Evect',linspace(0,8.5,400));
-
- % calculate difference between exact and numerical solutions
- specLiSim = specLi;
- specLiSim.omega = repmat(Jensen_LiNiPO4({Qa Qb nQ},Jvect),4,1);
- specLi = sw_omegasum(specLi,'zeroint',1e-8);
- specLiSim = sw_omegasum(specLiSim,'zeroint',1e-8);
- omegaDiff = abs(specLi.omega-specLiSim.omega);
- omegaDiff(isnan(omegaDiff)) = 0;
- omegaDiff = max(max(omegaDiff));
- ratioLi1 = omegaDiff/max(max(abs(real(specLi.omega))));
-
- hFig = figure;
- subplot(2,1,1)
- sw_plotspec(specLi,'axLim',1,'mode',4,'aHandle',gca)
- subplot(2,1,2)
- sw_plotspec(specLi,'axLim',15,'mode',2,'aHandle',gca,'imag',false)
- close(hFig)
-
- Qa = [0 0 0];
- Qb = [0 2 0];
-
- specLi = linipo.spinwave({Qa Qb nQ});
- specLi = sw_neutron(specLi,'pol',false);
- specLi = sw_egrid(specLi,'Evect',linspace(0,8.5,400));
-
- hFig = figure;
- subplot(2,1,1)
- sw_plotspec(specLi,'axLim',1,'mode',4,'aHandle',gca)
- subplot(2,1,2)
- sw_plotspec(specLi,'axLim',15,'mode',2,'aHandle',gca,'imag',false)
- close(hFig)
-
- Qa = [0 1 0];
- Qb = [0 1 2];
-
- specLi = linipo.spinwave({Qa Qb nQ});
- specLi = sw_neutron(specLi,'pol',false);
- specLi = sw_egrid(specLi,'Evect',linspace(0,8.5,400));
-
- % calculate difference between exact and numerical solutions
- specLiSim = specLi;
- specLiSim.omega = repmat(Jensen_LiNiPO4({Qa Qb nQ},Jvect),4,1);
- specLi = sw_omegasum(specLi,'zeroint',1e-8);
- specLiSim = sw_omegasum(specLiSim,'zeroint',1e-8);
- omegaDiff = abs(specLi.omega-specLiSim.omega);
- omegaDiff(isnan(omegaDiff)) = 0;
- omegaDiff = max(max(omegaDiff));
- ratioLi3 = omegaDiff/max(max(abs(real(specLi.omega))));
-
- hFig = figure;
- subplot(2,1,1)
- sw_plotspec(specLi,'axLim',1,'mode',4,'aHandle',gca)
- subplot(2,1,2)
- sw_plotspec(specLi,'axLim',15,'mode',2,'aHandle',gca,'imag',false)
- close(hFig)
-
-catch errMsg
- Res = 1;
- return;
-end
-
-try
- if max(abs(ratioLi1),abs(ratioLi3)) > tol
- error('sw_test_sw1:DataError','The calculated spin wave dispersion differs from the exact result!');
- end
-catch errMsg
- Res = 2;
- return;
-end
-
-end
-
-function omega = Jensen_LiNiPO4(Q, Jinp)
-% [omega(1,:)out omega(2,:)out] = Jensen_LiNiPO4(Q, Jinp)
-% simulate neutron scattering cross section for LiNiPO4 to compare with sw
-%
-% Q Momentum transfer, dimensions are [3 nQ].
-% Jinp Input parameters of the Hamiltonian: [Jyz Jxy Jy Jz Jxz Dx Dy].
-%
-
-if iscell(Q)
- Q = sw_qscan(Q);
-end
-
-h = Q(1,:);
-k = Q(2,:);
-l = Q(3,:);
-
-Jyz = Jinp(1);
-Jxy = Jinp(2);
-Jy = Jinp(3);
-Jz = Jinp(4);
-Jxz = Jinp(5);
-Dx = Jinp(6);
-Dy = Jinp(7);
-
-% spin of the magnetic Ni3+ ions
-S = 1;
-
-A = 4*S*(Jyz + Jxy)-2*S*(Jy*(1-cos(2*pi*k)) + Jz*(1-cos(2*pi*l))+...
- Jxz*(2-cos(pi*(h+l))-cos(pi*(h-l))))+ Dx * S + Dy * S;
-B = S*(Dx - Dy);
-D = 2*Jyz*S*(cos(pi*(k+l))+cos(pi*(k-l)))+2*Jxy*S*(cos(pi*(h+k))+cos(pi*(h-k)));
-
-omega = [sqrt(A.^2-(B+D).^2); -sqrt(A.^2-(B-D).^2)];
-
-end
\ No newline at end of file
diff --git a/test/sw_test_sym1.m b/test/sw_test_sym1.m
deleted file mode 100644
index 4cde1e367..000000000
--- a/test/sw_test_sym1.m
+++ /dev/null
@@ -1,78 +0,0 @@
-function [Res, errMsg] = sw_test_sym1(tol)
-% test symmetry on different simple cyclic space groups
-
-% no error
-Res = 0;
-errMsg = [];
-
-try
- %% TEST RUN BY HAND
- tetra = sw;
- tetra.genlattice('lat_const',[6 6 5],'sym','P 4','angle',[90 90 90]*pi/180);
-
- %tetra.addatom('r',[1/4 1/4 0]);
- tetra.addatom('r',[0/2 0/2 0]);
-
- tetra.addmatrix('label',{'J1'});
- tetra.addmatrix('label',{'J2'},'color',[0; 255; 0]);
- tetra.addmatrix('label',{'A'},'mat',diag([1 0 0]));
-
- tetra.gencoupling('maxDistance',10);
- tetra.addcoupling('J1',1);
- tetra.addcoupling('J2',2);
-
-
- tetra.addaniso('A',1);
- %tetra.matrix.mat(:,:,3) = [0 1 0;1 0 0;0 0 0];
- tetra.setmatrix('label','A','fid',0,'pref',{[1 1]})
-
- hFig = plot(tetra,'range',[2 2 1]);
-
- %%
- close(hFig);
-
-catch errMsg
-
- % code throws error
- Res = 1;
- return;
-end
-
-% test whether the results are right
-try
- aMat = tetra.getmatrix('aniso_idx',1);
- if size(aMat,3) ~= 2
- error('sw_test_sym1:WrongMat','Wrong number of allowed anisotropy matrix!');
- end
- rightMat = cat(3,diag([0 0 1]),diag([1 1 0]));
- errMat1 = (aMat - rightMat).^2;
- errMat2 = (aMat - rightMat(:,:,[2 1])).^2;
- if (sum(errMat1(:)) > tol) && (sum(errMat2(:)) > tol)
- error('sw_test_sym1:WrongMat','Wrong values in the allowed anisotropy matrix!');
- end
-
- cMat = tetra.getmatrix('coupling_idx',1);
- if size(cMat,3) ~= 3
- error('sw_test_sym1:WrongMat','Wrong number of allowed coupling matrix!');
- end
- rightMat = cat(3,diag([1 1 0]),diag([0 0 1]),[0 1 0;-1 0 0;0 0 0]);
- for ii = 1:3
- right = false;
- for jj = 1:3
- errMat = (cMat(:,:,ii) - rightMat(:,:,jj)).^2;
- if sum(errMat(:)) < tol
- right = true;
- end
- end
- if ~right
- error('sw_test_sym1:WrongMat','Wrong values in the allowed coupling matrix!');
- end
- end
-
-catch errMsg
-
- % wrong result
- Res = 2;
-end
-
-end
\ No newline at end of file
diff --git a/test/sw_test_sym2.m b/test/sw_test_sym2.m
deleted file mode 100644
index ca6ea277f..000000000
--- a/test/sw_test_sym2.m
+++ /dev/null
@@ -1,39 +0,0 @@
-function [Res, errMsg] = sw_test_sym2(tol)
-
-% no error
-Res = 0;
-errMsg = [];
-
-try
- %% RUN BY HAND
- mnco3 = sw;
- mnco3.genlattice('lat_const',[4.773 4.773 16.642],'angle',[90 90 120]*pi/180,'sym','R -3 c');
- %mnco3.genlattice('lat_const',[4.773 4.773 16.642],'angle',[90 90 120]*pi/180,'sym',167);
-
- mnco3.addatom('label',{'Mn'},'r',[0 0 0]','S',1,'color',[0;0;255]);
- mnco3.addatom('label',{'C'},'r', [0 0 1/4]','S',0,'color',[128;128;128]);
- mnco3.addatom('label',{'O'},'r', [0.2695 0 1/4]','S',0,'color',[255;0;0]);
-
- mnco3.gencoupling('maxdistance',10);
- mnco3.addmatrix('label',{'J1'},'color',[255;0;0]);
- mnco3.addmatrix('label',{'J2'},'color',[0;255;0]);
- mnco3.addmatrix('label',{'J3'},'color',[0;0;255]);
-
- mnco3.addcoupling('J1',1);
- mnco3.addcoupling('J2',1);
- %mnco3.addcoupling('J3',3);
-
- mnco3.setmatrix('label','J1','pref',{[1 0 0]});
- mnco3.setmatrix('label','J2','pref',{[0 1 0]});
-
- hFig = plot(mnco3,'range',[2 2 1],'pNonMagAtom',false);
- %%
- close(hFig);
-
-catch errMsg
- % code throws error
- Res = 1;
- return;
-end
-
-end
\ No newline at end of file
diff --git a/test/sw_test_sym3.m b/test/sw_test_sym3.m
deleted file mode 100644
index f813a2812..000000000
--- a/test/sw_test_sym3.m
+++ /dev/null
@@ -1,78 +0,0 @@
-function [Res, errMsg] = sw_test_sym3(tol)
-
-% no error
-Res = 0;
-errMsg = [];
-
-try
- %% RUN BY HAND
-
- % LuVO3 intermediate temperature Pbnm space group
-
- a = 5.2821;
- b = 5.6144;
- c = 7.5283;
-
- sw_addsym('x+1/2,-y+1/2,-z; -x,-y,z+1/2; -x,-y,-z','P b n m');
-
- luvo = sw;
- luvo.genlattice('lat_const',[a b c],'sym','P b n m');
-
- % V1 atom
- luvo.addatom('r',[1/2 0 0],'label','V','S',1,'color',[128; 128; 128]);
- % O1 atom
- luvo.addatom('r',[0.11560;0.44821;1/4],'label','O1','S',0,'color',[255; 0; 0]);
- % O2 atom
- luvo.addatom('r',[0.68170;0.30500;0.06540],'label','O2','S',0,'color',[255; 0; 0]);
-
- luvo.gencoupling('maxdistance',8);
-
- luvo.addmatrix('label',{'J1a'});
- luvo.addmatrix('label',{'J1b'});
- luvo.addmatrix('label',{'Jab'},'color',[0;0;255]);
-
- luvo.addcoupling('J1a',1);
- luvo.addcoupling('J1b',1);
-
- luvo.setmatrix('label','J1a','pref',{[1 0 0]});
- luvo.setmatrix('label','J1b','pref',{[0 1 0]});
-
- hFig = plot(luvo,'pNonMagAtom',false,'pZeroCoupling',true);
-
- %%
-
- close(hFig);
-
-catch errMsg
- % code throw error
- Res = 1;
- return;
-end
-
-try
-
- cMat = luvo.getmatrix('coupling_idx',1);
- sMat = (cMat-permute(cMat,[2 1 3])).^2;
- sMat = permute(sum(sum(sMat)),[2 3 1]) > tol;
-
- if sum(sMat) ~= 2
- error('sw_test_sym1:WrongMat','Wrong number of allowed antisymmetric coupling matrix!');
- end
-
- % test the generated DM interactions
- gMat = reshape(luvo.intmatrix.all(end-8:end,:),3,3,[]);
- % (- - + + - + + -)
- if any(abs(permute(cat(3,gMat(2,3,1:4),gMat(1,3,5:8)),[2,3,1]) - [1 1 -1 -1 -1 1 1 -1])>tol)
- error('sw_test_sym1:WrongMat','Wrong values of the generated antisymmetric coupling matrix!');
- end
- if any(abs(permute(cat(3,gMat(3,2,1:4),gMat(3,1,5:8)),[2,3,1]) - [-1 -1 1 1 1 -1 -1 1])>tol)
- error('sw_test_sym1:WrongMat','Wrong values of the generated antisymmetric coupling matrix!');
- end
-
-
-catch errMsg
- % wrong result
- Res = 2;
-end
-
-end
\ No newline at end of file
diff --git a/test_data/system_tests/systemstest_spinwave_KCu3As2O7.mat b/test_data/system_tests/systemstest_spinwave_KCu3As2O7.mat
new file mode 100644
index 000000000..f8d3b84f7
Binary files /dev/null and b/test_data/system_tests/systemstest_spinwave_KCu3As2O7.mat differ
diff --git a/test_data/system_tests/systemstest_spinwave_af33kagome.mat b/test_data/system_tests/systemstest_spinwave_af33kagome.mat
new file mode 100644
index 000000000..489010db1
Binary files /dev/null and b/test_data/system_tests/systemstest_spinwave_af33kagome.mat differ
diff --git a/test_data/system_tests/systemstest_spinwave_biquadratic.mat b/test_data/system_tests/systemstest_spinwave_biquadratic.mat
new file mode 100644
index 000000000..df8de7cc8
Binary files /dev/null and b/test_data/system_tests/systemstest_spinwave_biquadratic.mat differ
diff --git a/test_data/system_tests/systemstest_spinwave_pcsmo.mat b/test_data/system_tests/systemstest_spinwave_pcsmo.mat
new file mode 100644
index 000000000..23e0cef45
Binary files /dev/null and b/test_data/system_tests/systemstest_spinwave_pcsmo.mat differ
diff --git a/test_data/system_tests/systemstest_spinwave_yb2ti2o7.mat b/test_data/system_tests/systemstest_spinwave_yb2ti2o7.mat
new file mode 100644
index 000000000..cd51f6a8c
Binary files /dev/null and b/test_data/system_tests/systemstest_spinwave_yb2ti2o7.mat differ
diff --git a/test_data/unit_tests/Figure/afm_chain_spec.fig b/test_data/unit_tests/Figure/afm_chain_spec.fig
new file mode 100644
index 000000000..7cb706b1f
Binary files /dev/null and b/test_data/unit_tests/Figure/afm_chain_spec.fig differ
diff --git a/test_data/unit_tests/Figure/default_structure.fig b/test_data/unit_tests/Figure/default_structure.fig
new file mode 100644
index 000000000..734f5d950
Binary files /dev/null and b/test_data/unit_tests/Figure/default_structure.fig differ
diff --git a/test_data/unit_tests/cifs/BiMn2O5.fst b/test_data/unit_tests/cifs/BiMn2O5.fst
new file mode 100644
index 000000000..d4bb1bf5a
--- /dev/null
+++ b/test_data/unit_tests/cifs/BiMn2O5.fst
@@ -0,0 +1,12 @@
+! FILE for FullProf Studio: generated automatically by FullProf
+!Title: BiMn2O5
+SPACEG P b a m
+CELL 7.540001 8.534000 5.766000 90.0000 90.0000 90.0000 DISPLAY MULTIPLE
+BOX -0.15 1.15 -0.15 1.15 -0.15 1.15
+ATOM Bi BI 0.16018 0.16892 0.00000
+ATOM Mn1 MN 0.50000 0.00000 0.26780
+ATOM Mn2 MN 0.40904 0.35905 0.50000
+ATOM O1 O 0.00000 0.00000 0.27441
+ATOM O2 O 0.39117 0.17375 0.29189
+ATOM O3 O 0.13426 0.40576 0.50000
+ATOM O4 O 0.11655 0.47764 0.00000
diff --git a/test_data/unit_tests/cifs/BiMnO3.fst b/test_data/unit_tests/cifs/BiMnO3.fst
new file mode 100644
index 000000000..ed7419ee4
--- /dev/null
+++ b/test_data/unit_tests/cifs/BiMnO3.fst
@@ -0,0 +1,16 @@
+! FILE for FullProf Studio: generated automatically by FullProf
+!Title: BiMnO3
+SPACEG C 1 2 1
+CELL 9.483113 5.613111 9.734455 90.0000 111.1613 90.0000 DISPLAY MULTIPLE
+BOX -0.15 1.15 -0.15 1.15 -0.15 1.15
+ATOM Bi1 BI 0.13500 0.03700 0.37200
+ATOM Bi2 BI 0.35900 0.10000 0.11500
+ATOM Mn1 MN 0.00000 0.00000 0.00000
+ATOM Mn2 MN 0.25400 0.06200 0.75700
+ATOM Mn3 MN 0.50000 0.09600 0.50000
+ATOM O1 O 0.10000 0.01500 0.83800
+ATOM O2 O 0.40000 0.16900 0.67200
+ATOM O3 O 0.14900 0.40500 0.63000
+ATOM O4 O 0.34600 0.36300 0.41500
+ATOM O5 O 0.36500 0.27600 0.91500
+ATOM O6 O 0.14400 0.25700 0.10600
diff --git a/test_data/unit_tests/cifs/LaFeO3_fullprof.cif b/test_data/unit_tests/cifs/LaFeO3_fullprof.cif
new file mode 100644
index 000000000..eeccd934f
--- /dev/null
+++ b/test_data/unit_tests/cifs/LaFeO3_fullprof.cif
@@ -0,0 +1,422 @@
+##############################################################################
+### FullProf-generated CIF output file (version: May 2011) ###
+### Template of CIF submission form for structure report ###
+##############################################################################
+
+# This file has been generated using FullProf.2k taking one example of
+# structure report provided by Acta Cryst. It is given as a 'template' with
+# filled structural items. Many other items are left unfilled and it is the
+# responsibility of the user to properly fill or suppress them. In principle
+# all question marks '?' should be replaced by the appropriate text or
+# numerical value depending on the kind of CIF item.
+# See the document: cif_core.dic (URL: http://www.iucr.org) for details.
+
+# Please notify any error or suggestion to:
+# Juan Rodriguez-Carvajal (jrc@ill.eu)
+# Improvements will be progressively added as needed.
+
+#=============================================================================
+ data_global
+#=============================================================================
+
+# PROCESSING SUMMARY (IUCr Office Use Only)
+
+_journal_data_validation_number ?
+
+_journal_date_recd_electronic ?
+_journal_date_to_coeditor ?
+_journal_date_from_coeditor ?
+_journal_date_accepted ?
+_journal_date_printers_first ?
+_journal_date_printers_final ?
+_journal_date_proofs_out ?
+_journal_date_proofs_in ?
+_journal_coeditor_name ?
+_journal_coeditor_code ?
+_journal_coeditor_notes
+; ?
+;
+_journal_techeditor_code ?
+_journal_techeditor_notes
+; ?
+;
+_journal_coden_ASTM ?
+_journal_name_full ?
+_journal_year ?
+_journal_volume ?
+_journal_issue ?
+_journal_page_first ?
+_journal_page_last ?
+_journal_paper_category ?
+_journal_suppl_publ_number ?
+_journal_suppl_publ_pages ?
+
+#=============================================================================
+
+# 1. SUBMISSION DETAILS
+
+_publ_contact_author_name ? # Name of author for correspondence
+_publ_contact_author_address # Address of author for correspondence
+; ?
+;
+_publ_contact_author_email ?
+_publ_contact_author_fax ?
+_publ_contact_author_phone ?
+
+_publ_contact_letter
+; ?
+;
+
+_publ_requested_journal ?
+_publ_requested_coeditor_name ?
+_publ_requested_category ? # Acta C: one of CI/CM/CO/FI/FM/FO
+
+
+# Definition of non standard CIF items (Reliability indices used in FULLPROF)
+
+loop_
+_publ_manuscript_incl_extra_item
+_publ_manuscript_incl_extra_info
+_publ_manuscript_incl_extra_defn
+# Name Explanation Standard?
+# ------ ----------- ---------
+ '_pd_proc_ls_prof_cR_factor' 'Prof. R-factor CORRECTED for background' no
+ '_pd_proc_ls_prof_cwR_factor' 'wProf.R-factor CORRECTED for background' no
+ '_pd_proc_ls_prof_cwR_expected' 'wProf.Expected CORRECTED for background' no
+ '_pd_proc_ls_prof_chi2' 'Chi-square for all considered points' no
+ '_pd_proc_ls_prof_echi2' 'Chi-2 for points with Bragg contribution' no
+#=============================================================================
+
+# 3. TITLE AND AUTHOR LIST
+
+_publ_section_title
+; ' LaFeO3 Bio-D'
+;
+_publ_section_title_footnote
+;
+;
+
+# The loop structure below should contain the names and addresses of all
+# authors, in the required order of publication. Repeat as necessary.
+
+loop_
+ _publ_author_name
+ _publ_author_footnote
+ _publ_author_address
+? #<--'Last name, first name'
+; ?
+;
+; ?
+;
+
+#=============================================================================
+
+# 4. TEXT
+
+_publ_section_synopsis
+; ?
+;
+_publ_section_abstract
+; ?
+;
+_publ_section_comment
+; ?
+;
+_publ_section_exptl_prep # Details of the preparation of the sample(s)
+ # should be given here.
+; ?
+;
+_publ_section_exptl_refinement
+; ?
+;
+_publ_section_references
+; ?
+;
+_publ_section_figure_captions
+; ?
+;
+_publ_section_acknowledgements
+; ?
+;
+
+#=============================================================================
+
+#=============================================================================
+# If more than one structure is reported, the remaining sections should be
+# completed per structure. For each data set, replace the '?' in the
+# data_? line below by a unique identifier.
+
+data_LaFeO3
+
+#=============================================================================
+
+# 5. CHEMICAL DATA
+
+_chemical_name_systematic
+; ?
+;
+_chemical_name_common ?
+_chemical_formula_moiety ?
+_chemical_formula_structural ?
+_chemical_formula_analytical ?
+_chemical_formula_iupac ?
+_chemical_formula_sum ?
+_chemical_formula_weight ?
+_chemical_melting_point ?
+_chemical_compound_source ? # for minerals and
+ # natural products
+
+loop_
+ _atom_type_symbol
+ _atom_type_scat_length_neutron
+ _atom_type_scat_source
+FE 0.94500 V.F._Sears_Neutron_News_3_26_(1992)
+LA 0.82400 V.F._Sears_Neutron_News_3_26_(1992)
+O 0.58030 V.F._Sears_Neutron_News_3_26_(1992)
+
+#=============================================================================
+
+# 6. CRYSTAL DATA
+
+_symmetry_cell_setting Orthorhombic
+_symmetry_space_group_name_H-M 'P n m a'
+_symmetry_space_group_name_Hall '-P 2ac 2n'
+
+loop_
+ _symmetry_equiv_pos_as_xyz #<--must include 'x,y,z'
+'x,y,z'
+'x+1/2,-y+1/2,-z+1/2'
+'-x,y+1/2,-z'
+'-x+1/2,-y,z+1/2'
+'-x,-y,-z'
+'-x+1/2,y+1/2,z+1/2'
+'x,-y+1/2,z'
+'x+1/2,y,-z+1/2'
+
+_cell_length_a 5.59410
+_cell_length_b 7.88740
+_cell_length_c 5.55850
+_cell_angle_alpha 90.00000
+_cell_angle_beta 90.00000
+_cell_angle_gamma 90.00000
+_cell_volume 245.25731
+_cell_formula_units_Z ?
+_cell_measurement_temperature ?
+_cell_special_details
+; ?
+;
+_cell_measurement_reflns_used ?
+_cell_measurement_theta_min ?
+_cell_measurement_theta_max ?
+
+_exptl_crystal_description ?
+_exptl_crystal_colour ?
+_exptl_crystal_size_max ?
+_exptl_crystal_size_mid ?
+_exptl_crystal_size_min ?
+_exptl_crystal_size_rad ?
+_exptl_crystal_density_diffrn ?
+_exptl_crystal_density_meas ?
+_exptl_crystal_density_method ?
+_exptl_crystal_F_000 ?
+
+# The next four fields are normally only needed for transmission experiments.
+
+_exptl_absorpt_coefficient_mu ?
+_exptl_absorpt_correction_type ?
+_exptl_absorpt_process_details ?
+_exptl_absorpt_correction_T_min ?
+_exptl_absorpt_correction_T_max ?
+
+#=============================================================================
+
+# 7. EXPERIMENTAL DATA
+
+_exptl_special_details
+; ?
+;
+
+_diffrn_ambient_temperature ?
+_diffrn_source 'nuclear reactor'
+_diffrn_radiation_type 'Constant Wavelength Neutron Diffraction'
+_diffrn_radiation_wavelength 1.54056
+_diffrn_source_type ? # Put here the diffractometer and site
+
+_diffrn_radiation_monochromator ?
+_diffrn_measurement_device_type ?
+_diffrn_measurement_method ?
+_diffrn_detector_area_resol_mean ? # Not in version 2.0.1
+_diffrn_detector ?
+_diffrn_detector_type ? # make or model of detector
+
+_diffrn_reflns_number ?
+_diffrn_reflns_av_R_equivalents ?
+_diffrn_reflns_av_sigmaI/netI ?
+_diffrn_reflns_theta_min ?
+_diffrn_reflns_theta_max ?
+_diffrn_reflns_theta_full ? # Not in version 2.0.1
+_diffrn_measured_fraction_theta_max ? # Not in version 2.0.1
+_diffrn_measured_fraction_theta_full ? # Not in version 2.0.1
+_diffrn_reflns_limit_h_min ?
+_diffrn_reflns_limit_h_max ?
+_diffrn_reflns_limit_k_min ?
+_diffrn_reflns_limit_k_max ?
+_diffrn_reflns_limit_l_min ?
+_diffrn_reflns_limit_l_max ?
+_diffrn_reflns_reduction_process ?
+
+_diffrn_standards_number ?
+_diffrn_standards_interval_count ?
+_diffrn_standards_interval_time ?
+_diffrn_standards_decay_% ?
+loop_
+ _diffrn_standard_refln_index_h
+ _diffrn_standard_refln_index_k
+ _diffrn_standard_refln_index_l
+? ? ?
+
+#=============================================================================
+
+# 8. REFINEMENT DATA
+
+_refine_special_details
+; ?
+;
+
+_reflns_number_total ?
+_reflns_number_gt ? # Not in version 2.0.1
+_reflns_threshold_expression ? # Not in version 2.0.1
+
+_refine_ls_structure_factor_coef ?
+_refine_ls_matrix_type ?
+_refine_ls_R_I_factor ?
+_refine_ls_R_Fsqd_factor ?
+_refine_ls_R_factor_all ?
+_refine_ls_R_factor_gt ? # Not in version 2.0.1
+_refine_ls_wR_factor_all ?
+_refine_ls_wR_factor_ref ? # Not in version 2.0.1
+_refine_ls_goodness_of_fit_all ?
+_refine_ls_goodness_of_fit_ref ? # Not in version 2.0.1
+_refine_ls_restrained_S_all ?
+_refine_ls_restrained_S_obs ?
+_refine_ls_number_reflns ?
+_refine_ls_number_parameters ?
+_refine_ls_number_restraints ?
+_refine_ls_number_constraints ?
+_refine_ls_hydrogen_treatment ?
+_refine_ls_weighting_scheme ?
+_refine_ls_weighting_details ?
+_refine_ls_shift/su_max ? # Not in version 2.0.1
+_refine_ls_shift/su_mean ? # Not in version 2.0.1
+_refine_diff_density_max ?
+_refine_diff_density_min ?
+_refine_ls_extinction_method ?
+_refine_ls_extinction_coef ?
+_refine_ls_abs_structure_details ?
+_refine_ls_abs_structure_Flack ?
+_refine_ls_abs_structure_Rogers ?
+
+# The following items are used to identify the programs used.
+
+_computing_data_collection ?
+_computing_cell_refinement ?
+_computing_data_reduction 'DATARED (CFML-v.1)'
+_computing_structure_solution ?
+_computing_structure_refinement FULLPROF
+_computing_molecular_graphics ?
+_computing_publication_material ?
+
+#=============================================================================
+
+# 9. ATOMIC COORDINATES AND DISPLACEMENT PARAMETERS
+
+loop_
+ _atom_site_label
+ _atom_site_fract_x
+ _atom_site_fract_y
+ _atom_site_fract_z
+ _atom_site_U_iso_or_equiv
+ _atom_site_occupancy
+ _atom_site_adp_type # Not in version 2.0.1
+ _atom_site_type_symbol
+ Fe 0.00000 0.00000 0.50000 0.0083(5) 1.00000 Uiso FE
+ La 0.0290(3) 0.25000 -0.0063(7) 0.0108(5) 1.00000 Uiso LA
+ O1 0.4867(5) 0.25000 0.0789(11) 0.0143(7) 1.00000 Uiso O
+ O2 0.2821(4) 0.0397(3) 0.7181(8) 0.0139(5) 1.00000 Uiso O
+
+# Note: if the displacement parameters were refined anisotropically
+# the U matrices should be given as for single-crystal studies.
+
+#=============================================================================
+
+# 10. DISTANCES AND ANGLES / MOLECULAR GEOMETRY
+
+_geom_special_details ?
+
+loop_
+ _geom_bond_atom_site_label_1
+ _geom_bond_atom_site_label_2
+ _geom_bond_site_symmetry_1
+ _geom_bond_site_symmetry_2
+ _geom_bond_distance
+ _geom_bond_publ_flag
+ ? ? ? ? ? ?
+
+loop_
+ _geom_contact_atom_site_label_1
+ _geom_contact_atom_site_label_2
+ _geom_contact_distance
+ _geom_contact_site_symmetry_1
+ _geom_contact_site_symmetry_2
+ _geom_contact_publ_flag
+ ? ? ? ? ? ?
+
+loop_
+_geom_angle_atom_site_label_1
+_geom_angle_atom_site_label_2
+_geom_angle_atom_site_label_3
+_geom_angle_site_symmetry_1
+_geom_angle_site_symmetry_2
+_geom_angle_site_symmetry_3
+_geom_angle
+_geom_angle_publ_flag
+? ? ? ? ? ? ? ?
+
+loop_
+_geom_torsion_atom_site_label_1
+_geom_torsion_atom_site_label_2
+_geom_torsion_atom_site_label_3
+_geom_torsion_atom_site_label_4
+_geom_torsion_site_symmetry_1
+_geom_torsion_site_symmetry_2
+_geom_torsion_site_symmetry_3
+_geom_torsion_site_symmetry_4
+_geom_torsion
+_geom_torsion_publ_flag
+? ? ? ? ? ? ? ? ? ?
+
+loop_
+_geom_hbond_atom_site_label_D
+_geom_hbond_atom_site_label_H
+_geom_hbond_atom_site_label_A
+_geom_hbond_site_symmetry_D
+_geom_hbond_site_symmetry_H
+_geom_hbond_site_symmetry_A
+_geom_hbond_distance_DH
+_geom_hbond_distance_HA
+_geom_hbond_distance_DA
+_geom_hbond_angle_DHA
+_geom_hbond_publ_flag
+? ? ? ? ? ? ? ? ? ? ?
+
+#=============================================================================
+
+#=============================================================================
+# Additional structures (last six sections and associated data_? identifiers)
+# may be added at this point.
+#=============================================================================
+
+# The following lines are used to test the character set of files sent by
+# network email or other means. They are not part of the CIF data set.
+# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
+# !@#$%^&*()_+{}:"~<>?|\-=[];'`,./
diff --git a/test_data/unit_tests/cifs/YFeO3_mcphase.cif b/test_data/unit_tests/cifs/YFeO3_mcphase.cif
new file mode 100644
index 000000000..87d5dbf7e
--- /dev/null
+++ b/test_data/unit_tests/cifs/YFeO3_mcphase.cif
@@ -0,0 +1,46 @@
+data_8700-ICSD
+_database_code_ICSD 8700
+_chemical_name_systematic
+'Yttrium iron(III) oxide'
+_chemical_formula_structural
+'Y Fe O3'
+_chemical_formula_sum
+'Fe1 O3 Y1'
+_citation
+' Acta Crystallographica (1,1948-23,1967) (1965), 19, 524-531 '
+loop_
+ _publ_author_name
+ 'Coppens, P.'
+ 'Eibschuetz, M. '
+_cell_length_a 5.28
+_cell_length_b 5.59
+_cell_length_c 7.6
+_cell_angle_alpha 90.0
+_cell_angle_beta 90.0
+_cell_angle_gamma 90.0
+_cell_volume 224.76
+_cell_formula_units_Z 4.0
+_symmetry_space_group_name_H-M P b n 21
+_symmetry_Int_Tables_number 33
+loop_
+ _symmetry_equiv_pos_as_xyz
+ 'x,y,z'
+ '-x,-y,1/2+z'
+ '1/2-x,1/2+y,z'
+ '1/2+x,1/2-y,1/2+z'
+loop_
+ _atom_site_label
+ _atom_type_oxidation_number
+ _atom_site_symmetry_multiplicity
+ _atom_site_Wyckoff_symbol
+ _atom_site_fract_x
+ _atom_site_fract_y
+ _atom_site_fract_z
+ _atom_site_occupancy
+ _atom_site_B_iso_or_equiv
+ _atom_site_type_symbol
+ Y1 3.0 4 a -.0190 0.0679 0.25 1. 0.04 Y
+ Fe1 3.0 4 a -.0007 0.5012 0.0030 1. 0.12 Fe
+ O1 -2.0 4 a 0.1106 0.4610 0.2510 1. 0.1 O
+ O2 -2.0 4 a -.3077 0.3048 0.0520 1. -.63 O
+ O3 -2.0 4 a 0.3075 -.3066 -.0677 1. 0.43 O
diff --git a/test_data/unit_tests/spinw/spinw_afm_chain.mat b/test_data/unit_tests/spinw/spinw_afm_chain.mat
new file mode 100644
index 000000000..af8be3267
Binary files /dev/null and b/test_data/unit_tests/spinw/spinw_afm_chain.mat differ
diff --git a/test_data/unit_tests/spinw/spinw_default.mat b/test_data/unit_tests/spinw/spinw_default.mat
new file mode 100644
index 000000000..09e4ef594
Binary files /dev/null and b/test_data/unit_tests/spinw/spinw_default.mat differ
diff --git a/test_data/unit_tests/spinw/spinw_from_cif_LaFeO3_fullprof.mat b/test_data/unit_tests/spinw/spinw_from_cif_LaFeO3_fullprof.mat
new file mode 100644
index 000000000..e62076d49
Binary files /dev/null and b/test_data/unit_tests/spinw/spinw_from_cif_LaFeO3_fullprof.mat differ
diff --git a/test_data/unit_tests/spinw/spinw_from_cif_YFeO3_mcphase.mat b/test_data/unit_tests/spinw/spinw_from_cif_YFeO3_mcphase.mat
new file mode 100644
index 000000000..2c95b4aca
Binary files /dev/null and b/test_data/unit_tests/spinw/spinw_from_cif_YFeO3_mcphase.mat differ
diff --git a/test_data/unit_tests/spinw/spinw_from_fst_BiMn2O5.mat b/test_data/unit_tests/spinw/spinw_from_fst_BiMn2O5.mat
new file mode 100644
index 000000000..9a0516ff9
Binary files /dev/null and b/test_data/unit_tests/spinw/spinw_from_fst_BiMn2O5.mat differ
diff --git a/test_data/unit_tests/spinw/spinw_from_struct_lat224.mat b/test_data/unit_tests/spinw/spinw_from_struct_lat224.mat
new file mode 100644
index 000000000..5644ecdd4
Binary files /dev/null and b/test_data/unit_tests/spinw/spinw_from_struct_lat224.mat differ
diff --git a/tutorials/publish/tutorial1.m b/tutorials/publish/tutorial1.m
index 044958847..625558b80 100644
--- a/tutorials/publish/tutorial1.m
+++ b/tutorials/publish/tutorial1.m
@@ -55,6 +55,7 @@
% value is dinamically calculated at every call.
FMchain.energy
+assert(FMchain.energy == -1)
%% Calculate spin wave dispersion and spin-spin correlation function
% We calculate spin wave dispersion and correlation function along the
diff --git a/tutorials/publish/tutorial10.m b/tutorials/publish/tutorial10.m
index 1109f821d..f58ec3d55 100644
--- a/tutorials/publish/tutorial10.m
+++ b/tutorials/publish/tutorial10.m
@@ -77,6 +77,10 @@
% ranges in momentum and energy. Second we call Horace to fill up the empty
% d3d object with the simulated spin wave data and finally we plot a
% constant energy cut.
+if ~exist('sqw','file')
+ fprintf('Horace is not installed. Exiting....\n');
+ return
+end
Ebin = [0,0.01,5];
fwhm0 = 0.1;
diff --git a/tutorials/publish/tutorial12.m b/tutorials/publish/tutorial12.m
index d252397b6..4ac9ce26f 100644
--- a/tutorials/publish/tutorial12.m
+++ b/tutorials/publish/tutorial12.m
@@ -72,8 +72,13 @@
% Horace is installed and setup. Calculate spectra using disp2sqw_eval
% Horace function.
+if ~exist('sqw','file')
+ fprintf('Horace is not installed. Exiting....\n');
+ return
+end
+
horaceObj = d3d(tri.abc,[1 0 0 0],[0,0.005,1],[0 1 0 0],[0,0.005,1],[0 0 0 1],[0,0.1,10]);
-horaceObj = disp2sqw_eval(horaceObj,@tri.horace,{'component','Sperp'},dE);
+horaceObj = disp2sqw_eval(horaceObj,@tri.horace,{'component','Sperp'},dE,'-all');
cut1 = cut(horaceObj,[],[],[3.0 3.5]);
% We use the honest colormap cm_inferno.
@@ -85,4 +90,4 @@
%%
% Written by
% Sandor Toth
-% 16-June-2014
\ No newline at end of file
+% 16-June-2014
diff --git a/tutorials/publish/tutorial12/tutorial12.html b/tutorials/publish/tutorial12/tutorial12.html
index 9d6205a5c..5a464042e 100644
--- a/tutorials/publish/tutorial12/tutorial12.html
+++ b/tutorials/publish/tutorial12/tutorial12.html
@@ -103,7 +103,7 @@
horaceObj = d3d(tri.abc,[1 0 0 0],[0,0.005,1],[0 1 0 0],[0,0.005,1],[0 0 0 1],[0,0.1,10]);
-horaceObj = disp2sqw_eval(horaceObj,@tri.horace,{'component','Sperp'},dE);
+horaceObj = disp2sqw_eval(horaceObj,@tri.horace,{'component','Sperp'},dE,'-all');
cut1 = cut(horaceObj,[],[],[3.0 3.5]);
@@ -192,7 +192,7 @@
% Horace function.
horaceObj = d3d(tri.abc,[1 0 0 0],[0,0.005,1],[0 1 0 0],[0,0.005,1],[0 0 0 1],[0,0.1,10]);
-horaceObj = disp2sqw_eval(horaceObj,@tri.horace,{'component','Sperp'},dE);
+horaceObj = disp2sqw_eval(horaceObj,@tri.horace,{'component','Sperp'},dE,'-all');
cut1 = cut(horaceObj,[],[],[3.0 3.5]);
% We use the honest colormap cm_inferno.
@@ -206,4 +206,4 @@
% Sandor Toth
% 16-June-2014
##### SOURCE END #####
--->