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..e0c4ba7f6 --- /dev/null +++ b/+sw_tests/+unit_tests/unittest_spinw_spinwave.m @@ -0,0 +1,704 @@ +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 + 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..925ce96d7 --- /dev/null +++ b/+sw_tests/+unit_tests/unittest_sw_fitpowder.m @@ -0,0 +1,455 @@ +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', []); + 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(testCase) + out = sw_fitpowder(testCase.swobj, testCase.data_1d_cuts, ... + testCase.fit_func, testCase.j1, "independent"); + out.set_background_strategy("planar"); + expected_fitpow = testCase.default_fitpow; + expected_fitpow.modQ_cens = testCase.default_modQ_cens_1d; + testCase.verify_results(out, expected_fitpow); + 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 = testCase.default_modQ_cens_1d; + 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 = 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_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_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_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.powspec_args.dE = 0.1; % constant energy resolution + out.powspec_args.hermit = true; + out.estimate_scale_factor() + expected_fitpow = testCase.default_fitpow; + expected_fitpow.params(end) = 17.6; + 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 + 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..2374b3567 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,63 @@ +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: 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 @@ -[![DOI](https://zenodo.org/badge/33274418.svg)](https://zenodo.org/badge/latestdoi/33274418) [![Twitter Follow](https://img.shields.io/twitter/follow/spinw4.svg?style=social&label=Follow)](https://twitter.com/intent/user?screen_name=spinw4) [![Github All Releases](https://img.shields.io/github/downloads/tsdev/spinw/total.svg)](https://github.com/tsdev/spinw/releases) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2651100.svg)](https://doi.org/10.5281/zenodo.2651100)[![Twitter Follow](https://img.shields.io/twitter/follow/spinw4.svg?style=social&label=Follow)](https://twitter.com/intent/user?screen_name=spinw4) [![Old Github All Releases](https://img.shields.io/github/downloads/tsdev/spinw/total.svg)](https://github.com/tsdev/spinw/releases)[![Github All Releases](https://img.shields.io/github/downloads/spinw/spinw/total.svg)](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..83075afca 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,91 @@ 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 = (T*)mxGetData(plhs[0]); + T* lhs1 = (T*)mxGetData(plhs[1]); + T* rhs0 = (T*)mxGetData(prhs[0]); + T* irhs0 = (T*)mxGetImagData(prhs[0]); + T* ilhs0 = (T*)mxGetImagData(plhs[0]); + T* ilhs1 = (T*)mxGetImagData(plhs[1]); + 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 +271,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 +315,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..89ca5b732 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 = (T*)mxGetData(plhs[0]); + T* lhs1 = (T*)mxGetData(plhs[1]); + T* ilhs0 = (T*)mxGetImagData(plhs[0]); + T* 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 +784,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 +835,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/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