Skip to content

Commit 2e059b9

Browse files
authored
Add function to run directly from MATLAB API (#205)
1 parent f450266 commit 2e059b9

3 files changed

Lines changed: 281 additions & 15 deletions

File tree

ratapi/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ratapi.outputs import BayesResults, Results
1010
from ratapi.project import Project
1111
from ratapi.run import run
12-
from ratapi.utils import convert, plotting
12+
from ratapi.utils import convert, matlab, plotting
1313

1414
with suppress(ImportError): # orsopy is an optional dependency
1515
from ratapi.utils import orso as orso
@@ -26,4 +26,5 @@
2626
"run",
2727
"plotting",
2828
"convert",
29+
"matlab",
2930
]

ratapi/project.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -927,17 +927,15 @@ def classlist_script(name, classlist):
927927
+ "\n)"
928928
)
929929

930-
def save(self, filepath: str | Path = "./project.json"):
931-
"""Save a project to a JSON file.
930+
def to_dict(self):
931+
"""Generate a dictionary representation of the model.
932932
933-
Parameters
934-
----------
935-
filepath : str or Path
936-
The path to where the project file will be written.
933+
Returns
934+
-------
935+
model_dict : dict
936+
A dict containing the model information.
937937
"""
938-
filepath = Path(filepath).with_suffix(".json")
939-
940-
json_dict = {}
938+
model_dict = {}
941939
for field in self.model_fields:
942940
attr = getattr(self, field)
943941

@@ -951,7 +949,7 @@ def make_data_dict(item):
951949
"simulation_range": item.simulation_range,
952950
}
953951

954-
json_dict["data"] = [make_data_dict(data) for data in attr]
952+
model_dict["data"] = [make_data_dict(data) for data in attr]
955953

956954
elif field == "custom_files":
957955

@@ -960,20 +958,33 @@ def make_custom_file_dict(item):
960958
"name": item.name,
961959
"filename": item.filename,
962960
"language": item.language,
963-
"path": try_relative_to(item.path, filepath.parent),
961+
"path": str(item.path),
964962
}
965963
if item.name != item.function_name:
966964
file_dict["function_name"] = item.function_name
967965

968966
return file_dict
969967

970-
json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr]
968+
model_dict["custom_files"] = [make_custom_file_dict(file) for file in attr]
971969

972970
elif isinstance(attr, ClassList):
973-
json_dict[field] = [item.model_dump() for item in attr]
971+
model_dict[field] = [item.model_dump() for item in attr]
974972
else:
975-
json_dict[field] = attr
973+
model_dict[field] = attr
974+
return model_dict
976975

976+
def save(self, filepath: str | Path = "./project.json"):
977+
"""Save a project to a JSON file.
978+
979+
Parameters
980+
----------
981+
filepath : str or Path
982+
The path to where the project file will be written.
983+
"""
984+
filepath = Path(filepath).with_suffix(".json")
985+
json_dict = self.to_dict()
986+
for file in json_dict["custom_files"]:
987+
file["path"] = try_relative_to(file["path"], filepath.parent)
977988
filepath.write_text(json.dumps(json_dict))
978989

979990
@classmethod

ratapi/utils/matlab.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
"""Runs RAT from the MATLAB API."""
2+
3+
import json
4+
import tempfile
5+
import warnings
6+
from pathlib import Path
7+
8+
import numpy as np
9+
10+
from ..events import EventTypes, PlotEventData, ProgressEventData, notify
11+
from ..outputs import Results
12+
from ..project import Project
13+
from ..wrappers import MatlabWrapper
14+
15+
RUNNER = """function executeRAT()
16+
17+
cur_dir = pwd;
18+
cd('{rat_path}');
19+
addPaths;
20+
cd(cur_dir);
21+
22+
project = jsonToProject('{project}');
23+
controls = jsonToControls('{control}');
24+
customControls = customControl();
25+
customControls.update(controls);
26+
customControls.filePath = '{ipc_path}';
27+
28+
for i=1:project.customFile.rowCount
29+
addpath(project.customFile.varTable{{i, 5}});
30+
end
31+
eventManager.register(eventTypes.Message, @(x) logger('{msg_log_path}', x));
32+
eventManager.register(eventTypes.Progress, @(x) logger('{progress_log_path}', x));
33+
eventManager.register(eventTypes.Plot, @(x) logger('{plot_log_path}', x));
34+
global RAT_PROGRESS_UPDATE_FREQ RAT_PROGRESS_UPDATE_COUNT
35+
RAT_PROGRESS_UPDATE_FREQ = {progress_event_freq};
36+
RAT_PROGRESS_UPDATE_COUNT = -1;
37+
38+
[project, results] = RAT(project, customControls);
39+
40+
projectToJson(project, '{project}');
41+
resultsToJson(results, '{result}');
42+
eventManager.clear();
43+
close all
44+
end
45+
"""
46+
47+
48+
CONTROL = """classdef customControl < controlsClass
49+
properties(Hidden = true)
50+
filePath = ''
51+
end
52+
methods
53+
function update(obj, controls)
54+
propNames = properties(controls);
55+
for i = 1:length(propNames)
56+
obj.(propNames{i}) = controls.(propNames{i});
57+
end
58+
end
59+
function path = getIPCFilePath(obj)
60+
path = obj.filePath;
61+
end
62+
end
63+
end
64+
"""
65+
66+
LOGGER = """function logger(logPath, data)
67+
68+
if isstruct(data)
69+
entry = plotDataToJson(data);
70+
elseif iscell(data)
71+
global RAT_PROGRESS_UPDATE_FREQ RAT_PROGRESS_UPDATE_COUNT;
72+
RAT_PROGRESS_UPDATE_COUNT = RAT_PROGRESS_UPDATE_COUNT + 1;
73+
if rem(RAT_PROGRESS_UPDATE_COUNT, RAT_PROGRESS_UPDATE_FREQ) ~= 0
74+
return
75+
end
76+
entry = [data{1}, ',', num2str(data{2})];
77+
else
78+
entry = strip(data, 'right');
79+
end
80+
fid = fopen(logPath, "a");
81+
cleanup = onCleanup(@() fclose(fid));
82+
fprintf(fid, "%s\\n", entry);
83+
end
84+
85+
function encoded = plotDataToJson(data)
86+
% Encodes the results into a json file...
87+
tmpResults = struct();
88+
for fn = fieldnames(data)'
89+
tmpResults.(fn{1}) = data.(fn{1});
90+
end
91+
92+
tmpResults.reflectivity = correctCellArray(tmpResults.reflectivity);
93+
tmpResults.shiftedData = correctCellArray(tmpResults.shiftedData);
94+
tmpResults.sldProfiles = makeCellJson(tmpResults.sldProfiles);
95+
tmpResults.resampledLayers = makeCellJson(tmpResults.resampledLayers);
96+
97+
encoded = jsonencode(tmpResults,ConvertInfAndNaN=false);
98+
encoded = strrep(encoded, ']"', ']');
99+
encoded = strrep(encoded, '"[', '[');
100+
end
101+
102+
function outputArray = makeCellJson(cellArray)
103+
% The jsonencode function flattens 2d cell arrays this is a workaround to
104+
% avoid flattening by converting to a string array with is not flattened.
105+
[row, col] = size(cellArray, [1, 2]);
106+
outputArray = strings([row, col]);
107+
for i=1:row
108+
for j=1:col
109+
entry = cellArray{i, j};
110+
if size(entry, 1) == 1
111+
entry = {entry};
112+
end
113+
if col == 1
114+
entry = {entry};
115+
end
116+
outputArray(i, j) = jsonencode(entry);
117+
end
118+
end
119+
if row == 1
120+
outputArray = {outputArray};
121+
end
122+
123+
end
124+
125+
function cellArray = correctCellArray(cellArray)
126+
% Corrects array with single row so its written as 2D array in json
127+
[row, col] = size(cellArray, [1, 2]);
128+
for i=1:row
129+
for j=1:col
130+
if size(cellArray{i, j}, 1) == 1
131+
cellArray{i, j} = {cellArray{i, j}};
132+
end
133+
end
134+
end
135+
end
136+
"""
137+
138+
139+
def run_matlab_directly(
140+
project, controls, matlab_rat_path, ipc_path="", stdout=None, stderr=None, progress_event_freq=10
141+
):
142+
"""Run User provided MATLAB RAT for the given project and controls inputs.
143+
144+
Parameters
145+
----------
146+
project : RAT.Project or dict
147+
The project model (or equivalent json dict), which defines the physical system under study.
148+
controls : RAT.Controls or dict
149+
The controls model (or equivalent json dict), which defines algorithmic properties.
150+
matlab_rat_path : str
151+
The path to MATLAB RAT folder.
152+
ipc_path : str, optional
153+
IPC path for MATLAB to use.
154+
stdout : io.TextIOBase, optional
155+
Text stream for MATLAB console output.
156+
stderr : io.TextIOBase, optional
157+
Text stream for MATLAB console error output.
158+
progress_event_freq : int, default: 10
159+
Update frequency of the progress event.
160+
"""
161+
if MatlabWrapper.loader is None:
162+
raise ImportError(MatlabWrapper.loader_error_message) from None
163+
164+
engine = MatlabWrapper.loader.result()
165+
166+
with tempfile.TemporaryDirectory() as tmp:
167+
project_file = Path(tmp, "project.json")
168+
control_file = Path(tmp, "controls.json")
169+
result_file = Path(tmp, "results.json")
170+
runner_file = Path(tmp, "executeRAT.m")
171+
custom_controls_file = Path(tmp, "customControl.m")
172+
msg_log_file = Path(tmp, "runner_msg_log.txt")
173+
progress_log_file = Path(tmp, "runner_progress_log.txt")
174+
plot_log_file = Path(tmp, "runner_plot_log.txt")
175+
Path(tmp, "logger.m").write_text(LOGGER)
176+
with open(custom_controls_file, "w") as f:
177+
f.write(CONTROL)
178+
179+
with open(runner_file, "w") as f:
180+
f.write(
181+
RUNNER.format(
182+
project=project_file,
183+
control=control_file,
184+
result=result_file,
185+
rat_path=matlab_rat_path,
186+
ipc_path=ipc_path,
187+
msg_log_path=msg_log_file,
188+
progress_log_path=progress_log_file,
189+
plot_log_path=plot_log_file,
190+
progress_event_freq=progress_event_freq,
191+
)
192+
)
193+
194+
controls.save(control_file) if not isinstance(controls, dict) else control_file.write_text(json.dumps(controls))
195+
196+
with warnings.catch_warnings(): # Avoid warning about relative paths
197+
warnings.simplefilter("ignore")
198+
project.save(project_file) if not isinstance(project, dict) else project_file.write_text(
199+
json.dumps(project)
200+
)
201+
202+
engine.addpath(tmp, nargout=0)
203+
future = engine.executeRAT(nargout=0, stdout=stdout, stderr=stderr, background=True)
204+
msg_cur_line = 0
205+
plot_cur_line = 0
206+
progress_cur_line = 0
207+
while not future.done():
208+
if msg_log_file.exists():
209+
with open(msg_log_file, encoding="utf-8") as handle:
210+
handle.seek(msg_cur_line)
211+
text = handle.read()
212+
msg_cur_line = handle.tell()
213+
if text:
214+
notify(EventTypes.Message, text)
215+
if progress_log_file.exists():
216+
with open(progress_log_file, encoding="utf-8") as handle:
217+
handle.seek(progress_cur_line)
218+
lines = handle.readlines()
219+
progress_cur_line = handle.tell()
220+
if lines:
221+
msg, percent = lines[-1].strip().rsplit(",", 1)
222+
progress_data = ProgressEventData()
223+
progress_data.message = msg
224+
progress_data.percent = float(percent)
225+
notify(EventTypes.Progress, progress_data)
226+
if plot_log_file.exists():
227+
with open(plot_log_file, encoding="utf-8") as handle:
228+
handle.seek(plot_cur_line)
229+
lines = handle.readlines()
230+
plot_cur_line = handle.tell()
231+
if lines:
232+
plot_data = PlotEventData()
233+
plot_json = json.loads(lines[-1])
234+
plot_data.modelType = plot_json["modelType"]
235+
plot_data.reflectivity = [np.array(ref) for ref in plot_json["reflectivity"]]
236+
plot_data.shiftedData = [np.array(sd) for sd in plot_json["shiftedData"]]
237+
plot_data.sldProfiles = [
238+
[np.array(prof) for prof in profiles] for profiles in plot_json["sldProfiles"]
239+
]
240+
plot_data.resampledLayers = [
241+
[np.array(lay) for lay in layers] for layers in plot_json["resampledLayers"]
242+
]
243+
plot_data.dataPresent = plot_json["dataPresent"]
244+
plot_data.subRoughs = plot_json["subRoughs"]
245+
plot_data.resample = plot_json["resample"]
246+
plot_data.contrastNames = plot_json["contrastNames"]
247+
notify(EventTypes.Plot, plot_data)
248+
engine.rmpath(tmp, nargout=0)
249+
if future.result() is not None:
250+
raise RuntimeError(future.result())
251+
252+
project = Project.load(project_file)
253+
results = Results.load(result_file)
254+
return project, results

0 commit comments

Comments
 (0)