diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 009cfcf3e39..d3e8b72eeaf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,10 +66,31 @@ jobs: echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV - name: Set up MATLAB uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0 + - name: Set MATLAB search paths for tests and build MLTBX toolbox + uses: matlab-actions/run-command@v2 + with: + command: | + toolboxPath = fullfile(getenv('CANTERA_ROOT')); + libPath = fullfile(toolboxPath, 'build', 'lib'); + includePath = fullfile(toolboxPath, 'include'); + addpath(fullfile(toolboxPath, 'interfaces', 'matlab_experimental', 'Utility')); + ctPaths(libPath, includePath, toolboxPath); + buildToolbox; - name: Run tests uses: matlab-actions/run-tests@f9fc3d8ca29fadef6227fa52884b144b9011fa2f # v2.1.1 with: select-by-folder: /home/runner/work/cantera/cantera/test/matlab_experimental + - name: Upload Toolbox Artifact + uses: actions/upload-artifact@v4 + with: + name: Cantera_MATLAB_toolbox + path: interfaces/matlab_experimental/*.mltbx + retention-days: 14 + # MLTBX test is disabled until we can download packaged binaries and header files + # - name: Test the MLTBX toolbox + # uses: matlab-actions/run-tests@v2 + # with: + # select-by-folder: /home/runner/work/cantera/cantera/test/matlab_experimental ubuntu-multiple-pythons: name: ${{ matrix.os }} with Python ${{ matrix.python-version }}, Numpy ${{ matrix.numpy || 'latest' }}, Cython ${{ matrix.cython || 'latest' }} diff --git a/interfaces/matlab_experimental/Setup/Cantera_MATLAB_Toolbox.uuid b/interfaces/matlab_experimental/Setup/Cantera_MATLAB_Toolbox.uuid new file mode 100644 index 00000000000..bae5d58205c --- /dev/null +++ b/interfaces/matlab_experimental/Setup/Cantera_MATLAB_Toolbox.uuid @@ -0,0 +1 @@ +419f9e5f-b8d1-4265-b17a-239b6536fdd2 diff --git a/interfaces/matlab_experimental/Setup/buildToolbox.m b/interfaces/matlab_experimental/Setup/buildToolbox.m new file mode 100644 index 00000000000..6bc68b492d0 --- /dev/null +++ b/interfaces/matlab_experimental/Setup/buildToolbox.m @@ -0,0 +1,106 @@ +function buildToolbox(arg) + arguments + arg.version (1,:) char = '0.0.0'; + arg.ctRoot (1,:) char = pwd; + end + + ver = regexp(arg.version, '^\d+\.\d+\.\d+', 'match', 'once'); + + fprintf('Building toolbox version: %s\n', ver); + + % Read the UUID for the toolbox + uuidFile = fullfile(arg.ctRoot, 'interfaces', 'matlab_experimental', 'Setup', ... + 'Cantera_MATLAB_Toolbox.uuid'); + if isfile(uuidFile) + guid = strtrim(fileread(uuidFile)); + fprintf('The unique identifier for the toolbox is: %s\n', guid); + else + error('A unique identifier for the toolbox does not exist!'); + end + + % Define output file + outputFile = fullfile(arg.ctRoot, 'interfaces', 'matlab_experimental', ... + ['Cantera_MATLAB_Toolbox_', ver, '.mltbx']); + + % Create temporary folder for reorganizing and faster execution + tmpDir = fullfile(arg.ctRoot, 'build', 'mltbx'); + mkdir(tmpDir); + mapping = { + 'interfaces/matlab_experimental', 'toolbox'; + 'samples/matlab_experimental', 'samples'; + 'test/matlab_experimental', 'test/matlab_toolbox'; + 'test/data', 'test/data'; + 'data', 'data' + }; + + % Move all files to temporary folder + allFiles = {}; + for i = 1:size(mapping, 1) + src = fullfile(arg.ctRoot, mapping{i, 1}); + dest = fullfile(tmpDir, mapping{i, 2}); + copyfile(src, dest); + files = dir(fullfile(dest, '**', '*')); + files = files(~[files.isdir]); + filesList = fullfile({files.folder}, {files.name})'; + allFiles = [allFiles; filesList]; + end + + % Get relative paths + allPaths = strsplit(genpath(tmpDir), pathsep); + relPaths = cellfun(@(p) erase(p, [arg.ctRoot filesep]), allPaths, 'UniformOutput', false); + relPaths = relPaths(~cellfun(@isempty, relPaths)); + + % Get path to the icon file + iconFile = fullfile(arg.ctRoot, 'doc', 'sphinx', '_static', 'images', 'cantera-logo.png'); + + % Set up toolbox options + opts = matlab.addons.toolbox.ToolboxOptions(tmpDir, guid); + opts.ToolboxName = 'Cantera MATLAB Toolbox'; + opts.ToolboxVersion = ver; + opts.Summary = 'MATLAB interface for Cantera.'; + opts.Description = [ + 'Cantera is an open-source suite of tools for problems involving', ... + 'involving chemical kinetics, thermodynamics, and transport processes.', ... + 'This toolbox includes the MATLAB interface for Cantera, example scripts, and data files.' + ]; + opts.AuthorName = 'Cantera Developers'; % placeholder + opts.AuthorEmail = 'developers@cantera.org'; % placeholder + opts.ToolboxFiles = allFiles; + opts.ToolboxMatlabPath = relPaths; + opts.ToolboxImageFile = iconFile; + opts.MinimumMatlabRelease = 'R2014b'; + opts.OutputFile = outputFile; + opts.SupportedPlatforms.Win64 = true; + opts.SupportedPlatforms.Glnxa64 = true; + opts.SupportedPlatforms.Maci64 = true; + opts.SupportedPlatforms.MatlabOnline = false; + % These options will be enabled when we host Cantera binaries somewhere + % opts.RequiredAdditionalSoftware = [ + % struct( ... + % "Name", "CanteraBinaries", ... + % "Platform", "win64", ... + % "DownloadURL", "placeholder for download url", ... + % "LicenseURL", "placeholder for license url"), + % struct( ... + % "Name", "CanteraBinaries", ... + % "Platform", "maci64", ... + % "DownloadURL", "placeholder for download url", ... + % "LicenseURL", "placeholder for license url"), + % struct( ... + % "Name", "CanteraBinaries", ... + % "Platform", "glnxa64", ... + % "DownloadURL", "placeholder for download url", ... + % "LicenseURL", "placeholder for license url"), + % ]; + + % Package the toolbox + try + matlab.addons.toolbox.packageToolbox(opts); + fprintf('✅ Toolbox built successfully!\n'); + catch ME + fprintf('❌ Toolbox build failed: %s\n', ME.message); + end + + % Remove the temporary folder + rmdir(tmpDir, 's'); +end diff --git a/interfaces/matlab_experimental/Setup/downloadDependencies.m b/interfaces/matlab_experimental/Setup/downloadDependencies.m new file mode 100644 index 00000000000..f30eeff174c --- /dev/null +++ b/interfaces/matlab_experimental/Setup/downloadDependencies.m @@ -0,0 +1,36 @@ +function downloadDependencies() + % This file may not be necessary once we set up the MLTBX properly + + os = lower(computer); + % Get platform-specific binaries + switch os + case {'pcwin64', 'pcwin'} + platform = 'windows'; + case {'maci64'} + platform = 'macos'; + case {'glnxa64', 'glnx86'} + platform = 'linux'; + otherwise + error('Unsupported platform: %s', os); + end + + % Placeholder URL for ZIP files containing the binaries and headers + baseUrl = 'https://cantera.com/downloads'; + zipName = sprintf('cantera-binaries-%s.zip', platform); + url = fullfile(baseUrl, zipName); + + % Set target install folder in user path + installDir = fullfile(userpath, 'Dependencies'); + if ~isfolder(installDir) + fprintf('Downloading dependencies for %s...\n', platform); + zipFile = fullfile(tempdir, zipName); + websave(zipFile, url); + unzip(zipFile, installDir); + fprintf('Installed to: %s\n', installDir); + else + fprintf('Dependencies already installed at: %s\n', installDir); + end + + % Add to path + addpath(genpath(installDir)); +end diff --git a/interfaces/matlab_experimental/Utility/ctLoad.m b/interfaces/matlab_experimental/Utility/ctLoad.m index cdedd769c0c..783910292b5 100644 --- a/interfaces/matlab_experimental/Utility/ctLoad.m +++ b/interfaces/matlab_experimental/Utility/ctLoad.m @@ -1,32 +1,39 @@ function ctLoad() + % ctLoad % Load the Cantera C Library into Memory + paths = ctPaths(); + + if any(cellfun(@isempty, {paths.libPath, paths.includePath, paths.toolboxPath})) + error('ctLoad:MissingPath', ... + 'Library, header, and toolbox paths must be configured with ctPaths(libPath,includePath, toolboxPath).'); + end + if ispc - ctName = '/bin/cantera_shared.dll'; + libName = 'cantera_shared.dll'; elseif ismac - ctName = '/Lib/libcantera_shared.dylib'; + libName = 'libcantera_shared.dylib'; elseif isunix - ctName = '/lib/libcantera_shared.so'; + libName = 'libcantera_shared.so'; else - error('Operating System Not Supported!'); - return; + error('ctLoad:UnsupportedPlatform', 'Operating system not supported.'); end - root = ctRoot; - if ispc - root = [ctRoot, '/Library']; - end + fullLibPath = fullfile(paths.libPath, libName); + fullHeaderPath = fullfile(paths.includePath, 'cantera', 'clib', 'ctmatlab.h'); if ~libisloaded(ctLib) - [~, warnings] = loadlibrary([root, ctName], ... - [root, '/include/cantera/clib/ctmatlab.h'], ... - 'includepath', [root, '/include'], ... - 'addheader', 'ct', 'addheader', 'ctfunc', ... - 'addheader', 'ctmultiphase', 'addheader', ... - 'ctonedim', 'addheader', 'ctreactor', ... - 'addheader', 'ctrpath', 'addheader', 'ctsurf'); + [~, warnings] = loadlibrary(fullLibPath, fullHeaderPath, ... + 'includepath', paths.includePath, ... + 'addheader', 'ct', ... + 'addheader', 'ctfunc', ... + 'addheader', 'ctmultiphase', ... + 'addheader', 'ctonedim', ... + 'addheader', 'ctreactor', ... + 'addheader', 'ctrpath', ... + 'addheader', 'ctsurf'); end - disp(sprintf('Cantera %s is ready for use.', ctVersion)) + fprintf('Cantera %s is ready for use.\n', ctVersion); end diff --git a/interfaces/matlab_experimental/Utility/ctPaths.m b/interfaces/matlab_experimental/Utility/ctPaths.m new file mode 100644 index 00000000000..d5d95c795fc --- /dev/null +++ b/interfaces/matlab_experimental/Utility/ctPaths.m @@ -0,0 +1,73 @@ +function paths = ctPaths(varargin) + % ctPaths :: + % Configure or retrieve the library/header/toolbox paths for Cantera. + % The paths are stored as MATLAB preferences. + % + % >> ctPaths() % Get current config as struct + % >> ctPaths(libPath, includePath) % Set library/header/toolbox paths + % >> ctPaths('clear') % Clear saved paths + % + % :return: + % paths: struct with fields 'libPath', 'includePath', and + % 'toolboxPath' + + if nargin == 1 && strcmp(varargin{1}, 'clear') + if ispref('Cantera', 'Paths') + paths = getpref('Cantera', 'Paths'); + subDirs = strsplit(genpath(paths.toolboxPath), pathsep); + currentPaths = strsplit(path, pathsep); + subdirsToRemove = intersect(subDirs, currentPaths); + if ~isempty(subdirsToRemove) + rmpath(subdirsToRemove{:}); + end + rmpref('Cantera', 'Paths'); + end + paths = struct('libPath', '', 'includePath', '', 'toolboxPath', ''); + return + elseif nargin == 3 && all(cellfun(@ischar, varargin)) + paths = struct('libPath', varargin{1}, ... + 'includePath', varargin{2}, ... + 'toolboxPath', varargin{3}); + setpref('Cantera', 'Paths', paths); + else + % Load from saved preferences if available + if ispref('Cantera', 'Paths') + paths = getpref('Cantera', 'Paths'); + return + else + paths = struct('libPath', '', 'includePath', '', 'toolboxPath', ''); + end + end + + if any(cellfun(@isempty, {paths.libPath, paths.includePath, paths.toolboxPath})) + error('ctPaths:MissingPath', ... + 'Library, header, and toolbox paths must be configured with ctPaths(libPath,includePath, toolboxPath).'); + end + + mapping = { + 'interfaces/matlab_experimental', 'toolbox'; + 'samples/matlab_experimental', 'samples'; + 'test/matlab_experimental', 'test/matlab_toolbox'; + 'test/data', 'test/data'; + 'data', 'data' + }; + + % Check whether user is using Cantera source code or MLTBX based on folder structure + if isfolder(fullfile(paths.toolboxPath, 'interfaces')) + col = 1; + else + col = 2; + end + + for i = 1:size(mapping, 1) + subdir = fullfile(paths.toolboxPath, mapping{i, col}); + if isfolder(subdir) + addpath(genpath(subdir)); + else + warning('ctPaths:MissingDirectory', ... + 'Directory not found: %s', subdir); + end + end + + savepath(); +end diff --git a/test/matlab_experimental/ctTestPath.m b/test/matlab_experimental/ctTestPath.m deleted file mode 100644 index f7fa212efc0..00000000000 --- a/test/matlab_experimental/ctTestPath.m +++ /dev/null @@ -1,23 +0,0 @@ -% ctTestPath.m -% Set up environment for testing the Cantera Matlab interface -% from within the Cantera source tree. Run this file from the -% root of the Cantera source tree, for example: -% -% cd ~/src/cantera -% run interfaces/matlab_experimental/testpath.m - -% get the list of directories on the Matlab path -dirs = split(path, pathsep); - -% if 'cantera' is already in the path, remove it -for i = 1:length(dirs) - if contains(dirs{i}, 'CANTERA', 'IgnoreCase', true) - rmpath(dirs{i}); - end -end - -cantera_root = getenv('CANTERA_ROOT'); - -% Add the Cantera toolbox to the Matlab path -addpath(genpath([cantera_root, '/interfaces/matlab_experimental'])); -addpath(genpath([cantera_root, '/test/matlab_experimental'])); diff --git a/test/matlab_experimental/ctTestSetUp.m b/test/matlab_experimental/ctTestSetUp.m index 9590f149da7..c58f82d71e1 100644 --- a/test/matlab_experimental/ctTestSetUp.m +++ b/test/matlab_experimental/ctTestSetUp.m @@ -1,24 +1,3 @@ -clear all - -% Copy library to test folder -ctTestPath; - -if ispc - ctName = '/build/lib/cantera_shared.dll'; -elseif ismac - ctName = '/build/lib/libcantera_shared.dylib'; -elseif isunix - ctName = '/build/lib/libcantera_shared.so'; +function ctTestSetUp() + ctLoad(); end -% Load Cantera -if ~libisloaded('libcantera_shared') - [~, warnings] = loadlibrary([cantera_root, ctName], ... - [cantera_root, '/include/cantera/clib/ctmatlab.h'], ... - 'includepath', [cantera_root, '/include'], ... - 'addheader', 'ct', 'addheader', 'ctfunc', ... - 'addheader', 'ctmultiphase', 'addheader', ... - 'ctonedim', 'addheader', 'ctreactor', ... - 'addheader', 'ctrpath', 'addheader', 'ctsurf'); -end - -disp('Cantera is loaded for test'); diff --git a/test/matlab_experimental/ctTestTearDown.m b/test/matlab_experimental/ctTestTearDown.m index bab612e0866..2174435ec89 100644 --- a/test/matlab_experimental/ctTestTearDown.m +++ b/test/matlab_experimental/ctTestTearDown.m @@ -1,10 +1,3 @@ -clear all -% Unload Cantera -if ispc - lib = 'cantera_shared'; -else - lib = 'libcantera_shared'; +function ctTestTearDown() + ctUnload(); end - -unloadlibrary(lib); -disp('Cantera has been unloaded');