diff --git a/ratapi/__init__.py b/ratapi/__init__.py index 98683c7e..8f441f44 100644 --- a/ratapi/__init__.py +++ b/ratapi/__init__.py @@ -9,7 +9,7 @@ from ratapi.outputs import BayesResults, Results from ratapi.project import Project from ratapi.run import run -from ratapi.utils import convert, plotting +from ratapi.utils import convert, matlab, plotting with suppress(ImportError): # orsopy is an optional dependency from ratapi.utils import orso as orso @@ -26,4 +26,5 @@ "run", "plotting", "convert", + "matlab", ] diff --git a/ratapi/project.py b/ratapi/project.py index b8ca49f2..2371e24b 100644 --- a/ratapi/project.py +++ b/ratapi/project.py @@ -927,17 +927,15 @@ def classlist_script(name, classlist): + "\n)" ) - def save(self, filepath: str | Path = "./project.json"): - """Save a project to a JSON file. + def model_dump(self): + """Generate a dictionary representation of the model. - Parameters - ---------- - filepath : str or Path - The path to where the project file will be written. + Returns + ------- + model_dict : dict + A dict containing the model information. """ - filepath = Path(filepath).with_suffix(".json") - - json_dict = {} + model_dict = {} for field in self.model_fields: attr = getattr(self, field) @@ -951,7 +949,7 @@ def make_data_dict(item): "simulation_range": item.simulation_range, } - json_dict["data"] = [make_data_dict(data) for data in attr] + model_dict["data"] = [make_data_dict(data) for data in attr] elif field == "custom_files": @@ -960,20 +958,33 @@ def make_custom_file_dict(item): "name": item.name, "filename": item.filename, "language": item.language, - "path": try_relative_to(item.path, filepath.parent), + "path": str(item.path), } if item.name != item.function_name: file_dict["function_name"] = item.function_name return file_dict - json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr] + model_dict["custom_files"] = [make_custom_file_dict(file) for file in attr] elif isinstance(attr, ClassList): - json_dict[field] = [item.model_dump() for item in attr] + model_dict[field] = [item.model_dump() for item in attr] else: - json_dict[field] = attr + model_dict[field] = attr + return model_dict + def save(self, filepath: str | Path = "./project.json"): + """Save a project to a JSON file. + + Parameters + ---------- + filepath : str or Path + The path to where the project file will be written. + """ + filepath = Path(filepath).with_suffix(".json") + json_dict = self.model_dump() + for file in json_dict["custom_files"]: + file["path"] = try_relative_to(file["path"], filepath.parent) filepath.write_text(json.dumps(json_dict)) @classmethod diff --git a/ratapi/utils/matlab.py b/ratapi/utils/matlab.py new file mode 100644 index 00000000..0007c959 --- /dev/null +++ b/ratapi/utils/matlab.py @@ -0,0 +1,115 @@ +"""Runs RAT from the MATLAB API.""" + +import json +import tempfile +import warnings +from pathlib import Path + +from ..outputs import Results +from ..project import Project +from ..wrappers import MatlabWrapper + +RUNNER = """function executeRAT() + +cur_dir = pwd; +cd('{rat_path}'); +addPaths; +cd(cur_dir); + +project = jsonToProject('{project}'); +controls = jsonToControls('{control}'); +customControls = customControl(); +customControls.update(controls); +customControls.filePath = '{ipc_path}'; +if any(strcmpi(controls.procedure, {{procedures.DE.value, procedures.Simplex.value}})) + useLivePlot(1); +end +for i=1:project.customFile.rowCount + addpath(project.customFile.varTable{{i, 5}}); +end +[project, results] = RAT(project, customControls); + +projectToJson(project, '{project}'); +resultsToJson(results, '{result}'); +close all +end +""" + + +CONTROL = """classdef customControl < controlsClass + properties(Hidden = true) + filePath = '' + end + methods + function update(obj, controls) + propNames = properties(controls); + for i = 1:length(propNames) + obj.(propNames{i}) = controls.(propNames{i}); + end + end + function path = getIPCFilePath(obj) + path = obj.filePath; + end + end +end +""" + + +def run_matlab_directly(project, controls, matlab_rat_path, ipc_path="", stdout=None, stderr=None): + """Run User provided MATLAB RAT for the given project and controls inputs. + + Parameters + ---------- + project : RAT.Project or dict + The project model (or equivalent json dict), which defines the physical system under study. + controls : RAT.Controls or dict + The controls model (or equivalent json dict), which defines algorithmic properties. + matlab_rat_path : str + The path to MATLAB RAT folder. + ipc_path : str, optional + IPC path for MATLAB to use + stdout : io.TextIOBase, optional + Text stream for MATLAB console output + stderr : io.TextIOBase, optional + Text stream for MATLAB console error output + """ + if MatlabWrapper.loader is None: + raise ImportError(MatlabWrapper.loader_error_message) from None + + engine = MatlabWrapper.loader.result() + + with tempfile.TemporaryDirectory() as tmp: + project_file = Path(tmp, "project.json") + control_file = Path(tmp, "controls.json") + result_file = Path(tmp, "results.json") + runner_file = Path(tmp, "executeRAT.m") + custom_controls_file = Path(tmp, "customControl.m") + with open(custom_controls_file, "w") as f: + f.write(CONTROL) + + with open(runner_file, "w") as f: + f.write( + RUNNER.format( + project=project_file, + control=control_file, + result=result_file, + rat_path=matlab_rat_path, + ipc_path=ipc_path, + ) + ) + + controls.save(control_file) if not isinstance(controls, dict) else control_file.write_text(json.dumps(controls)) + + with warnings.catch_warnings(): # Avoid warning about relative paths + warnings.simplefilter("ignore") + project.save(project_file) if not isinstance(project, dict) else project_file.write_text( + json.dumps(project) + ) + + engine.addpath(tmp, nargout=0) + engine.executeRAT(nargout=0, stdout=stdout, stderr=stderr) + engine.rmpath(tmp, nargout=0) + + project = Project.load(project_file) + results = Results.load(result_file) + return project, results