Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RELEASE v0.1.6 #22

Merged
merged 7 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,20 @@ jobs:
os: [ubuntu-22.04, windows-2022, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache MCR
if: ${{ matrix.os != 'ubuntu-22.04' }}
id: cache-mcr
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: mcr
key: ${{ runner.os }}-matlab-${{ env.MCRVER }}-mcr
lookup-only: true
- name: Cache gists
id: cache-gists
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: gists
key: ${{ runner.os }}-gists
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
os: [ubuntu-22.04, windows-2022, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup variables
Expand Down Expand Up @@ -143,28 +143,27 @@ jobs:
uses: matlab-actions/[email protected]
with:
release: ${{ env.MLVER }}
- uses: actions/cache/restore@v3
- uses: actions/cache/restore@v4
if: ${{ matrix.os != 'ubuntu-22.04' }}
with:
path: mcr
key: ${{ runner.os }}-matlab-${{ env.MCRVER }}-mcr
fail-on-cache-miss: true
- uses: actions/cache/restore@v3
- uses: actions/cache/restore@v4
with:
path: gists
key: ${{ runner.os }}-gists
fail-on-cache-miss: true
- uses: pypa/cibuildwheel@v2.12.0
- uses: pypa/cibuildwheel@v2.17.0
env:
CIBW_BUILD: ${{ env.CIBW }}
CIBW_ENVIRONMENT: >-
MATLABEXECUTABLE="${{ env.MLPREFIX }}${{ env.MATLABEXECUTABLE }}"
HOSTDIRECTORY="${{ env.MLPREFIX }}${{ env.HOSTDIRECTORY }}"
CIBW_BUILD_VERBOSITY: 1
MACOSX_DEPLOYMENT_TARGET: "10.15"
- uses: mamba-org/provision-with-micromamba@main
- uses: mamba-org/setup-micromamba@v1
with:
environment-file: false
cache-downloads: true
- name: Install wheels and run test
run: |
Expand All @@ -176,7 +175,9 @@ jobs:
eval "$($MAMBA_EXE shell activate py$pyver)"
python -m pip install wheelhouse/*cp$(echo $pyver | sed s/\\.//)*
cd test
python run_test.py
# Sometimes the test results in a segfault on exit
python run_test.py || true
test -f success
cd ..
done
- name: Upload release wheels
Expand All @@ -187,7 +188,7 @@ jobs:
#- name: Setup tmate
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}_artifacts.zip
path: |
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# [v0.1.6](https://github.com/pace-neutrons/libpymcr/compare/v0.1.5...v0.1.6)

## Bugfixes

Bugfixes for various user reported issues when used with pace-python and PySpinW.

* Add search of `matlab` executable on path for Linux.
* Add a `type` method to interrogate Matlab type (fixes issue with [pace-python-demo example](https://github.com/pace-neutrons/pace-python-demo/blob/main/demo.py#L86))
* Add a proxy to allow plain struct properties of a class to be manipulated like in Matlab (so `class.prop.subprop = 1` will work if `class.prop` is a plain Matlab `struct`).
* Change to using `evalAsync` in Matlab and as part of this polls the output streams every 1ms and prints output to Python - this allows synchronous output to both console, Jupyter and Spyder without additional code.


# [v0.1.5](https://github.com/pace-neutrons/libpymcr/compare/v0.1.4...v0.1.5)

## Bugfixes for PySpinW
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ authors:
given-names: "Gregory S."
orcid: https://orcid.org/0000-0002-2787-8054
title: "libpymcr"
version: "0.1.5"
date-released: "2023-03-24"
version: "0.1.6"
date-released: "2024-04-26"
license: "GPL-3.0-only"
repository: "https://github.com/pace-neutrons/libpymcr"
url: "https://github.com/pace-neutrons/libpymcr"
Expand Down
6 changes: 6 additions & 0 deletions libpymcr/Matlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ def __getattr__(self, name):
"""
return NamespaceWrapper(self._interface, name)

def type(self, obj):
if hasattr(obj, 'handle'):
return self._interface.call('class', obj.handle, nargout=1)
else:
return str(type(obj))

def get_matlab_functions(self):
"""
Returns a list of public functions in this CTF archive
Expand Down
31 changes: 30 additions & 1 deletion libpymcr/MatlabProxyObject.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ def unwrap(inputs, interface):
else:
return inputs


class DictPropertyWrapper:
# A proxy for dictionary properties of classes to allow Matlab .dot syntax
def __init__(self, val, name, parent):
assert isinstance(val, dict), "DictPropertyWrapper can only wrap dict objects"
self.__dict__['val'] = val
self.__dict__['name'] = name
self.__dict__['parent'] = parent

def __getattr__(self, name):
rv = self.val[name]
if isinstance(rv, dict):
rv = DictPropertyWrapper(rv, name, self)
return rv

def __setattr__(self, name, value):
self.val[name] = value
setattr(self.parent, self.name, self.val)

def __repr__(self):
rv = "Matlab struct with fields:\n"
for k, v in self.val.items():
rv += f" {k}: {v}\n"
return rv


class matlab_method:
def __init__(self, proxy, method):
self.proxy = proxy
Expand Down Expand Up @@ -114,7 +140,10 @@ def __getattr__(self, name):
# if it's a property, just retrieve it
if name in self._getAttributeNames():
try:
return wrap(self.interface.call('subsref', self.handle, {'type':'.', 'subs':name}), self.interface)
rv = wrap(self.interface.call('subsref', self.handle, {'type':'.', 'subs':name}), self.interface)
if isinstance(rv, dict):
rv = DictPropertyWrapper(rv, name, self)
return rv
except TypeError:
return None
# if it's a method, wrap it in a functor
Expand Down
38 changes: 30 additions & 8 deletions libpymcr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import inspect
import dis
import linecache
import shutil
from pathlib import Path


Expand All @@ -22,6 +23,8 @@
"CALL_FUNCTION_EX", "LOAD_METHOD", "CALL_METHOD", "DICT_MERGE", "DICT_UPDATE", "LIST_EXTEND",
}

MLVERDIC = {f'R{rv[0]}{rv[1]}':f'9.{vr}' for rv, vr in zip([[yr, ab] for yr in range(2017,2023) for ab in ['a', 'b']], range(2, 14))}
MLVERDIC.update({'R2023a':'9.14', 'R2023b':'23.2'})

def get_nret_from_dis(frame):
# Tries to get the number of return values for a function
Expand Down Expand Up @@ -164,8 +167,18 @@ def __init__(self, version):
else:
raise RuntimeError(f'Operating system {self.system} is not supported.')

@property
def ver(self):
return self._ver

@ver.setter
def ver(self, val):
self._ver = str(val)
if self._ver.startswith('R') and self._ver in MLVERDIC.keys():
self._ver = MLVERDIC[self._ver]

def find_version(self, root_dir):
print(f'Searching for Matlab in {root_dir}')
print(f'Searching for Matlab {self.ver} in {root_dir}')
def find_file(path, filename, max_depth=3):
""" Finds a file, will return first match"""
for depth in range(max_depth + 1):
Expand Down Expand Up @@ -202,22 +215,29 @@ def guess_path(self, mlPath=[]):
pp = ml_env.split('/')[1:]
ml_env = pp[0] + ':\\' + '\\'.join(pp[1:])
mlPath += [os.path.abspath(os.path.join(ml_env, '..', '..'))]
print(f'mlPath={mlPath}')
for possible_dir in mlPath + GUESSES[self.system]:
if os.path.isdir(possible_dir):
rv = self.find_version(possible_dir)
if rv is not None:
return rv
return None

def guess_from_env(self):
ld_path = os.getenv(self.path_var)
def guess_from_env(self, ld_path=None):
if ld_path is None:
ld_path = os.getenv(self.path_var)
if ld_path is None: return None
for possible_dir in ld_path.split(self.sep):
if os.path.exists(os.path.join(possible_dir, self.file_to_find)):
return os.path.abspath(os.path.join(possible_dir, '..', '..'))
return None

def guess_from_syspath(self):
matlab_exe = shutil.which('matlab')
if matlab_exe is None:
return None if self.system == 'Windows' else self.guess_from_env('PATH')
mlbinpath = os.path.dirname(os.path.realpath(matlab_exe))
return self.find_version(os.path.abspath(os.path.join(mlbinpath, '..')))

def env_not_set(self):
# Determines if the environment variables required by the MCR are set
if self.path_var not in os.environ:
Expand All @@ -242,7 +262,7 @@ def set_environment(self, mlPath=None):
return None


def checkPath(runtime_version, mlPath=None):
def checkPath(runtime_version, mlPath=None, error_if_not_found=True):
"""
Sets the environmental variables for Win, Mac, Linux

Expand All @@ -260,14 +280,16 @@ def checkPath(runtime_version, mlPath=None):
raise FileNotFoundError(f'Input Matlab folder {mlPath} not found')
else:
mlPath = obj.guess_from_env()
if mlPath is None:
mlPath = obj.guess_from_syspath()
if mlPath is None:
mlPath = obj.guess_path()
if mlPath is None:
raise RuntimeError('Cannot find Matlab')
else:
if mlPath is not None:
ld_path = obj.sep.join([os.path.join(mlPath, sub, obj.arch) for sub in obj.required_dirs])
os.environ[obj.path_var] = ld_path
#print('Set ' + os.environ.get(obj.path_var))
elif error_if_not_found:
raise RuntimeError('Cannot find Matlab')
#else:
# print('Found: ' + os.environ.get(obj.path_var))

Expand Down
28 changes: 23 additions & 5 deletions src/libpymcr.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "load_matlab.hpp"
#include "libpymcr.hpp"
#include <chrono>
#include <ratio>
#include <pybind11/stl.h>

namespace libpymcr {
Expand All @@ -24,6 +26,22 @@ namespace libpymcr {
return nargout;
}

template <class T> T matlab_env::evalloop(matlab::cpplib::FutureResult<T> resAsync) {
std::chrono::duration<int, std::milli> period(1);
std::future_status status = resAsync.wait_for(std::chrono::duration<int, std::milli>(1));
while (status != std::future_status::ready) {
status = resAsync.wait_for(period);
// Prints outputs and errors
if(_m_output.get()->in_avail() > 0) {
py::gil_scoped_acquire gil_acquire;
py::print(_m_output.get()->str(), py::arg("flush")=true);
py::gil_scoped_release gil_release;
_m_output.get()->str(std::basic_string<char16_t>());
}
}
return resAsync.get();
}

py::object matlab_env::feval(const std::u16string &funcname, py::args args, py::kwargs& kwargs) {
// Calls Matlab function
const size_t nlhs = 0;
Expand All @@ -38,18 +56,18 @@ namespace libpymcr {
py::gil_scoped_release gil_release;
if (nargout == 1) {
if (m_args.size() == 1) {
outputs.push_back(_lib->feval(funcname, m_args[0], _m_output_buf, _m_error_buf));
outputs.push_back(evalloop(_lib->fevalAsync(funcname, m_args[0], _m_output_buf, _m_error_buf)));
} else {
outputs.push_back(_lib->feval(funcname, m_args, _m_output_buf, _m_error_buf));
outputs.push_back(evalloop(_lib->fevalAsync(funcname, m_args, _m_output_buf, _m_error_buf)));
}
} else {
outputs = _lib->feval(funcname, nargout, m_args, _m_output_buf, _m_error_buf);
outputs = evalloop(_lib->fevalAsync(funcname, nargout, m_args, _m_output_buf, _m_error_buf));
}
// Re-aquire the GIL
py::gil_scoped_acquire gil_acquire;
// Prints outputs and errors
if(_m_output.get()->in_avail() > 0) {
py::print(_m_output.get()->str(), py::arg("flush")=true); }
//if(_m_output.get()->in_avail() > 0) {
// py::print(_m_output.get()->str(), py::arg("flush")=true); }
if(_m_error.get()->in_avail() > 0) {
py::print(_m_error.get()->str(), py::arg("file")=py::module::import("sys").attr("stderr"), py::arg("flush")=true); }
// Converts outputs to Python types
Expand Down
1 change: 1 addition & 0 deletions src/libpymcr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace libpymcr {
std::shared_ptr<StreamBuffer> _m_error_buf = std::static_pointer_cast<StreamBuffer>(_m_error);
pymat_converter _converter;
size_t _parse_inputs(std::vector<matlab::data::Array>& m_args, py::args py_args, py::kwargs& py_kwargs);
template <class T> T evalloop(matlab::cpplib::FutureResult<T> resAsync);
public:
py::object feval(const std::u16string &funcname, py::args args, py::kwargs& kwargs);
py::object call(py::args args, py::kwargs& kwargs);
Expand Down
Loading
Loading