Skip to content
Open
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
159 changes: 159 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,9 @@ jobs:

matlab:
name: MATLAB ${{ matrix.release }} on ${{ matrix.os }}
# Disabled: MATLAB is tested with the self-contained library that ships with the
# MATLAB toolbox instead (see 'matlab-bundled'). This job is to be removed.
if: false
strategy:
matrix:
os: [ubuntu-24.04, windows-2025, macos-14]
Expand Down Expand Up @@ -1402,3 +1405,159 @@ jobs:
uses: matlab-actions/run-tests@214a16e600ef2c705a4b2dd6c0794dc6957682a9 # v3.3.0
with:
select-by-folder: test/matlab

matlab-library:
# Builds a self-contained Cantera library for MATLAB: all dependencies are vendored
# and linked statically, and the result is audited for non-system dependencies.
# The cantera_matlab toolbox builds its library with the same scripts in
# interfaces/matlab/buildUtilities.
name: MATLAB bundled library on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
matrix:
os: [ubuntu-24.04, macos-14, macos-15, windows-2025]
fail-fast: false
env:
BOOST_ROOT: ${{ github.workspace }}/3rdparty/boost
BOOST_URL: https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-b2-nodocs.7z
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
name: Checkout the repository
with:
submodules: recursive
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Install SCons and Cantera build dependencies
run: python -m pip install --upgrade 'scons>=4.0' packaging jinja2 ruamel.yaml
- name: Install Boost and Doxygen (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libboost-dev doxygen
- name: Install Boost and Doxygen (macOS)
if: runner.os == 'macOS'
run: |
brew install --display-times boost doxygen
echo "BOOST_INC_DIR=$(brew --prefix)/include" >> "$GITHUB_ENV"
- name: Restore Boost cache (Windows)
if: runner.os == 'Windows'
uses: actions/cache@v6
id: cache-boost
with:
path: ${{ env.BOOST_ROOT }}
key: boost-187-win
- name: Install Boost headers (Windows)
if: runner.os == 'Windows' && steps.cache-boost.outputs.cache-hit != 'true'
run: |
BOOST_ROOT=$(echo "$BOOST_ROOT" | sed 's/\\/\//g')
mkdir -p "$BOOST_ROOT"
curl --progress-bar --location --output "$BOOST_ROOT/download.7z" "$BOOST_URL"
7z -o"$BOOST_ROOT" x "$BOOST_ROOT/download.7z" -y -bd boost-1.87.0/boost
mv "$BOOST_ROOT/boost-1.87.0/boost" "$BOOST_ROOT/boost"
rm "$BOOST_ROOT/download.7z"
- name: Set Boost include directory and install Doxygen (Windows)
if: runner.os == 'Windows'
run: |
echo "BOOST_INC_DIR=$(echo "$BOOST_ROOT" | sed 's/\\/\//g')" >> "$GITHUB_ENV"
choco install doxygen.install -y --no-progress
echo "C:/Program Files/doxygen/bin" >> "$GITHUB_PATH"
- name: Build the Cantera library
run: >-
python interfaces/matlab/buildUtilities/build_cantera_library.py --verbose
--cantera-root "$GITHUB_WORKSPACE" --prefix "$GITHUB_WORKSPACE/build/canteraLib"
- name: Archive the Cantera library
# tar keeps the versioned library symlinks, which upload-artifact does not
run: tar -czf canteraLib.tar.gz -C build canteraLib
- name: Upload the Cantera library
uses: actions/upload-artifact@v7
with:
name: matlab-canteraLib-${{ matrix.os }}
path: canteraLib.tar.gz

matlab-bundled:
# Runs the tests of the 'matlab' job with the self-contained library from
# 'matlab-library', so no third-party runtime libraries are installed.
name: MATLAB ${{ matrix.release }} on ${{ matrix.os }} (bundled library)
strategy:
matrix:
os: [ubuntu-24.04, windows-2025, macos-14]
# R2024b is the oldest release supported by the MATLAB toolbox
release: [R2024b, latest]
include:
- os: 'macos-15'
release: 'latest'
fail-fast: false
env:
CANTERA_ROOT: ${{ github.workspace }}
CANTERA_DATA: ${{ github.workspace }}/data
runs-on: ${{ matrix.os }}
needs: [matlab-library]
timeout-minutes: 60
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
name: Checkout the repository
with:
persist-credentials: false
- name: Download the Cantera library
uses: actions/download-artifact@v8
with:
name: matlab-canteraLib-${{ matrix.os }}
- name: Extract the Cantera library
run: |
mkdir -p build
tar -xzf canteraLib.tar.gz -C build
- name: Add Cantera library directory to PATH (Windows)
if: runner.os == 'Windows'
run: |
echo "$GITHUB_WORKSPACE/build/canteraLib/lib" >> "$GITHUB_PATH"
echo "$GITHUB_WORKSPACE/interfaces/matlab/+ct/+impl/ctMatlab" >> "$GITHUB_PATH"
- name: Install Visual C++ 2022 build tools (Windows, R2024b)
# R2024b supports Visual C++ 2022 at newest, but windows-2025 ships a newer
# Visual Studio, so mex finds no supported compiler; see #2152.
run: |
choco install visualstudio2022buildtools -y --no-progress
choco install visualstudio2022-workload-vctools -y --no-progress
if: runner.os == 'Windows' && matrix.release == 'R2024b'
- name: Set up MATLAB
uses: matlab-actions/setup-matlab@f9e43010f1ae678f7cfa0542fe2a4f60f7d1ad8d # v3.1.0
with:
release: ${{ matrix.release }}
- name: Build MATLAB C++ Interface
uses: matlab-actions/run-command@bfa857648f4895aa98a446fe41c85e0788421ed5 # v3.3.0
env:
# On Linux, MATLAB R2024b crashes while generating the interface unless the
# system libstdc++ is preloaded. Only the build needs it: the interface and the
# Cantera library link libstdc++ statically, so the tests run without it.
LD_PRELOAD: ${{ runner.os == 'Linux' && '/lib/x86_64-linux-gnu/libstdc++.so.6' || '' }}
with:
command: |
disp("MATLAB version: " + version);
ctDir = getenv('CANTERA_ROOT');
ctToolboxDir = fullfile(ctDir, 'interfaces', 'matlab');
ctIncludeDir = fullfile(ctDir, 'build', 'canteraLib', 'include');
ctLibDir = fullfile(ctDir, 'build', 'canteraLib', 'lib');
addpath(genpath(ctToolboxDir));
ct.buildInterface(ctToolboxDir, ctIncludeDir, ctLibDir);
- name: Run tests
# The library has no third-party dependencies, so it loads in-process on every
# platform without LD_PRELOAD. The tests use the library loaded here instead of
# loading it.
uses: matlab-actions/run-command@bfa857648f4895aa98a446fe41c85e0788421ed5 # v3.3.0
with:
command: |
ctDir = getenv('CANTERA_ROOT');
addpath(genpath(fullfile(ctDir, 'interfaces', 'matlab')));
ct.load("inprocess");
results = runtests(fullfile(ctDir, 'test', 'matlab'));
disp(table(results));
assertSuccess(results);
3 changes: 3 additions & 0 deletions doc/sphinx/matlab/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Utility Functions

Library Setup
-------------
.. autofunction:: install
.. autofunction:: uninstall
.. autofunction:: load
.. autofunction:: unload
.. autofunction:: isLoaded
Expand All @@ -23,6 +25,7 @@ Library Setup

Global Settings
---------------
.. autofunction:: addDataDirectories
.. autofunction:: dataDirectories
.. autofunction:: makeDeprecationWarningsFatal

Expand Down
4 changes: 4 additions & 0 deletions interfaces/matlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ Cantera objects and functions.

1. **Load the toolbox**

Run `ct.install` once to register the data directory and verify the build;
the setting persists across sessions. Then load Cantera in each session:

```matlab
ct.install() % once
ct.load()
```

Expand Down
22 changes: 22 additions & 0 deletions interfaces/matlab/Utility/+ct/addDataDirectories.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function addDataDirectories(dirs)
% Add one or more directories to the data file search path. ::
%
% >> ct.addDataDirectories('/path/to/data')
% >> ct.addDataDirectories(["/path/one", "/path/two"])
%
% Directories are added to the front of the Cantera data file search path,
% so the most recently added directory is searched first. Use
% :mat:func:`dataDirectories` to inspect the current search path.
%
% :param dirs:
% A string, char array, or string array of directories to add to the
% Cantera data file search path.
arguments
dirs (1,:) string
end

ct.isLoaded(true);
for d = dirs
ct.impl.call('mCt_addDataDirectory', char(d));
end
end
21 changes: 20 additions & 1 deletion interfaces/matlab/Utility/+ct/buildInterface.m
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function generateLibraryDefinitions(includeDir, ctLibDir, outputDir)
headerPaths = fullfile({headerFiles.folder}, headerPaths);

% Get path for the shared library file
libraries = ct.ctLib(ctLibDir);
libraries = ctLib(ctLibDir);
disp("Using shared library: " + libraries);

if isMATLABReleaseOlderThan("R2024a")
Expand All @@ -97,19 +97,38 @@ function generateLibraryDefinitions(includeDir, ctLibDir, outputDir)

overwriteExistingDefinitionFiles = true;

% On Linux, link the interface statically against libstdc++ and libgcc. The
% interface is built with the system compiler, whose libstdc++ can be newer than
% the one MATLAB ships (R2024b ships the one from GCC 12), so the interface would
% otherwise only load with the system libstdc++ preloaded.
linkerArgs = {};
if isunix && ~ismac
linkerArgs = {"AdditionalLinkerFlags", ["-static-libstdc++", "-static-libgcc"]};
end

% Set up C++ compiler
mex -setup cpp

% With R2026b, the interface build command on macOS no longer includes a C++
% standard flag, so older compilers (for example, Xcode 15) fall back to C++98,
% which cannot compile the MATLAB Data API headers.
compilerArgs = {};
if ~ispc
compilerArgs = {"AdditionalCompilerFlags", "-std=c++17"};
end

% Generate definition file for C++ library
clibgen.generateLibraryDefinition(headerPaths, ...
"IncludePath", includeDir, ...
"Libraries", libraries, ...
"OutputFolder", outputDir, ...
nameArg, "ctMatlab", ...
"OverwriteExistingDefinitionFiles", overwriteExistingDefinitionFiles, ...
compilerArgs{:}, ...
"CLinkage", true, ...
"TreatObjectPointerAsScalar", true, ...
"TreatConstCharPointerAsCString", true, ...
linkerArgs{:}, ...
"ReturnCArrays", false, ...
"Verbose", true);
end
Expand Down
8 changes: 4 additions & 4 deletions interfaces/matlab/Utility/+ct/cleanUp.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ function cleanUp()

ct.isLoaded(true);

classList = {'ct.Interface', 'ct.Kinetics', 'ct.Mixture', 'ct.ThermoPhase', ...
'ct.Transport', 'ct.Solution', 'ct.Func1', 'ct.oneD.Domain', ...
'ct.oneD.Sim1D', 'ct.zeroD.Connector', 'ct.zeroD.ReactorBase', ...
'ct.zeroD.ReactorNet'};
classList = {'ct.Interface', 'ct.Kinetics', 'ct.Mixture', 'ct.ThermoPhase', ...
'ct.Transport', 'ct.Solution', 'ct.Func1', 'ct.oneD.Domain', ...
'ct.oneD.Sim1D', 'ct.zeroD.Connector', 'ct.zeroD.ReactorBase', ...
'ct.zeroD.ReactorNet'};

varList = evalin('base', 'whos');

Expand Down
99 changes: 99 additions & 0 deletions interfaces/matlab/Utility/+ct/install.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
function install(opts)
% Set up the Cantera MATLAB toolbox after installing it. ::
%
% >> ct.install
% >> ct.install(Force=true)
%
% Run once after installing the toolbox; afterwards use :func:`ct.load`.
% This registers the data directory shipped with the toolbox and verifies
% that Cantera loads. Settings persist as MATLAB preferences in the
% "Cantera" group, and later calls do nothing unless the installation is
% incomplete or ``Force`` is true. Use :func:`ct.uninstall` to clear them.
%
% :param DataDirectory:
% Data directory to register instead of the one shipped with the
% toolbox.
% :param Force:
% Reinstall even if Cantera is already installed.

arguments
opts.DataDirectory (1,1) string = ""
opts.Force (1,1) logical = false
end

GROUP = "Cantera";
[interfaceDir, toolboxDir] = toolboxPaths();

if ~opts.Force && isInstalled(GROUP, interfaceDir)
fprintf("Cantera is already installed. " + ...
"Use ct.install(Force=true) to reinstall.\n");
return
end

dataDir = resolveDataDirectory(toolboxDir, opts.DataDirectory);
setpref(GROUP, "DataDirectory", char(dataDir));
fprintf("Registered Cantera data directory: %s\n", dataDir);

% Set the flag only after verification, so a failed install is retried.
verifyInstallation(dataDir);
setpref(GROUP, "Installed", true);
fprintf("Cantera %s installed successfully.\n", ct.version);
end

function tf = isInstalled(GROUP, interfaceDir)
% The flag alone is not trusted: preferences outlive the toolbox when it is
% removed in the Add-On Manager without running ct.uninstall.
tf = ispref(GROUP, "Installed") && isequal(getpref(GROUP, "Installed"), true) ...
&& hasInterface(interfaceDir) && ispref(GROUP, "DataDirectory") ...
&& isfolder(string(getpref(GROUP, "DataDirectory")));
end

function dataDir = resolveDataDirectory(toolboxDir, override)
if override ~= ""
if ~isfolder(override)
error("ct:install:BadDataDir", ...
"Data directory does not exist: %s", override);
end
dataDir = override;
return
end

candidates = [
fullfile(fileparts(toolboxDir), "data") % packaged toolbox
fullfile(fileparts(fileparts(toolboxDir)), "data") % source checkout
];
for c = candidates'
if ~isempty(dir(fullfile(c, "*.yaml")))
dataDir = c;
return
end
end

error("ct:install:NoDataDir", ...
("Could not find the Cantera data files shipped with the toolbox. " + ...
"Pass a folder with ct.install(DataDirectory=<folder>)."));
end

function verifyInstallation(dataDir)
if ct.isLoaded
ct.addDataDirectories(dataDir);
else
ct.load();
end

registered = string(ct.dataDirectories());
if ~any(arrayfun(@(d) samePath(d, dataDir), registered))
error("ct:install:DataDirNotRegistered", ...
"Cantera loaded, but did not register the data directory %s", ...
dataDir);
end
end

function tf = samePath(a, b)
normalize = @(p) regexprep(strrep(string(p), "\", "/"), "/+$", "");
if ispc
tf = strcmpi(normalize(a), normalize(b));
else
tf = normalize(a) == normalize(b);
end
end
Loading
Loading