From 89c63fa021f2d56641b7ca82defbe318d6c13034 Mon Sep 17 00:00:00 2001 From: Duc Le Date: Fri, 31 May 2024 16:47:38 +0100 Subject: [PATCH] RELEASE PySpinW v4 (#190) * Fix it so example ipynb works. Update pyspinw CI Uses new matlab2python script from libpymcr to generate wrappers (and so pydoc works on spinw) Remove non-ascii characters in mtimesx which was confusing mcc Update Python __init__.py so initialized check is global Fix issue in addmatrix where Python could input an integer vector value breaking the code. Fix bug in sw_plotspec where handles were unassigned. * Add python command to compile all ctfs * Fix addmatrix test fail * Add wrappers for common Matlab plotting commands * Update change log. Fix addmatrix again. * Change compile ctf CI to use mcc_all.py --- .github/workflows/build_pyspinw.yml | 28 ++++++--- CHANGELOG.md | 3 +- CITATION.cff | 2 +- external/mtimesx/sw_mtimesx.m | 4 +- python/build_ctf.m | 17 ++--- python/mcc_all.py | 11 ++++ python/pyproject.toml | 7 ++- python/pyspinw/__init__.py | 29 ++++++--- python/pyspinw/matlab_plotting.py | 97 +++++++++++++++++++++++++++++ swfiles/@spinw/addmatrix.m | 4 ++ swfiles/sw_plotspec.m | 6 +- 11 files changed, 169 insertions(+), 39 deletions(-) create mode 100644 python/mcc_all.py create mode 100644 python/pyspinw/matlab_plotting.py diff --git a/.github/workflows/build_pyspinw.yml b/.github/workflows/build_pyspinw.yml index d7f2a6a9..6a0169c5 100644 --- a/.github/workflows/build_pyspinw.yml +++ b/.github/workflows/build_pyspinw.yml @@ -54,9 +54,6 @@ jobs: build_ctfs: needs: compile_mex - strategy: - matrix: - matlab_version: [R2021a, R2021b, R2022a, R2022b, R2023a] runs-on: self-hosted steps: - name: Check out SpinW @@ -71,11 +68,12 @@ jobs: - name: Build ctf run: | cd python - /Applications/MATLAB_${{ matrix.matlab_version }}.app/bin/matlab -nodisplay -r "build_ctf; exit" + mkdir ctf + python mcc_all.py - name: Upload CTF results uses: actions/upload-artifact@v4 with: - name: ctf-${{ matrix.matlab_version }} + name: ctf-all path: ${{ github.workspace }}/python/ctf/*.ctf build_wheel: @@ -101,15 +99,27 @@ jobs: 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: Build Wheel - run: | - cd ${{ github.workspace }}/python - python -m pip wheel --no-deps --wheel-dir build . - 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 build . - name: Run python test run: | pip install scipy diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dcd4136..fa1979aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ -# [Unreleased](https://github.com/spinw/spinw/compare/v3.2.0...master) +# [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 diff --git a/CITATION.cff b/CITATION.cff index 18873a3e..ff3961ad 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -17,7 +17,7 @@ authors: - family-names: "Waite" given-names: "Richard" title: "libpymcr" -version: "3.2.0" +version: "4.0.0" date-released: "2023-06-12" license: "GPL-3.0-only" repository: "https://github.com/spinw/spinw" diff --git a/external/mtimesx/sw_mtimesx.m b/external/mtimesx/sw_mtimesx.m index 41d1cd3b..f060fc84 100644 --- a/external/mtimesx/sw_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). % diff --git a/python/build_ctf.m b/python/build_ctf.m index c74b20ec..dfc63f10 100644 --- a/python/build_ctf.m +++ b/python/build_ctf.m @@ -3,14 +3,9 @@ package_name = ['SpinW_', VERSION]; full_package = ['SpinW_', VERSION, '.ctf']; -opts = compiler.build.ProductionServerArchiveOptions( ... - ['matlab', filesep, 'call.m'], ... - 'ArchiveName', package_name, ... - 'OutputDir', out_dir, ... - 'AutoDetectDataFiles', 'on', ... - 'AdditionalFiles', { ... - ['..', filesep, 'swfiles'], ... - ['..', filesep, 'external'], ... - ['..', filesep, 'dat_files']}); - -compiler.build.productionServerArchive(opts); +mcc('-U', '-W', ['CTF:' package_name], ... + '-d', out_dir, ... + 'matlab/call.m', ... + '-a', '../swfiles', ... + '-a', '../external', ... + '-a', '../dat_files') diff --git a/python/mcc_all.py b/python/mcc_all.py new file mode 100644 index 00000000..47794196 --- /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 index 69f65f31..62afb860 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -47,10 +47,11 @@ include = [ { path = "pyspinw/ctfs/*" } ] [tool.poetry.dependencies] python = ">=3.8,<=3.12" -libpymcr = ">=0.1.3" +libpymcr = ">=0.1.7" numpy = "^1.21.4" -pyqt5="~5.15" -vispy=">=0.14.1" +pyqt5 = "~5.15" +vispy = ">=0.14.1" +scipy = ">=1.0.0" # Optional dependencies pytest = {version = ">=7.0.0", optional = true} diff --git a/python/pyspinw/__init__.py b/python/pyspinw/__init__.py index 0b56ccbe..7d98e593 100644 --- a/python/pyspinw/__init__.py +++ b/python/pyspinw/__init__.py @@ -17,6 +17,9 @@ '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): @@ -30,31 +33,39 @@ def __init__(self, matlab_path: Optional[str] = None, matlab_version: Optional[s than 1 MATLAB installation. """ - initialized = False - if matlab_version is None: + global INITIALIZED + global VERSION + if INITIALIZED: + super().__init__(VERSION, mlPath=matlab_path) + elif matlab_version is None: for version in _VERSIONS: - if initialized: + if INITIALIZED: break try: print(f"Trying MATLAB version: {version['version']} ({version['file']}))") super().__init__(version['file'], mlPath=matlab_path) - initialized = True + INITIALIZED = True + VERSION = version['version'] except RuntimeError: continue else: - ctf = [version['file'] for version in _VERSIONS if version['version'].lower() == matlab_version.lower()] + 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, mlPath=matlab_path) - initialized = True + super().__init__(ctf['file'], mlPath=matlab_path) + INITIALIZED = True + VERSION = ctf['version'] except RuntimeError: pass - if not initialized: + if not INITIALIZED: raise RuntimeError( - f"No MATLAB versions found. Please use: [{', '.join([version['version'] for version in _VERSIONS])}]\n " + 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 00000000..2e71a9d2 --- /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/swfiles/@spinw/addmatrix.m b/swfiles/@spinw/addmatrix.m index 3276c1f1..f68ad5d4 100644 --- a/swfiles/@spinw/addmatrix.m +++ b/swfiles/@spinw/addmatrix.m @@ -84,6 +84,10 @@ function addmatrix(obj, varargin) error('spinw:addmatrix:WrongInput','Matrix value has to be numeric or symbolic variable!') end +if isnumeric(newMat.value) + newMat.value = double(newMat.value); +end + if ~isempty(newMat.value) newMat.mat = newMat.value; end diff --git a/swfiles/sw_plotspec.m b/swfiles/sw_plotspec.m index 6a7d54d6..bbc744e5 100644 --- a/swfiles/sw_plotspec.m +++ b/swfiles/sw_plotspec.m @@ -238,7 +238,7 @@ param.mode = 3; param.dashed = true; param.colorbar = false; - [fHandle0, pHandle0] = plotspec_internal(spectra, param); + [fHandle, pHandle] = plotspec_internal(spectra, param); end % Fallthrough - plots dispersion line on top of colormap if applicable if numel(spectra.hklA) ~= length(spectra.hklA) @@ -265,13 +265,13 @@ param.imag = ~pColor & param.imag; param.colormap = cMap0; param.axLim = [0, 1.1*Emax]; - [fHandle0, pHandle0] = plotspec_internal(spectra, param); + [fHandle, pHandle] = plotspec_internal(spectra, param); else error('sw_plotspec:WrongInput', ['Input looks like a powder spectrum but has no '... '''swConv'' field, something has gone wrong somewhere']); end else - [fHandle0, pHandle0] = plotspec_internal(spectra, param); + [fHandle, pHandle] = plotspec_internal(spectra, param); end if ~isfield(spectra,'swConv') && param.mode>1 && param.mode<4