├── .github └── workflows │ ├── python-publish.yml │ └── qualify_25a.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── pyproject.toml ├── python ├── setup.py ├── src └── matlab │ ├── __init__.py │ └── engine │ ├── __init__.py │ ├── _arch.txt │ ├── basefuture.py │ ├── engineerror.py │ ├── enginehelper.py │ ├── enginesession.py │ ├── fevalfuture.py │ ├── futureresult.py │ ├── matlabengine.py │ └── matlabfuture.py └── test └── tInstall.m /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: 3.9 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install setuptools twine 25 | 26 | - name: Build package 27 | run: python setup.py sdist 28 | 29 | - name: Publish package to TestPyPI since the "prerelease" checkbox is checked 30 | if: "github.event.release.prerelease" 31 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.MATLABENGINE_TEST_PYPI_TOKEN }} 35 | repository_url: https://test.pypi.org/legacy/ 36 | 37 | - name: Publish package to PyPI since the "prerelease" checkbox is unchecked 38 | if: "!github.event.release.prerelease" 39 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 40 | with: 41 | user: __token__ 42 | password: ${{ secrets.MATLABENGINE_PYPI_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/qualify_25a.yml: -------------------------------------------------------------------------------- 1 | # Run tInstall on Ubuntu against python versions 3.12, 3.11, 3.10 and 3.9 2 | 3 | name: Test R2025a 4 | 5 | on: 6 | push: 7 | branches: 8 | - R2025a 9 | 10 | pull_request: 11 | branches: 12 | - R2025a 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | 18 | 19 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 20 | jobs: 21 | test-python-engine: 22 | strategy: 23 | matrix: 24 | python: ["3.12", "3.11", "3.10", "3.9"] 25 | 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - name: Set up Python 30 | uses: actions/setup-python@v3.1.3 31 | with: 32 | python-version: ${{ matrix.python }} 33 | 34 | - name: Set up MATLAB 35 | uses: matlab-actions/setup-matlab@v2 36 | with: 37 | release: R2025a 38 | 39 | - uses: actions/checkout@v3 40 | 41 | - name: Run tests 42 | uses: matlab-actions/run-tests@v1 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | *.egg-info 4 | dist 5 | matlabengine-* 6 | matlabengine-test* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, The MathWorks, Inc. 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 5 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 6 | 3. In all cases, the software is, and all modifications and derivatives of the software shall be, licensed to you solely for use in conjunction with MathWorks products and service offerings. 7 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MATLAB Engine API for Python 2 | 3 | The MATLAB® Engine API for Python® provides a package to integrate MATLAB functionality directly with a Python application, creating an interface to call functions from your MATLAB installation from Python code. 4 | 5 | --- 6 | ## Requirements 7 | ### Required MathWorks Products 8 | 9 | * MATLAB release R2025a 10 | 11 | ### Required 3rd Party Products 12 | 13 | * Python 3.9, 3.10, 3.11, or 3.12 14 | * Supported Python versions by MATLAB release can be found [here](https://www.mathworks.com/support/requirements/python-compatibility.html). 15 | 16 | --- 17 | 18 | ## Install 19 | 20 | ### Windows 21 | MATLAB Engine API for Python can be installed directly from the Python Package Index. 22 | 23 | ```bash 24 | $ python -m pip install matlabengine==25.1.2 25 | ``` 26 | 27 | 28 | 29 | ### Linux® 30 | Prior to installation, check the default install location of MATLAB by calling ```matlabroot``` in a MATLAB Command Window. By default, Linux installs MATLAB at:
31 | 32 | ```/usr/local/MATLAB/R2025a``` 33 | 34 | When MATLAB is not installed in the default location, the bin/*architecture* directory within the MATLAB root directory must be added to the environment variable LD_LIBRARY_PATH. The path can be added to the environment variable within the shell startup configuration file (for example, .bashrc for bash shell or .tcshrc for tcsh). 35 | 36 | ```bash 37 | # in .bashrc 38 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/bin/glnxa64 39 | ``` 40 | 41 | ```bash 42 | # in .tcshrc 43 | setenv LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:/bin/glnxa64 44 | ``` 45 | 46 | MATLAB Engine API for Python can be installed directly from the Python Package Index. 47 | 48 | ```bash 49 | $ python -m pip install matlabengine==25.1.2 50 | ``` 51 | 52 | ### macOS 53 | Prior to installation, check the default install location of MATLAB by calling ```matlabroot``` in a MATLAB Command Window. By default, macOS installs MATLAB at:
54 | 55 | 56 | ```/Applications/MATLAB_R2025a.app``` 57 | 58 | When MATLAB is not installed in the default location, the bin/*architecture* directory within the MATLAB root directory must be added to the environment variable DYLD_LIBRARY_PATH. The path can be added to the environment variable within the shell startup configuration file (for example, .bashrc for bash shell or .tcshrc for tcsh). 59 | 60 | ```bash 61 | # in .bashrc 62 | export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/bin/maci64 63 | ``` 64 | 65 | ```bash 66 | # in .tcshrc 67 | setenv DYLD_LIBRARY_PATH ${DYLD_LIBRARY_PATH}:/bin/maci64 68 | ``` 69 | 70 | MATLAB Engine API for Python can be installed directly from the Python Package Index. 71 | 72 | ```bash 73 | $ python -m pip install matlabengine==25.1.2 74 | ``` 75 | 76 | --- 77 | 78 | ## Getting Started 79 | * Start Python. 80 | * Import the ```matlab.engine``` package into the Python session. 81 | * Start a new MATLAB process by calling ```start_matlab```. The ```start_matlab``` function returns a Python object which enables you to pass data and call functions executed by MATLAB. 82 | 83 | ```python 84 | >>> import matlab.engine 85 | >>> eng = matlab.engine.start_matlab() 86 | >>> eng.sqrt(4.0) 87 | 2.0 88 | ``` 89 | 90 | * Call either the ```exit``` or ```quit``` function to stop the engine. Exiting Python with an engine running stops the engine and its MATLAB processes. 91 | 92 | ```python 93 | >>> eng.quit() 94 | ``` 95 | 96 | See [Start and Stop MATLAB Engine for Python](https://www.mathworks.com/help/matlab/matlab_external/start-the-matlab-engine-for-python.html) for advanced startup examples. 97 | 98 | --- 99 | 100 | ## Examples 101 | You can call any MATLAB function directly and return the results to Python. 102 | ```python 103 | >>> eng.plus(2, 3) 104 | 5 105 | >>> eng.isprime(37) 106 | True 107 | >>> eng.gcd(100.0, 80.0, nargout=3) 108 | (20.0, 1.0, -1.0) 109 | ``` 110 | See [Call MATLAB Functions from Python](https://www.mathworks.com/help/matlab/matlab_external/call-matlab-functions-from-python.html) for more usage examples. 111 | 112 | --- 113 | 114 | ## Limitations 115 | Limitations of the MATLAB Engine API for Python can be found [here](https://www.mathworks.com/help/matlab/matlab_external/limitations-to-the-matlab-engine-for-python.html). 116 | 117 | --- 118 | 119 | ## Troubleshooting 120 | See [Troubleshoot MATLAB Errors in Python](https://www.mathworks.com/help/matlab/matlab_external/troubleshoot-matlab-errors-in-python.html) for troubleshooting assistance. 121 | 122 | --- 123 | 124 | ## License 125 | The license is available in the LICENSE.txt file within this repository. 126 | 127 | --- 128 | 129 | ## Support 130 | Technical issues or enhancement requests can be submitted [here](https://github.com/mathworks/matlab-engine-for-python/issues). 131 | 132 | --- 133 | 134 | Copyright © 2022 MathWorks, Inc. All rights reserved. 135 | 136 | Linux® is the registered trademark of Linus Torvalds in the U.S. and other countries. 137 | 138 | Mac OS is a trademark of Apple Inc., registered in the U.S. and other countries. 139 | 140 | "Python" and the Python logos are trademarks or registered trademarks of the Python Software Foundation, used by MathWorks with permission from the Foundation. 141 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | If you believe you have discovered a security vulnerability, please report it to 4 | [security@mathworks.com](mailto:security@mathworks.com). Please see 5 | [MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html) 6 | for additional information. 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 42", "wheel"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /python: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-engine-for-python/65cb554db27232c380005edc3e085015e607cf8b/python -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Mathworks, Inc. 2 | 3 | from setuptools import setup, find_packages 4 | from setuptools.command.build_py import build_py 5 | import os 6 | import re 7 | import sys 8 | import platform 9 | import xml.etree.ElementTree as xml 10 | if platform.system() == 'Windows': 11 | import winreg 12 | 13 | class _MatlabFinder(build_py): 14 | """ 15 | Private class that finds MATLAB on user's computer prior to package installation. 16 | """ 17 | PLATFORM_DICT = { 18 | 'Windows': 'PATH', 19 | 'Linux': 'LD_LIBRARY_PATH', 20 | 'Darwin': 'DYLD_LIBRARY_PATH' 21 | } 22 | 23 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 24 | MATLAB_REL = 'R2025a' 25 | 26 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 27 | MATLAB_VER = '25.1.2' 28 | 29 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 30 | SUPPORTED_PYTHON_VERSIONS = set(['3.9', '3.10', '3.11', '3.12']) 31 | 32 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 33 | VER_TO_REL = { 34 | "9.9": "R2020b", 35 | "9.10": "R2021a", 36 | "9.11": "R2021b", 37 | "9.12": "R2022a", 38 | "9.13": "R2022b", 39 | "9.14": "R2023a", 40 | "23.2": "R2023b", 41 | "24.1": "R2024a", 42 | "24.2": "R2024b", 43 | "25.1": "R2025a" 44 | } 45 | 46 | DEFAULT_INSTALLS = { 47 | 'Darwin': f"/Applications/MATLAB_{MATLAB_REL}.app", 48 | 'Linux': f"/usr/local/MATLAB/{MATLAB_REL}" 49 | } 50 | 51 | arch = '' 52 | path_env_var_name = '' 53 | python_ver = '' 54 | platform = '' 55 | found_matlab_release = '' 56 | found_matlab_version = '' 57 | found_matlab_with_wrong_arch_in_default_install = '' 58 | found_matlab_with_wrong_arch_in_path = '' 59 | verbose = False 60 | 61 | # ERROR MESSAGES 62 | minimum_maximum = "No compatible version of MATLAB was found. " + \ 63 | "Version {this_v:s} was found, but this feature only supports MATLAB {min_v:s} ({min_r:s}) through {max_v:s} ({max_r:s}), inclusive." 64 | dir_not_found = "Directory not found: " 65 | no_windows_install = "MATLAB installation not found in Windows Registry:" 66 | unsupported_platform = "{platform:s} is not a supported platform." 67 | unsupported_python = "{python:s} is not supported. The supported Python versions are {supported:s}." 68 | unset_env = "Environment variable {path1:s} has not been set. Add /bin/{arch:s} to {path2:s}, where is the root of a valid MATLAB installation." 69 | install_or_set_path = "MATLAB {ver:s} installation not found. Install to default location, or add /bin/{arch:s} to {path:s}, where is the root of a MATLAB {ver:s} installation." 70 | no_compatible_matlab = "No compatible MATLAB installation found in Windows Registry. This release of " + \ 71 | "MATLAB Engine API for Python is compatible with version {ver:s}. The found versions were" 72 | no_matlab = "No compatible MATLAB installation found in Windows Registry." 73 | incompatible_ver = "MATLAB version {ver:s} ({rel:s}) was found, but this release of MATLAB Engine API for Python is not compatible with it. " + \ 74 | "To install a compatible version, call 'python -m pip install matlabengine=={ver:s}'." 75 | invalid_version_from_matlab_ver = "Format of MATLAB version '{ver:s}' is invalid." 76 | invalid_version_from_eng = "Format of MATLAB Engine API version '{ver:s}' is invalid." 77 | next_steps = "Reinstall MATLAB, use DYLD_LIBRARY_PATH to specify a different MATLAB installation, or use a different Python interpreter." 78 | wrong_arch_in_default_install = "MATLAB installation in {path1:s} is {matlab_arch:s}, but Python interpreter is {python_arch:s}. {next_steps:s}." 79 | wrong_arch_in_path = "MATLAB installation in {path1:s}, listed in DYLD_LIBRARY_PATH, is {matlab_arch:s}, but Python interpreter is {python_arch:s}. {next_steps:s}." 80 | 81 | def _print_if_verbose(self, msg): 82 | if self.verbose: 83 | print(msg) 84 | 85 | def set_platform_and_arch(self): 86 | """ 87 | Sets the platform and architecture. 88 | """ 89 | self.platform = platform.system() 90 | if self.platform not in self.PLATFORM_DICT: 91 | raise RuntimeError(self.unsupported_platform.format(platform=self.platform)) 92 | else: 93 | self.path_env_var_name = self.PLATFORM_DICT[self.platform] 94 | 95 | if self.platform == 'Windows': 96 | self.arch = 'win64' 97 | elif self.platform == 'Linux': 98 | self.arch = 'glnxa64' 99 | elif self.platform == 'Darwin': 100 | if platform.mac_ver()[-1] == 'arm64': 101 | self.arch = 'maca64' 102 | else: 103 | self.arch = 'maci64' 104 | else: 105 | raise RuntimeError(self.unsupported_platform.format(platform=self.platform)) 106 | 107 | def set_python_version(self): 108 | """ 109 | Gets Python version and ensures it is supported. 110 | """ 111 | ver = sys.version_info 112 | self.python_ver = f"{ver.major}.{ver.minor}" 113 | 114 | if self.python_ver not in self.SUPPORTED_PYTHON_VERSIONS: 115 | raise RuntimeError(self.unsupported_python.format(python=self.python_ver, supported=str(self.SUPPORTED_PYTHON_VERSIONS))) 116 | 117 | def unix_default_install_exists(self): 118 | """ 119 | Determines whether MATLAB is installed in default UNIX location. 120 | """ 121 | path = self.DEFAULT_INSTALLS[self.platform] 122 | if not os.path.exists(path): 123 | return False 124 | 125 | if self.platform == 'Darwin': 126 | # On Mac, we need to further verify that there is a 'bin/maci64' subdir if the Python is maci64 127 | # or a 'bin/maca64' subdir if the Python is maca64. 128 | path_to_bin = os.path.join(path, 'bin', self.arch) 129 | if os.path.exists(path_to_bin): 130 | # The path exists, and we don't need to do anything further. 131 | return True 132 | 133 | if self.arch == 'maci64': 134 | alternate_arch = 'maca64' 135 | else: 136 | alternate_arch = 'maci64' 137 | 138 | if os.path.exists(os.path.join(path, 'bin', alternate_arch)): 139 | # There is a default install, but its arch doesn't match the Python arch. Save this info 140 | # so that if we don't find an install with a valid arch in DYLD_LIBRARY_PATH, we can 141 | # issue an error message that says that there is a Mac installation in the default 142 | # location that has the wrong arch. The user can choose whether to change the 143 | # Python interpreter or the MATLAB installation so that the arch will match. 144 | self.found_matlab_with_wrong_arch_in_default_install = path 145 | return False 146 | 147 | return True 148 | 149 | def _create_path_list(self): 150 | """ 151 | Creates a list of directories on the path to be searched. 152 | """ 153 | path_dirs = [] 154 | path_string = '' 155 | if self.path_env_var_name in os.environ: 156 | path_string = os.environ[self.path_env_var_name] 157 | path_dirs.extend(path_string.split(os.pathsep)) 158 | 159 | if not path_dirs: 160 | raise RuntimeError(self.install_or_set_path.format( 161 | ver=self.MATLAB_REL, arch=self.arch, 162 | path=self.path_env_var_name)) 163 | 164 | self._print_if_verbose(f'_create_path_list returned: {path_dirs}') 165 | return path_dirs 166 | 167 | def _get_alternate_arch(self): 168 | if self.arch == 'maci64': 169 | return 'maca64' 170 | if self.arch == 'maca64': 171 | return 'maci64' 172 | return self.arch 173 | 174 | def _arch_in_mac_dir_is_correct(self, dir): 175 | ARCH_LEN = 6 # == len('maci64') or len('maca64') 176 | BIN_ARCH_LEN = ARCH_LEN + 4 # == len('bin/maci64') or len('bin/maca64') 177 | 178 | if len(dir) < BIN_ARCH_LEN: 179 | return False 180 | 181 | if dir[-1] == os.sep: 182 | # It's safe to look at dir[[-1 * (ARCH_LEN+1)] because BIN_ARCH_LEN > ARCH_LEN + 1. 183 | possible_arch = dir[-1 * (ARCH_LEN+1) : -1] 184 | else: 185 | possible_arch = dir[-1 * ARCH_LEN :] 186 | 187 | self._print_if_verbose(f'possible_arch: {possible_arch}; self.arch: {self.arch}') 188 | if possible_arch == self.arch: 189 | return True 190 | else: 191 | return False 192 | 193 | def _get_matlab_root_from_unix_bin(self, dir): 194 | """ 195 | Searches bin directory for presence of MATLAB file. Used only for 196 | UNIX systems. 197 | """ 198 | matlab_path = os.path.join(dir, 'MATLAB') 199 | possible_root = os.path.normpath(os.path.join(dir, os.pardir, os.pardir)) 200 | matlab_root = '' 201 | if os.path.isfile(matlab_path) and self.verify_matlab_release(possible_root): 202 | if self.platform == 'Darwin' and not self._arch_in_mac_dir_is_correct(dir): 203 | self.found_matlab_with_wrong_arch_in_path = possible_root 204 | self._print_if_verbose(f'self.found_matlab_with_wrong_arch_in_path: {self.found_matlab_with_wrong_arch_in_path}') 205 | else: 206 | matlab_root = possible_root 207 | self._print_if_verbose(f'_get_matlab_root_from_unix_bin returned: {matlab_root}') 208 | return matlab_root 209 | 210 | def get_matlab_root_from_windows_reg(self): 211 | """ 212 | Searches Windows Registry for MATLAB installs and gets the root directory of MATLAB. 213 | """ 214 | try: 215 | reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 216 | key = winreg.OpenKey(reg, "SOFTWARE\\MathWorks\\MATLAB") 217 | except OSError as err: 218 | raise RuntimeError(f"{self.no_windows_install} {err}") 219 | 220 | matlab_ver_key = self._find_matlab_key_from_windows_registry(key) 221 | ret = self._get_root_from_version_key(reg, matlab_ver_key) 222 | self._print_if_verbose(f'get_matlab_root_from_windows_reg returned: {ret}') 223 | return ret 224 | 225 | def _get_root_from_version_key(self, reg, ver_key): 226 | """ 227 | Opens a registry corresponding to the version of MATLAB specified and queries for 228 | MATLABROOT. 229 | """ 230 | try: 231 | key = winreg.OpenKey(reg, "SOFTWARE\\MathWorks\\MATLAB\\" + ver_key) 232 | matlab_root = winreg.QueryValueEx(key, "MATLABROOT")[0] 233 | except (OSError, FileNotFoundError) as err: 234 | raise RuntimeError(f"{self.no_windows_install} {err}") 235 | 236 | self._print_if_verbose(f'_get_root_from_version_key returned: {matlab_root}') 237 | return matlab_root 238 | 239 | def _find_matlab_key_from_windows_registry(self, key): 240 | """ 241 | Searches the MATLAB folder in Windows registry for the specified version of MATLAB. When found, 242 | the MATLAB root directory will be returned. 243 | """ 244 | # QueryInfoKey returns a tuple, index 0 is the number of sub keys we need to search 245 | num_keys = winreg.QueryInfoKey(key)[0] 246 | key_value = '' 247 | found_vers = [] 248 | for idx in range(num_keys): 249 | sub_key = winreg.EnumKey(key, idx) 250 | if sub_key in self.VER_TO_REL: 251 | found_vers.append(sub_key) 252 | # Example: the version in the registry could be "9.X" whereas the version in this file could be "9.X.Y". 253 | # We want to allow this. 254 | if self._check_matlab_ver_against_engine(sub_key): 255 | key_value = sub_key 256 | break 257 | 258 | if not key_value: 259 | if found_vers: 260 | vers = ', '.join(found_vers) 261 | eng_ver_major_minor = self._get_engine_ver_major_minor(self.MATLAB_VER) 262 | eng_ver_major_minor_as_str = '{}.{}'.format(eng_ver_major_minor[0], eng_ver_major_minor[1]) 263 | raise RuntimeError(f"{self.no_compatible_matlab.format(ver=eng_ver_major_minor_as_str)} {vers}.") 264 | else: 265 | raise RuntimeError(f"{self.no_matlab}") 266 | 267 | self._print_if_verbose(f'_find_matlab_key_from_windows_registry returned: {key_value}') 268 | return key_value 269 | 270 | def _get_engine_ver_major_minor(self, id): 271 | re_major_minor = "^(\d+)\.(\d+)" 272 | eng_match = re.match(re_major_minor, id) 273 | if not eng_match: 274 | raise RuntimeError(f"{self.invalid_version_from_eng.format(ver=self.MATLAB_VER)}") 275 | ret = (eng_match.group(1), eng_match.group(2)) 276 | self._print_if_verbose(f'_get_engine_ver_major_minor returned: {ret}') 277 | return ret 278 | 279 | def _check_matlab_ver_against_engine(self, matlab_ver): 280 | re_major_minor = "^(\d+)\.(\d+)" 281 | matlab_ver_match = re.match(re_major_minor, matlab_ver) 282 | if not matlab_ver_match: 283 | raise RuntimeError(f"{self.invalid_version_from_matlab_ver.format(ver=matlab_ver)}") 284 | eng_major_minor = self._get_engine_ver_major_minor(self.MATLAB_VER) 285 | matlab_ver_major_minor = (matlab_ver_match.group(1), matlab_ver_match.group(2)) 286 | return (matlab_ver_major_minor == eng_major_minor) 287 | 288 | def verify_matlab_release(self, root): 289 | """ 290 | Parses VersionInfo.xml to verify that the MATLAB release matches the supported release 291 | for the Python Engine. 292 | """ 293 | version_info = os.path.join(root, 'VersionInfo.xml') 294 | if not os.path.isfile(version_info): 295 | return False 296 | 297 | tree = xml.parse(version_info) 298 | tree_root = tree.getroot() 299 | 300 | matlab_release = '' 301 | for child in tree_root: 302 | if child.tag == 'release': 303 | matlab_release = self.found_matlab_release = child.text 304 | elif child.tag == 'version': 305 | major, minor = self._get_engine_ver_major_minor(child.text) 306 | self.found_matlab_version = f'{major}.{minor}' 307 | return matlab_release == self.MATLAB_REL 308 | 309 | def search_path_for_directory_unix(self, arch, path_dirs): 310 | """ 311 | Used for finding MATLAB root in UNIX systems. Searches all paths ending in 312 | /bin/ for the presence of MATLAB file to ensure the path is within 313 | the MATLAB tree. 314 | """ 315 | dir_to_find = os.path.join('bin', arch) 316 | # directory could end with slashes 317 | endings = [dir_to_find, dir_to_find + os.sep] 318 | 319 | matlab_root = '' 320 | dir_idx = 0 321 | while not matlab_root and dir_idx < len(path_dirs): 322 | path = path_dirs[dir_idx] 323 | ending_idx = 0 324 | while not matlab_root and ending_idx < len(endings): 325 | ending = endings[ending_idx] 326 | if path.endswith(ending): 327 | # _get_matlab_root_from_unix_bin will return an empty string if MATLAB is not found. 328 | # Non-empty string (MATLAB found) will break both loops. 329 | matlab_root = self._get_matlab_root_from_unix_bin(path) 330 | ending_idx += 1 331 | dir_idx += 1 332 | self._print_if_verbose(f'search_path_for_directory_unix returned: {matlab_root}') 333 | return matlab_root 334 | 335 | def _err_msg_if_bad_matlab_root(self, matlab_root): 336 | if not matlab_root: 337 | if self.found_matlab_version: 338 | self._print_if_verbose(f'self.found_matlab_version: {self.found_matlab_version}; self.VER_TO_REL: {self.VER_TO_REL}') 339 | if self.found_matlab_version in self.VER_TO_REL: 340 | return self.incompatible_ver.format(ver=self.found_matlab_version, rel=self.found_matlab_release) 341 | # Found a MATLAB release but it is older than the oldest version supported, 342 | # or newer than the newest version supported. 343 | else: 344 | v_to_r_keys = list(self.VER_TO_REL.keys()) 345 | self._print_if_verbose(f'v_to_r_keys: {v_to_r_keys}') 346 | min_v = v_to_r_keys[0] 347 | min_r = self.VER_TO_REL[min_v] 348 | max_v = v_to_r_keys[-1] 349 | max_r = self.VER_TO_REL[max_v] 350 | return self.minimum_maximum.format(this_v=self.found_matlab_release, min_v=min_v, min_r=min_r, max_v=max_v, max_r=max_r) 351 | else: 352 | # If we reach this line, we assume that the default location has already been checked for an 353 | # appropriate MATLAB installation but none was found. 354 | return self.install_or_set_path.format(ver=self.MATLAB_REL, arch=self.arch, 355 | path=self.path_env_var_name) 356 | 357 | if not os.path.isdir(matlab_root): 358 | return f"{self.dir_not_found} {matlab_root}" 359 | 360 | return '' 361 | 362 | def write_text_file(self, matlab_root): 363 | """ 364 | Writes root.txt for use at import time. 365 | """ 366 | file_location = os.path.join(os.getcwd(), 'src', 'matlab', 'engine', '_arch.txt') 367 | bin_arch = os.path.join(matlab_root, 'bin', self.arch) 368 | engine_arch = os.path.join(matlab_root, 'extern', 'engines', 'python', 'dist', 'matlab', 'engine', self.arch) 369 | extern_bin = os.path.join(matlab_root, 'extern', 'bin', self.arch) 370 | with open(file_location, 'w') as root_file: 371 | root_file.write(self.arch + '\n') 372 | root_file.write(bin_arch + '\n') 373 | root_file.write(engine_arch + '\n') 374 | root_file.write(extern_bin) 375 | 376 | def run(self): 377 | """ 378 | Logic that runs prior to installation. 379 | """ 380 | self.set_platform_and_arch() 381 | self.set_python_version() 382 | 383 | if self.platform == 'Windows': 384 | matlab_root = self.get_matlab_root_from_windows_reg() 385 | else: 386 | if self.unix_default_install_exists(): 387 | matlab_root = self.DEFAULT_INSTALLS[self.platform] 388 | else: 389 | path_dirs = self._create_path_list() 390 | matlab_root = self.search_path_for_directory_unix(self.arch, path_dirs) 391 | err_msg = self._err_msg_if_bad_matlab_root(matlab_root) 392 | if err_msg: 393 | if self.platform == 'Darwin': 394 | if self.found_matlab_with_wrong_arch_in_default_install: 395 | raise RuntimeError( 396 | self.wrong_arch_in_default_install.format( 397 | path1=self.found_matlab_with_wrong_arch_in_default_install, 398 | matlab_arch=self._get_alternate_arch(), 399 | python_arch=self.arch, 400 | next_steps=self.next_steps)) 401 | if self.found_matlab_with_wrong_arch_in_path: 402 | raise RuntimeError( 403 | self.wrong_arch_in_path.format( 404 | path1=self.found_matlab_with_wrong_arch_in_path, 405 | matlab_arch=self._get_alternate_arch(), 406 | python_arch=self.arch, 407 | next_steps=self.next_steps)) 408 | raise RuntimeError(err_msg) 409 | 410 | self.write_text_file(matlab_root) 411 | build_py.run(self) 412 | 413 | 414 | if __name__ == '__main__': 415 | with open('README.md', 'r', encoding='utf-8') as rm: 416 | long_description = rm.read() 417 | 418 | setup( 419 | name="matlabengine", 420 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 421 | version="25.1.2", 422 | description='A module to call MATLAB from Python', 423 | author='MathWorks', 424 | license="LICENSE.txt, located in this repository", 425 | url='https://github.com/mathworks/matlab-engine-for-python/', 426 | long_description=long_description, 427 | long_description_content_type="text/markdown", 428 | package_dir={'': 'src'}, 429 | packages=find_packages(where="src"), 430 | cmdclass={'build_py': _MatlabFinder}, 431 | package_data={'': ['_arch.txt']}, 432 | zip_safe=False, 433 | project_urls={ 434 | 'Documentation': 'https://www.mathworks.com/help/matlab/matlab-engine-for-python.html', 435 | 'Source': 'https://github.com/mathworks/matlab-engine-for-python', 436 | 'Tracker': 'https://github.com/mathworks/matlab-engine-for-python/issues' 437 | }, 438 | keywords=[ 439 | "MATLAB", 440 | "MATLAB Engine for Python" 441 | ], 442 | classifiers=[ 443 | "Natural Language :: English", 444 | "Intended Audience :: Developers", 445 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 446 | "Programming Language :: Python :: 3.9", 447 | "Programming Language :: Python :: 3.10", 448 | "Programming Language :: Python :: 3.11", 449 | "Programming Language :: Python :: 3.12" 450 | ], 451 | # MUST_BE_UPDATED_EACH_RELEASE (Search repo for this string) 452 | python_requires=">=3.9, <3.13" 453 | ) 454 | -------------------------------------------------------------------------------- /src/matlab/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 MathWorks, Inc. 2 | 3 | import os 4 | import platform 5 | import sys 6 | import pkgutil 7 | 8 | __path__ = pkgutil.extend_path(__path__, __name__) 9 | package_folder = os.path.dirname(os.path.realpath(__file__)) 10 | sys.path.append(package_folder) 11 | 12 | def add_dirs_to_path(bin_dir, engine_dir, extern_dir): 13 | """ 14 | Adds MATLAB engine and extern/bin directories to sys.path. 15 | """ 16 | path = 'PATH' 17 | 18 | if not os.path.isdir(engine_dir): 19 | raise RuntimeError("Could not find directory: {0}".format(engine_dir)) 20 | 21 | if not os.path.isdir(extern_dir): 22 | raise RuntimeError("Could not find directory: {0}".format(extern_dir)) 23 | 24 | if platform.system() == 'Windows': 25 | if not os.path.isdir(bin_dir): 26 | raise RuntimeError("Could not find directory: {0}".format(bin_dir)) 27 | if path in os.environ: 28 | paths = os.environ[path] 29 | os.environ[path] = bin_dir + os.pathsep + paths 30 | else: 31 | os.environ[path] = bin_dir 32 | if sys.version_info.major >= 3 and sys.version_info.minor >= 8: 33 | os.add_dll_directory(bin_dir) 34 | 35 | sys.path.insert(0, engine_dir) 36 | sys.path.insert(0, extern_dir) 37 | 38 | arch_file = os.path.join(package_folder, 'engine', '_arch.txt') 39 | if not os.path.isfile(arch_file): 40 | raise RuntimeError("The MATLAB Engine for Python install is corrupted. Please try to re-install.") 41 | 42 | with open(arch_file, 'r') as root: 43 | [arch, bin_folder, engine_folder, extern_bin] = [line.strip() for line in root.readlines()] 44 | 45 | 46 | add_dirs_to_path(bin_folder, engine_folder, extern_bin) 47 | 48 | from matlabmultidimarrayforpython import double, single, uint8, int8, uint16, \ 49 | int16, uint32, int32, uint64, int64, logical, ShapeError, SizeError 50 | -------------------------------------------------------------------------------- /src/matlab/engine/__init__.py: -------------------------------------------------------------------------------- 1 | #Copyright 2014-2024 MathWorks, Inc. 2 | 3 | """ 4 | The MATLAB Engine enables you to call any MATLAB statement either synchronously 5 | or asynchronously. With synchronous execution, the invocation of a MATLAB 6 | statement returns the result after the call finishes. With asynchronous 7 | execution, the invocation of a MATLAB statement is performed in the background 8 | and a FutureResult object is returned immediately. You can call its "done" 9 | function to check if the call has finished, and its "result" function to obtain 10 | the actual result of the MATLAB statement. 11 | 12 | This example shows how to call a MATLAB function: 13 | 14 | >>> import matlab.engine 15 | >>> eng = matlab.engine.start_matlab() 16 | >>> eng.sqrt(4.0) 17 | 2.0 18 | >>> eng.exit() 19 | """ 20 | 21 | 22 | import os 23 | import sys 24 | import importlib 25 | import atexit 26 | import weakref 27 | import threading 28 | import warnings 29 | 30 | # UPDATE_IF_PYTHON_VERSION_ADDED_OR_REMOVED : search for this string in codebase 31 | # when support for a Python version must be added or removed 32 | 33 | _supported_versions = ['3_9', '3_10', '3_11', '3_12'] 34 | _ver = sys.version_info 35 | _version = '{0}_{1}'.format(_ver[0], _ver[1]) 36 | _newer_than_supported = _ver[1] > 12 37 | 38 | _PYTHONVERSION = None 39 | 40 | if _version in _supported_versions: 41 | _PYTHONVERSION = _version 42 | elif _newer_than_supported: 43 | warnings.warn('MATLAB Engine for Python supports Python version' 44 | ' 3.9, 3.10, 3.11, and 3.12, but your version of Python ' 45 | 'is %s' % _version) 46 | _PYTHONVERSION = _version 47 | else: 48 | raise EnvironmentError('MATLAB Engine for Python supports Python version' 49 | ' 3.9, 3.10, 3.11, and 3.12, but your version of Python ' 50 | 'is %s' % _version) 51 | 52 | 53 | _module_folder = os.path.dirname(os.path.realpath(__file__)) 54 | _arch_filename = os.path.join(_module_folder, "_arch.txt") 55 | success = False 56 | firstExceptionMessage = '' 57 | secondExceptionMessage = '' 58 | try: 59 | if _PYTHONVERSION != '3_9' and _PYTHONVERSION != '3_10': 60 | pythonengine = importlib.import_module("matlabengineforpython_abi3") 61 | else: 62 | pythonengine = importlib.import_module("matlabengineforpython" + _PYTHONVERSION) 63 | except Exception as firstE: 64 | firstExceptionMessage = str(firstE) 65 | 66 | if firstExceptionMessage: 67 | try: 68 | _arch_file = open(_arch_filename,'r') 69 | _lines = _arch_file.readlines() 70 | [_arch, _bin_dir,_engine_dir, _extern_bin_dir] = [x.rstrip() for x in _lines if x.rstrip() != ""] 71 | _arch_file.close() 72 | sys.path.insert(0,_engine_dir) 73 | sys.path.insert(0,_extern_bin_dir) 74 | 75 | _envs = {'win32': 'PATH', 'win64': 'PATH'} 76 | if _arch in _envs: 77 | if _envs[_arch] in os.environ: 78 | _env = os.environ[_envs[_arch]] 79 | os.environ[_envs[_arch]] = _bin_dir + os.pathsep + os.environ[_envs[_arch]] 80 | else: 81 | os.environ[_envs[_arch]] = _bin_dir 82 | os.add_dll_directory(_bin_dir) 83 | if _PYTHONVERSION != '3_9' or _PYTHONVERSION != '3_10': 84 | pythonengine = importlib.import_module("matlabengineforpython_abi3") 85 | else: 86 | pythonengine = importlib.import_module("matlabengineforpython" + _PYTHONVERSION) 87 | except Exception as secondE: 88 | str1 = 'Please reinstall MATLAB Engine for Python or contact ' 89 | str2 = 'MathWorks Technical Support for assistance:\nFirst issue: {}\nSecond issue: {}'.format( 90 | firstExceptionMessage, secondE) 91 | secondExceptionMessage = str1 + str2 92 | 93 | if secondExceptionMessage: 94 | raise EnvironmentError(secondExceptionMessage) 95 | 96 | 97 | """ 98 | This lock can make sure the global variable _engines is updated correctly in 99 | multi-thread use case. Also, it guarantees that only one MATLAB is launched 100 | when connect_matlab() is called if there is no shared MATLAB session. 101 | """ 102 | _engine_lock = threading.RLock() 103 | _engines = [] 104 | 105 | from matlab.engine.engineerror import RejectedExecutionError 106 | from matlab.engine.basefuture import BaseFuture 107 | from matlab.engine.matlabfuture import MatlabFuture 108 | from matlab.engine.fevalfuture import FevalFuture 109 | from matlab.engine.futureresult import FutureResult 110 | from matlab.engine.enginesession import EngineSession 111 | from matlab.engine.matlabengine import MatlabEngine 112 | from matlab.engine.matlabengine import enginehelper 113 | 114 | _session = EngineSession() 115 | 116 | def start_matlab(option="-nodesktop", **kwargs): 117 | """ 118 | Start the MATLAB Engine. This function creates an instance of the 119 | MatlabEngine class. The local version of MATLAB will be launched 120 | with the "-nodesktop" argument. 121 | 122 | Please note the invocation of this function is synchronous, which 123 | means it only returns after MATLAB launches. 124 | 125 | Parameters 126 | option - MATLAB startup option. 127 | background: bool - start MATLAB asynchronously or not. This parameter 128 | is optional and false by default. 129 | 130 | Returns 131 | MatlabEngine - if aync or background is false. This object can be used to evaluate 132 | MATLAB statements. 133 | FutureResult - if async or background is true. This object can be used to obtain the 134 | real MatlabEngine instance. 135 | 136 | Raises 137 | EngineError - if MATLAB can't be started. 138 | """ 139 | if not isinstance(option, str): 140 | raise TypeError(pythonengine.getMessage('StartupOptionShouldBeStr')) 141 | 142 | background = enginehelper._get_async_or_background_argument(kwargs) 143 | future = FutureResult(option=option) 144 | if not background: 145 | #multi-threads cannot launch MATLAB simultaneously 146 | eng = future.result() 147 | return eng 148 | else: 149 | return future 150 | 151 | def find_matlab(): 152 | """ 153 | Discover all shared MATLAB sessions on the local machine. This function 154 | returns the names of all shared MATLAB sessions. 155 | 156 | Returns 157 | tuple - the names of all shared MATLAB sessions running locally. 158 | """ 159 | engines = pythonengine.findMATLAB() 160 | return engines 161 | 162 | def connect_matlab(name=None, **kwargs): 163 | """ 164 | Connect to a shared MATLAB session. This function creates an instance 165 | of the MatlabEngine class and connects it to a MATLAB session. The MATLAB 166 | session must be a shared session on the local machine. 167 | 168 | If name is not specified and there is no shared MATLAB available, this 169 | function launches a shared MATLAB session with default options. If name 170 | is not specified and there are shared MATLAB sessions available, the first 171 | shared MATLAB created is connected. If name is specified and there are no 172 | shared MATLAB sessions with that name, an exception is raised. 173 | 174 | Parameters 175 | name: str - the name of the shared MATLAB session, which is optional. 176 | By default it is None. 177 | async, background: bool - connect to the shared MATLAB session asynchronously or 178 | not. This is optional and false by default. "async" is a synonym for 179 | "background" that will be removed in a future release. 180 | 181 | Returns 182 | MatlabEngine - if async or background is false. This object can be used to evaluate 183 | MATLAB functions. 184 | FutureResult - if async or background is true. This object can be used to obtain the 185 | real MatlabEngine instance. 186 | 187 | Raises 188 | EngineError - if the MATLAB cannot be connected. 189 | """ 190 | 191 | #multi-threads cannot run this function simultaneously 192 | 193 | background = enginehelper._get_async_or_background_argument(kwargs) 194 | if name is None: 195 | with _engine_lock: 196 | #if there is no shareable or more than one shareable MATLAB 197 | engines = find_matlab() 198 | 199 | if len(engines) == 0: 200 | future = FutureResult(option="-r matlab.engine.shareEngine") 201 | else: 202 | #if there are shareable MATLAB sessions available 203 | future = FutureResult(name=engines[0], attach=True) 204 | 205 | if not background: 206 | eng = future.result() 207 | return eng 208 | else: 209 | return future 210 | else: 211 | future = FutureResult(name=name, attach=True) 212 | if not background: 213 | eng = future.result() 214 | return eng 215 | else: 216 | return future 217 | 218 | @atexit.register 219 | def __exit_engines(): 220 | for eng in _engines: 221 | if eng() is not None: 222 | eng().exit() 223 | _session.release() 224 | -------------------------------------------------------------------------------- /src/matlab/engine/_arch.txt: -------------------------------------------------------------------------------- 1 | win64 2 | C:\UserAddedPrograms\MATLAB\R2022b\bin\win64 3 | C:\UserAddedPrograms\MATLAB\R2022b\extern\engines\python\dist\matlab\engine\win64 4 | C:\UserAddedPrograms\MATLAB\R2022b\extern\bin\win64 -------------------------------------------------------------------------------- /src/matlab/engine/basefuture.py: -------------------------------------------------------------------------------- 1 | #Copyright 2015 MathWorks, Inc. 2 | 3 | """ 4 | BaseFuture: The base class of FevalFuture and MatlabFuture. 5 | 6 | """ 7 | 8 | import time 9 | 10 | 11 | class BaseFuture(object): 12 | 13 | def wait(self, timeout, wait_for_func): 14 | """ 15 | Wait for the execution of a function. 16 | 17 | Parameter 18 | timeout: int 19 | Number of seconds to wait before returning. 20 | 21 | Returns 22 | The result is ready or not. 23 | """ 24 | time_slice = 1 25 | if timeout is None: 26 | result_ready = self.done() 27 | while not result_ready: 28 | result_ready = wait_for_func(self._future, time_slice) 29 | else: 30 | result_ready = self.done() 31 | current_time = time.time() 32 | sleep_until = current_time + timeout 33 | while (not result_ready) and (current_time < sleep_until): 34 | if (sleep_until - current_time) >= time_slice: 35 | result_ready = wait_for_func(self._future, time_slice) 36 | else: 37 | time_to_sleep = sleep_until - current_time 38 | result_ready = wait_for_func(self._future, time_to_sleep) 39 | current_time = time.time() 40 | return result_ready -------------------------------------------------------------------------------- /src/matlab/engine/engineerror.py: -------------------------------------------------------------------------------- 1 | #Copyright 2014 MathWorks, Inc. 2 | 3 | class RejectedExecutionError(Exception): 4 | """Exception raised from MATLAB engine""" 5 | 6 | def __init__(self, message): 7 | self.message = message 8 | def __repr__(self): 9 | return self.message 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/matlab/engine/enginehelper.py: -------------------------------------------------------------------------------- 1 | #Copyright 2017-2022 MathWorks, Inc. 2 | import warnings 3 | from matlab.engine import pythonengine 4 | import sys 5 | 6 | def _get_async_or_background_argument(kwargs): 7 | if 'async' in kwargs and 'background' in kwargs: 8 | raise KeyError(pythonengine.getMessage('EitherAsyncOrBackground')) 9 | background = False 10 | if 'async' in kwargs: 11 | background = kwargs.pop('async', False) 12 | if not isinstance(background, bool): 13 | raise TypeError(pythonengine.getMessage('AsyncMustBeBool')) 14 | # No test should be passing "async" with Python 3.7 or higher, so throw an exception 15 | # if a test tries to do it. 16 | raise SyntaxError(pythonengine.getMessage('AsyncWillDeprecate')) 17 | 18 | if 'background' in kwargs: 19 | background = kwargs.pop('background', False) 20 | if not isinstance(background, bool): 21 | raise TypeError(pythonengine.getMessage('BackgroundMustBeBool')) 22 | 23 | if kwargs: 24 | raise TypeError((pythonengine.getMessage('InvalidKwargs', list(kwargs.keys())[0].__repr__()))) 25 | 26 | return background -------------------------------------------------------------------------------- /src/matlab/engine/enginesession.py: -------------------------------------------------------------------------------- 1 | #Copyright 2014 MathWorks, Inc. 2 | 3 | from matlab.engine import pythonengine 4 | 5 | class EngineSession(): 6 | def __init__(self): 7 | try: 8 | pythonengine.createProcess() 9 | self._process_created = True 10 | except: 11 | raise 12 | 13 | def __del__(self): 14 | self.release() 15 | 16 | def release(self): 17 | if self._process_created: 18 | try: 19 | pythonengine.closeProcess() 20 | self._process_created = False 21 | except: 22 | pass 23 | -------------------------------------------------------------------------------- /src/matlab/engine/fevalfuture.py: -------------------------------------------------------------------------------- 1 | #Copyright 2015 MathWorks, Inc. 2 | 3 | """ 4 | FevalFuture: The class name of a future result returned by the MATLAB Engine. 5 | 6 | An instance of FevalFuture is returned from each asynchronous invocation of a 7 | MATLAB statement or function. The future result serves as a placeholder of the 8 | actual result, so the future result can be returned immediately. MATLAB puts 9 | the actual result into the placeholder when the MATLAB function finishes its 10 | evaluation. The future result can be used to interrupt the execution, check the 11 | completion status, and get the result of a MATLAB statement. 12 | 13 | """ 14 | 15 | from matlab.engine import pythonengine 16 | from matlab.engine import RejectedExecutionError 17 | from matlab.engine import TimeoutError 18 | from matlab.engine import BaseFuture 19 | import time 20 | import weakref 21 | 22 | class FevalFuture(BaseFuture): 23 | """ 24 | A FevalFuture object is used to hold the future result of a MATLAB 25 | statement. The FevalFuture object should be only created by MatlabEngine 26 | after submitting a MATLAB command for evaluation. 27 | """ 28 | 29 | def __init__(self, eng, handle, nout, stdout, stderr): 30 | self._engine = weakref.ref(eng) 31 | self._future = handle 32 | self._nargout = nout 33 | self._out = stdout 34 | self._err = stderr 35 | self._retrieved = False 36 | self._result = None 37 | 38 | def result(self, timeout=None): 39 | """ 40 | Get the result of a MATLAB statement. 41 | 42 | Parameter 43 | timeout: int 44 | Number of seconds to wait before returning. By default, 45 | this function will wait until the result is generated. 46 | 47 | Returns 48 | The result of MATLAB statement. A tuple is returned if multiple 49 | outputs are returned. 50 | 51 | Raises 52 | SyntaxError - if there is an error in the MATLAB statement. 53 | InterruptedError - if the task is interrupted. 54 | CancelledError - if the evaluation of MATLAB function is cancelled already. 55 | TimeoutError - if this method fails to get the result in timeout seconds. 56 | MatlabExecutionError - if the MATLAB statement fails in execution. 57 | TypeError - if the data type of return value is not supported. 58 | RejectedExecutionError - an error occurs if the engine is terminated. 59 | """ 60 | self.__validate_engine() 61 | 62 | if self._retrieved: 63 | return self._result 64 | 65 | """ 66 | Following code is used to poll the Ctrl+C every second from keyboard in 67 | order to cancel a MATLAB function. 68 | """ 69 | 70 | try: 71 | result_ready = self.wait(timeout, pythonengine.waitForFEval) 72 | 73 | if not result_ready: 74 | raise TimeoutError(pythonengine.getMessage('MatlabFunctionTimeout')) 75 | 76 | self._result = pythonengine.getFEvalResult(self._future,self._nargout, None, out=self._out, err=self._err) 77 | self._retrieved = True 78 | return self._result 79 | 80 | except KeyboardInterrupt: 81 | self.cancel() 82 | if self.cancelled(): 83 | print(pythonengine.getMessage('MatlabFunctionCancelled')) 84 | except: 85 | raise 86 | 87 | def cancel(self): 88 | """ 89 | Cancel the execution of an evaluation of a MATLAB statement. 90 | 91 | Returns 92 | bool - True if the corresponding MATLAB statement can be cancelled; 93 | False otherwise. 94 | 95 | Raises 96 | RejectedExecutionError - an error occurs if the engine is terminated. 97 | """ 98 | self.__validate_engine() 99 | return pythonengine.cancelFEval(self._future) 100 | 101 | def cancelled(self): 102 | """ 103 | Obtain the cancellation status of the asynchronous execution of a MATLAB 104 | command. 105 | 106 | Returns 107 | bool - True if the execution is cancelled; False otherwise. 108 | 109 | Raises 110 | RejectedExecutionError - an error occurs if the engine is terminated. 111 | """ 112 | self.__validate_engine() 113 | return pythonengine.isCancelledFEval(self._future) 114 | 115 | def done(self): 116 | """ 117 | Obtain the completion status of the asynchronous invocation of a MATLAB 118 | command. 119 | 120 | Returns 121 | bool - True if the execution is finished; False otherwise. It 122 | returns True even if there is an error generated from the MATLAB 123 | statement or it is cancelled. 124 | 125 | Raises 126 | RejectedExecutionError - an error occurs if the engine is terminated. 127 | """ 128 | self.__validate_engine() 129 | return pythonengine.isDoneFEval(self._future) 130 | 131 | def __del__(self): 132 | if self._future is not None: 133 | pythonengine.destroyFEvalResult(self._future) 134 | self._future = None 135 | self._result = None 136 | 137 | def __validate_engine(self): 138 | if self._engine() is None or not self._engine()._check_matlab(): 139 | raise RejectedExecutionError(pythonengine.getMessage('MatlabTerminated')) -------------------------------------------------------------------------------- /src/matlab/engine/futureresult.py: -------------------------------------------------------------------------------- 1 | #Copyright 2014-2017 MathWorks, Inc. 2 | 3 | """ 4 | FutureResult: The class name of a future result returned by the MATLAB Engine. 5 | 6 | An instance of FutureResult is returned from each asynchronous invocation of a 7 | function call: start_matlab, connect_matlab, or MatlabEngine.. The 8 | future result serves as a placeholder of the actual result, so the future 9 | result can be returned immediately. The actual result is placed into the 10 | placeholder when the function finishes its evaluation. The future result can 11 | be used to interrupt the execution, check the completion status, and get the 12 | actual result. 13 | """ 14 | 15 | from matlab.engine import pythonengine 16 | from matlab.engine import MatlabFuture 17 | from matlab.engine import FevalFuture 18 | 19 | class FutureResult(): 20 | """ 21 | A FutureResult object is used to hold the future result of a function call. 22 | The FutureResult object can be created by start_matlab(async=True), 23 | connect_matlab(async=True), and MaltabEngine.(async=True). 24 | """ 25 | 26 | def __init__(self, *args, **kwargs): 27 | feval = kwargs.pop("feval", False) 28 | if feval: 29 | self.__future = FevalFuture(*args) 30 | else: 31 | self.__future = MatlabFuture(*args, **kwargs) 32 | 33 | def result(self, timeout=None): 34 | """ 35 | Get the MatlabEngine object or the result of a MATLAB statement. 36 | 37 | Parameter 38 | timeout: int 39 | Number of seconds to wait before returning. By default, 40 | this function will wait until the result is generated. 41 | 42 | Returns 43 | The MatlabEngine object or the result of MATLAB statement. A tuple 44 | is returned if multiple outputs are returned. 45 | 46 | Raises 47 | SyntaxError - if there is an error in the MATLAB statement. 48 | InterruptedError - if the task is interrupted. 49 | CancelledError - if the evaluation of MATLAB function is cancelled already. 50 | TimeoutError - if this method fails to get the result in timeout seconds. 51 | MatlabExecutionError - if the MATLAB statement fails in execution. 52 | TypeError - if the data type of return value is not supported. 53 | RejectedExecutionError - an error occurs if the engine is terminated. 54 | """ 55 | if timeout is not None: 56 | if not isinstance(timeout, (int, float)): 57 | raise TypeError(pythonengine.getMessage('TimeoutMustBeNumeric', type(timeout).__name__)) 58 | 59 | if timeout < 0: 60 | raise TypeError(pythonengine.getMessage('TimeoutCannotBeNegative')) 61 | 62 | return self.__future.result(timeout) 63 | 64 | def cancel(self): 65 | """ 66 | Cancel the launch/connection of MATLAB or evaluation of a MATLAB 67 | task. 68 | 69 | Returns 70 | bool - True if the corresponding task can be cancelled; 71 | False otherwise. 72 | 73 | Raises 74 | RejectedExecutionError - an error occurs if the Engine is terminated. 75 | """ 76 | return self.__future.cancel() 77 | 78 | def cancelled(self): 79 | """ 80 | Obtain the cancellation status of the asynchronous execution of the 81 | function. 82 | 83 | Returns 84 | bool - True if the execution is cancelled; False otherwise. 85 | 86 | Raises 87 | RejectedExecutionError - an error occurs if the engine is terminated. 88 | """ 89 | return self.__future.cancelled() 90 | 91 | def done(self): 92 | """ 93 | Obtain the completion status of the asynchronous invocation of the 94 | task. 95 | 96 | Returns 97 | bool - True if the execution is finished; False otherwise. It 98 | returns True even if there is an error generated from the task 99 | or the task is cancelled. 100 | 101 | Raises 102 | RejectedExecutionError - an error occurs if the engine is terminated. 103 | """ 104 | return self.__future.done() 105 | 106 | def __del__(self): 107 | if self.__future is not None: 108 | del self.__future -------------------------------------------------------------------------------- /src/matlab/engine/matlabengine.py: -------------------------------------------------------------------------------- 1 | #Copyright 2014-2017 MathWorks, Inc. 2 | 3 | """ 4 | MatlabEngine: The class name of MATLAB Engine. You can call MATLAB software as 5 | a computational engine using the MatlabEngine class. 6 | """ 7 | 8 | 9 | from matlab.engine import pythonengine 10 | from matlab.engine import FutureResult 11 | from matlab.engine import RejectedExecutionError 12 | from matlab.engine import MatlabExecutionError 13 | import weakref 14 | import shlex 15 | from matlab.engine import enginehelper 16 | 17 | try: 18 | import StringIO as sIO 19 | except ImportError: 20 | import io as sIO 21 | 22 | class MatlabFunc(object): 23 | """ 24 | Reference to a MATLAB function, where "matlabfunc" is replaced by the 25 | function called by the user. *args are passed to MATLAB. **kwargs are 26 | only passed to the engine. 27 | """ 28 | 29 | def __init__(self, eng, name): 30 | self.__dict__["_engine"] = weakref.ref(eng) 31 | self.__dict__["_name"] = name 32 | 33 | def __getattr__(self, name): 34 | return MatlabFunc(self._engine(), "%s.%s" % (self._name, name)) 35 | 36 | def __setattr__(self, kw, value): 37 | raise AttributeError(pythonengine.getMessage('AttrCannotBeAddedToM')) 38 | 39 | def __call__(self, *args, **kwargs): 40 | self.__validate_engine() 41 | 42 | nargs = kwargs.pop('nargout', 1) 43 | 44 | if not isinstance(nargs, int): 45 | raise TypeError(pythonengine.getMessage('NargoutMustBeInt', type(nargs).__name__)) 46 | 47 | if nargs < 0: 48 | raise ValueError(pythonengine.getMessage('NargoutCannotBeLessThanZero')) 49 | 50 | _stdout = kwargs.pop('stdout', None) 51 | _stderr = kwargs.pop('stderr', None) 52 | 53 | background = enginehelper._get_async_or_background_argument(kwargs) 54 | 55 | _sIO_info = '{0}.{1}'.format(sIO.__name__, sIO.StringIO.__name__); 56 | if (_stdout is not None) and (not isinstance(_stdout, sIO.StringIO)): 57 | _stdout_info = '{0}.{1}'.format(_stdout.__class__.__module__, _stdout.__class__.__name__); 58 | raise TypeError(pythonengine.getMessage('StdoutMustBeStringIO', _sIO_info, _stdout_info)) 59 | 60 | if (_stderr is not None) and (not isinstance(_stderr, sIO.StringIO)): 61 | _stderr_info = '{0}.{1}'.format(_stderr.__class__.__module__, _stderr.__class__.__name__); 62 | raise TypeError(pythonengine.getMessage('StderrMustBeStringIO', _sIO_info, _stderr_info)) 63 | 64 | future = pythonengine.evaluateFunction(self._engine()._matlab, 65 | self._name, nargs,args, 66 | out=_stdout, err=_stderr) 67 | if background: 68 | return FutureResult(self._engine(), future, nargs, _stdout, _stderr, feval=True) 69 | else: 70 | return FutureResult(self._engine(), future, nargs, _stdout, 71 | _stderr, feval=True).result() 72 | 73 | def __validate_engine(self): 74 | if self._engine() is None or not self._engine()._check_matlab(): 75 | raise RejectedExecutionError(pythonengine.getMessage('MatlabTerminated')) 76 | 77 | class MatlabWorkSpace(object): 78 | """ 79 | [''] 80 | ['']=vardata 81 | 82 | Pass a variable into the MATLAB workspace and copy a 83 | variable from the MATLAB workspace. 84 | 85 | Parameters 86 | : str 87 | Variable name to be used in the MATLAB workspace. 88 | 89 | vardata: object 90 | A Python variable to be passed into the MATLAB workspace. 91 | 92 | Returns 93 | [''] returns the variable copied from the 94 | MATLAB workspace. 95 | 96 | ['']=vardata returns None. 97 | 98 | Raises 99 | NameError - if there is no such variable in the MATLAB 100 | workspace. 101 | 102 | SyntaxError - if the data is passed to the MATLAB 103 | workspace with an illegal variable name. 104 | 105 | TypeError - if is not a string, or if 106 | the data type of vardata is not supported. 107 | 108 | ValueError - if is empty. 109 | 110 | RejectedExecutionError - if the Engine is terminated. 111 | """ 112 | 113 | def __init__(self,eng): 114 | self.__dict__["_engine"] = weakref.ref(eng) 115 | 116 | def __getitem__(self,attr): 117 | self.__validate_engine() 118 | self.__validate_identity(attr) 119 | _method=MatlabFunc(self._engine(), "matlab.internal.engine.getVariable") 120 | future = _method(attr) 121 | return future 122 | 123 | def __setitem__(self,attr,value): 124 | self.__validate_engine() 125 | self.__validate_identity(attr) 126 | 127 | _method=MatlabFunc(self._engine(), "assignin") 128 | return _method("base", attr, value, nargout=0) 129 | 130 | def __repr__(self): 131 | _method = MatlabFunc(self._engine(), "whos") 132 | _method(nargout=0) 133 | return "" 134 | 135 | def __setattr__(self, kw, value): 136 | raise AttributeError(pythonengine.getMessage('AttrCannotBeAddedToMWS')) 137 | 138 | def __validate_engine(self): 139 | if self._engine() is None or not self._engine()._check_matlab(): 140 | raise RejectedExecutionError(pythonengine.getMessage('MatlabTerminated')) 141 | 142 | def __validate_identity(self, attr): 143 | if not isinstance(attr, str): 144 | raise TypeError(pythonengine.getMessage('VarNameMustBeStr', type(attr).__name__)) 145 | if not pythonengine.validateIdentity(attr): 146 | raise ValueError(pythonengine.getMessage('VarNameNotValid')) 147 | 148 | class MatlabEngine(object): 149 | """ 150 | By default, the MATLAB Engine starts a MATLAB instance in a separate 151 | process without the desktop on the local machine. The MATLAB version 152 | used by the engine application is the version of MATLAB specified in PATH. 153 | 154 | The MATLAB Engine supports calling MATLAB functions directly. MATLAB 155 | functions are dynamically added to a MatlabEngine object as callable 156 | attributes. The function name is a replaceable MATLAB 157 | function name (for example, sqrt). The function signature is the same as in 158 | MATLAB, with optional named arguments nargout, async|background, stdout, and stderr. 159 | 160 | workspace[''] 161 | A property to represent the MATLAB workspace. Variables in 162 | the MATLAB workspace can be accessed through . The 163 | type of this property is MatlabWorkSpace. 164 | 165 | 166 | (*args, nargout=1, async=False, stdout=sys.stdout, stderr=sys.stderr) 167 | (*args, nargout=1, background=False, stdout=sys.stdout, stderr=sys.stderr) 168 | 169 | The invocation of a MATLAB statement can be either synchronous 170 | or asynchronous. While a synchronous function call returns 171 | the result after it finishes executing, an asynchronous 172 | function call is performed in the background and returns a FutureResult 173 | immediately. This FutureResult object can be used to retrieve the 174 | actual result later. If there are any output or error messages 175 | generated from , by default they will be redirected to the 176 | standard output or standard error of the Python console. 177 | 178 | Please note that you can call an arbitrary MATLAB function 179 | available in the MATLAB path using feval and eval. 180 | 181 | Parameters 182 | args: 183 | Arguments accepted by the MATLAB function to be called. 184 | nargout: int 185 | By default, the number of output is 1. If the number of output 186 | is more than 1, a tuple is returned. 187 | async, background: bool 188 | This parameter is used to specify how the MATLAB command is 189 | evaluated: asynchronously or synchronously. By default, async|background 190 | is chosen to be False so the MATLAB command is evaluated synchronously. 191 | "async" is a synonym for "background" that will be removed in a future release. 192 | stdout: StringIO.StringIO (Python 2.7), io.StringIO (Python 3) 193 | Stream used to capture the output of the MATLAB command. By 194 | default, the system standard output sys.stdout is used. 195 | stderr: StringIO.StringIO (Python 2.7), io.StringIO (Python 3) 196 | Stream used to capture the error message of the MATLAB command. 197 | By default, the system standard error output sys.stderr is used. 198 | 199 | Returns 200 | The type of the return value of this function varies based on the 201 | value of parameter async or background. For the case of synchronously invocation, 202 | the result of the MATLAB command is returned directly. For the case 203 | of asynchronous invocation, a FutureResult is returned which can be 204 | used to retrieve the actual result, check completion status, and 205 | interrupt the execution of the MATLAB function. 206 | 207 | Raises 208 | RejectedExecutionError - if the engine is terminated. 209 | SyntaxError - if there is an error in parsing the MATLAB statement. 210 | MatlabExecutionError - if the MATLAB statement fails in execution. 211 | TypeError - if the data types of *args are not supported by 212 | MATLABEngine; or if the data type of a return value is not supported. 213 | 214 | """ 215 | 216 | def __init__(self, matlab): 217 | self.__dict__["_matlab"] = matlab 218 | self.__dict__["workspace"] = MatlabWorkSpace(self) 219 | 220 | def __enter__(self): 221 | return self 222 | 223 | def __exit__(self, exc_type, exc_value, traceback): 224 | self.exit() 225 | 226 | def exit(self): 227 | """ 228 | Stop the MATLAB session. Calling this method will terminate the 229 | MatlabEngine instance immediately. 230 | """ 231 | if self._check_matlab(): 232 | pythonengine.closeMATLAB(self.__dict__["_matlab"]) 233 | self.__dict__.pop("_matlab") 234 | 235 | def quit(self): 236 | """ 237 | Stop the MATLAB session. Calling this method will terminate the 238 | MatlabEngine instance immediately. 239 | """ 240 | self.exit() 241 | 242 | def __getattr__(self,name): 243 | """Dynamic attribute of MatlabEngine""" 244 | return MatlabFunc(self, name) 245 | 246 | def __setattr__(self, kw, value): 247 | raise AttributeError(pythonengine.getMessage('AttrCannotBeAddedToM')) 248 | 249 | def __del__(self): 250 | self.exit() 251 | 252 | def _check_matlab(self): 253 | return "_matlab" in self.__dict__ 254 | -------------------------------------------------------------------------------- /src/matlab/engine/matlabfuture.py: -------------------------------------------------------------------------------- 1 | #Copyright 2015 MathWorks, Inc. 2 | 3 | 4 | """ 5 | MatlabFuture: The class name of a future handle returned by starting MATLAB 6 | asynchronously. 7 | 8 | An instance of MatlabFuture is returned from the invocation of starting MATLAB 9 | asyncronously. The future handle serves as a placeholder of the actual MATLAB 10 | instance, the the future handle can be returned immediately. The future handle 11 | can be used to interrupt the launch of MATLAB, check the completion status and 12 | get the real MATLAB Engine object. 13 | 14 | """ 15 | 16 | from matlab.engine import pythonengine 17 | from matlab.engine import TimeoutError 18 | from matlab.engine import CancelledError 19 | from matlab.engine import BaseFuture 20 | from matlab.engine import _engines 21 | from matlab.engine import _engine_lock 22 | import shlex 23 | import weakref 24 | 25 | 26 | class MatlabFuture(BaseFuture): 27 | """ 28 | A MatlabFuture object is used to hold the future handle of a MATLAB 29 | instance. The MatlabFuture object should be only created by calling 30 | start_matlab or connect_matlab asynchronously. 31 | """ 32 | 33 | def __init__(self, **kargs): 34 | tokens = [] 35 | option = kargs.pop("option", None) 36 | name_ = kargs.pop("name", None) 37 | attach = kargs.pop("attach", False) 38 | self._matlab = None 39 | self._cancelled = False 40 | self._done = False 41 | if option is not None: 42 | tokens = shlex.split(option) 43 | try: 44 | if attach: 45 | self._future = pythonengine.attachMATLABAsync(name_) 46 | self._attach = True 47 | else: 48 | self._future = pythonengine.createMATLABAsync(tokens) 49 | self._attach = False 50 | except: 51 | raise 52 | 53 | def result(self, timeout=None): 54 | """ 55 | Get the MatlabEngine instance. 56 | 57 | Parameter 58 | timeout: Number of seconds to wait before returning. By default, 59 | this function will wait until the MatlabEngine instance is ready. 60 | 61 | Returns 62 | An object of MatlabEngine class. 63 | 64 | Raises 65 | CancelledError - if the launch or connection of MATLAB is cancelled already. 66 | TimeoutError - if the MATLAB instance is not ready in timeout seconds. 67 | """ 68 | from matlab.engine import MatlabEngine 69 | if self._cancelled: 70 | if self._attach: 71 | raise CancelledError(pythonengine.getMessage('ConnectMatlabCancelled')) 72 | else: 73 | raise CancelledError(pythonengine.getMessage('LaunchMatlabCancelled')) 74 | 75 | if self._matlab is not None: 76 | return self._matlab 77 | 78 | try: 79 | result_ready = self.wait(timeout, pythonengine.waitForMATLAB) 80 | 81 | if not result_ready: 82 | if self._attach: 83 | raise TimeoutError(pythonengine.getMessage('ConnectMatlabTimeout')) 84 | else: 85 | raise TimeoutError(pythonengine.getMessage('LaunchMatlabTimeout')) 86 | 87 | handle = pythonengine.getMATLAB(self._future) 88 | eng = MatlabEngine(handle) 89 | self._matlab = eng 90 | with _engine_lock: 91 | _engines.append(weakref.ref(eng)) 92 | return eng 93 | 94 | except KeyboardInterrupt: 95 | self.cancel() 96 | print(pythonengine.getMessage('MatlabCancelled')) 97 | except: 98 | raise 99 | 100 | 101 | def cancel(self): 102 | """ 103 | Cancel the launch of or connection to a MATLAB instance. 104 | 105 | Returns 106 | bool - True if the action can be cancelled; False otherwise. 107 | """ 108 | if self._matlab is None and not self._cancelled and not self._done: 109 | pythonengine.cancelMATLAB(self._future) 110 | self._cancelled = True 111 | return True 112 | else: 113 | return False 114 | 115 | def cancelled(self): 116 | """ 117 | Obtain the cancellation status of the asynchronous launch or connection 118 | to a MATLAB instance. 119 | 120 | Returns 121 | bool - True if the execution was cancelled; False otherwise. 122 | """ 123 | if self._cancelled: 124 | return True 125 | return False 126 | 127 | def done(self): 128 | """ 129 | Obtain the completion status of the asynchronous launch or connection 130 | to a MATLAB instance. 131 | 132 | Returns 133 | bool - True if the action is finished; False otherwise. 134 | """ 135 | if self._done or self._cancelled or self._matlab is not None: 136 | return True 137 | self._done = pythonengine.isDoneMATLAB(self._future) 138 | return self._done 139 | 140 | def __del__(self): 141 | if self._future is not None: 142 | pythonengine.destroyMATLAB(self._future) 143 | self._future = None 144 | self._matlab = None -------------------------------------------------------------------------------- /test/tInstall.m: -------------------------------------------------------------------------------- 1 | classdef tInstall < matlab.unittest.TestCase 2 | % Verify installation of matlab engine 3 | 4 | % Copyright 2023 Mathworks, Inc. 5 | 6 | properties (Constant) 7 | MATLABVersion = string(ver('MATLAB').Version) % Example: 23.2 8 | end 9 | 10 | methods (Test) 11 | function installNoVersionSpecified(testCase) 12 | [status, out] = system("pip install matlabengine"); 13 | addTeardown(testCase, @system, "pip uninstall -y matlabengine"); 14 | verifyEqual(testCase, status, 0, out) 15 | verifyInstallation(testCase) 16 | end 17 | 18 | function installMatchingEngine(testCase) 19 | [status, out] = system("pip install matlabengine==" + testCase.MATLABVersion + ".*"); 20 | addTeardown(testCase, @system, "pip uninstall -y matlabengine"); 21 | verifyEqual(testCase, status, 0, out) 22 | verifyInstallation(testCase) 23 | end 24 | end 25 | 26 | methods 27 | function verifyInstallation(testCase) 28 | % Verify installation by calling functions in matlab engine 29 | % Share this session and see if find_matlab can find it. 30 | sharedEngineName = matlab.engine.engineName; 31 | if isempty(sharedEngineName) 32 | sharedEngineName = 'MATLAB_tInstall'; 33 | matlab.engine.shareEngine(sharedEngineName) 34 | end 35 | pySharedEngineName = char(py.matlab.engine.find_matlab()); 36 | verifySubstring(testCase, pySharedEngineName, sharedEngineName) 37 | end 38 | end 39 | end --------------------------------------------------------------------------------