From 07b4f2a8c3c36665a3d909fb7fba80997082766c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 2 Jun 2026 12:52:36 +0100 Subject: [PATCH 1/3] UI changes to import ORSO and Rascal1 with same dialog --- rascal2/dialogs/startup_dialog.py | 27 ++++++++++++++++++++------- rascal2/ui/view.py | 22 +++++++++++++++++----- rascal2/widgets/startup.py | 4 ++-- tests/dialogs/test_project_dialog.py | 14 ++++++++++---- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/rascal2/dialogs/startup_dialog.py b/rascal2/dialogs/startup_dialog.py index d61d3a11..66e4d567 100644 --- a/rascal2/dialogs/startup_dialog.py +++ b/rascal2/dialogs/startup_dialog.py @@ -406,22 +406,28 @@ def block_for_worker(self, disabled: bool): self.loading_bar.setVisible(disabled) -class LoadR1Dialog(StartupDialog): +class ImportProjectDialog(StartupDialog): """Dialog to load a RasCAL-1 project.""" - def __init__(self, parent): + def __init__(self, parent, file_extension): + if file_extension == "*.mat": + self.project_type = "RasCAL-1" + elif file_extension == "*.ort": + self.project_type = "ORSO" + # our 'folder selector' is actually a .mat file selector in this case self.folder_selector = lambda p, _: QtWidgets.QFileDialog.getOpenFileName( - p, "Select RasCAL-1 File", filter="*.mat" + p, "Select Project File", filter=file_extension )[0] super().__init__(parent) + print(file_extension) def create_form(self, form_layout): - self.setWindowTitle("Load RasCAL-1 Project") + self.setWindowTitle(f"Import {self.project_type} Project") super().create_form(form_layout) - self.project_folder_label.setText("RasCAL-1 file:") - self.project_folder.setPlaceholderText("Select RasCAL-1 file") + self.project_folder_label.setText(f"{self.project_type} file:") + self.project_folder.setPlaceholderText(f"Select {self.project_type} Project file") def create_buttons(self): load_button = QtWidgets.QPushButton("Load", objectName="LoadButton") @@ -432,12 +438,19 @@ def create_buttons(self): @staticmethod def verify_folder(file_path: str): if not os.access(file_path, os.R_OK): - raise ValueError("You do not have permission to read this RasCAL-1 project.") + raise ValueError("You do not have permission to read this project.") if not os.access(Path(file_path).parent, os.W_OK): raise ValueError("You do not have permission to create a project in this folder.") def load_project(self): """Load the project if inputs are valid.""" + if ".ort" in self.project_folder.text(): + print("found ort file") + elif ".mat" in self.project_folder.text(): + self.load_r1_project() + + def load_r1_project(self): + """Load the RasCAL-1 project if inputs are valid.""" if self.project_folder.text() == "": self.set_folder_error("Please specify a project file.") if self.project_folder_error.isHidden(): diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 2420abe0..fed87a30 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -6,7 +6,13 @@ from rascal2.core.enums import UnsavedReply from rascal2.dialogs.about_dialog import AboutDialog from rascal2.dialogs.settings_dialog import SettingsDialog -from rascal2.dialogs.startup_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog, StartupDialog +from rascal2.dialogs.startup_dialog import ( + PROJECT_FILES, + ImportProjectDialog, + LoadDialog, + NewProjectDialog, + StartupDialog, +) from rascal2.settings import MDIGeometries, get_global_settings from rascal2.widgets import ControlsWidget, PlotWidget, TerminalWidget from rascal2.widgets.project import ProjectWidget @@ -62,7 +68,7 @@ def closeEvent(self, event): else: event.ignore() - def show_project_dialog(self, dialog: StartupDialog): + def show_project_dialog(self, dialog: StartupDialog, *args): """Show a startup dialog of a given type. Parameters @@ -74,7 +80,7 @@ def show_project_dialog(self, dialog: StartupDialog): self.startup_dlg.hide() if self.presenter.ask_to_save_project(): - project_dlg = dialog(self) + project_dlg = dialog(self, *args) project_dlg.show() def show_settings_dialog(self, tab_name=""): @@ -109,7 +115,11 @@ def create_actions(self): self.open_r1_action = QtGui.QAction("Open &RasCAL-1 Project") self.open_r1_action.setStatusTip("Open a RasCAL-1 project") - self.open_r1_action.triggered.connect(lambda: self.show_project_dialog(LoadR1Dialog)) + self.open_r1_action.triggered.connect(lambda: self.show_project_dialog(ImportProjectDialog, "*.mat")) + + self.actionImportORT = QtGui.QAction("Import ORSO (.ort)…", self) + self.actionImportORT.setStatusTip("Import an ORSO .ort file (data + model)") + self.actionImportORT.triggered.connect(lambda: self.show_project_dialog(ImportProjectDialog, "*.ort")) self.save_project_action = QtGui.QAction("&Save", self) self.save_project_action.setStatusTip("Save project") @@ -213,7 +223,9 @@ def create_menus(self): file_menu.addAction(self.new_project_action) file_menu.addSeparator() file_menu.addAction(self.open_project_action) - file_menu.addAction(self.open_r1_action) + file_submenu_import = file_menu.addMenu("&Import Project") + file_submenu_import.addAction(self.open_r1_action) + file_submenu_import.addAction(self.actionImportORT) file_menu.addSeparator() file_menu.addAction(self.save_project_action) file_menu.addAction(self.save_as_action) diff --git a/rascal2/widgets/startup.py b/rascal2/widgets/startup.py index 960d2a63..39bf0a1f 100644 --- a/rascal2/widgets/startup.py +++ b/rascal2/widgets/startup.py @@ -1,7 +1,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from rascal2.config import path_for -from rascal2.dialogs.startup_dialog import LoadDialog, LoadR1Dialog, NewProjectDialog +from rascal2.dialogs.startup_dialog import ImportProjectDialog, LoadDialog, NewProjectDialog class StartUpWidget(QtWidgets.QWidget): @@ -66,7 +66,7 @@ def create_buttons(self) -> None: self.import_project_button.clicked.connect(lambda: self.parent().show_project_dialog(LoadDialog)) self.import_r1_button = QtWidgets.QToolButton(objectName="ImportR1Button") - self.import_r1_button.clicked.connect(lambda: self.parent().show_project_dialog(LoadR1Dialog)) + self.import_r1_button.clicked.connect(lambda: self.parent().show_project_dialog(ImportProjectDialog)) def create_labels(self) -> None: """Create labels.""" diff --git a/tests/dialogs/test_project_dialog.py b/tests/dialogs/test_project_dialog.py index 1c9b1d80..6699af2d 100644 --- a/tests/dialogs/test_project_dialog.py +++ b/tests/dialogs/test_project_dialog.py @@ -5,7 +5,13 @@ import pytest from PyQt6 import QtCore, QtWidgets -from rascal2.dialogs.startup_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog, StartupDialog +from rascal2.dialogs.startup_dialog import ( + PROJECT_FILES, + ImportProjectDialog, + LoadDialog, + NewProjectDialog, + StartupDialog, +) class MockParentWindow(QtWidgets.QMainWindow): @@ -25,7 +31,7 @@ def __init__(self): ( [NewProjectDialog, 1], [LoadDialog, 1], - [LoadR1Dialog, 1], + [ImportProjectDialog, 1], ), ) def test_project_dialog_initial_state(dialog, num_widgets): @@ -42,7 +48,7 @@ def test_project_dialog_initial_state(dialog, num_widgets): assert project_dialog.project_name_error.text() == "Project name needs to be specified." assert project_dialog.project_name_error.isHidden() - if dialog == LoadR1Dialog: + if dialog == ImportProjectDialog: assert project_dialog.project_folder.placeholderText() == "Select RasCAL-1 file" assert project_dialog.project_folder_label.text() == "RasCAL-1 file:" else: @@ -75,7 +81,7 @@ def test_create_button(name, name_valid, folder, folder_valid, other_folder_erro mock_create.assert_not_called() -@pytest.mark.parametrize("widget", [LoadDialog, LoadR1Dialog]) +@pytest.mark.parametrize("widget", [LoadDialog, ImportProjectDialog]) @pytest.mark.parametrize("folder, folder_valid", [("", False), ("Folder", True)]) @pytest.mark.parametrize("other_folder_error", [True, False]) @patch("rascal2.dialogs.startup_dialog.Worker", autospec=True) From 712c595dfa082a5d95c79d78882f3b265c069d22 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 3 Jun 2026 12:49:40 +0100 Subject: [PATCH 2/3] added ORSO Importer with relevant bilayer utility functions --- rascal2/core/bilayer_utils.py | 122 ++++++++++ rascal2/core/orso_importer.py | 364 ++++++++++++++++++++++++++++++ rascal2/dialogs/startup_dialog.py | 31 ++- rascal2/ui/model.py | 30 +++ rascal2/ui/presenter.py | 43 ++++ requirements.txt | 2 + tests/ui/test_view.py | 2 +- 7 files changed, 586 insertions(+), 8 deletions(-) create mode 100644 rascal2/core/bilayer_utils.py create mode 100644 rascal2/core/orso_importer.py diff --git a/rascal2/core/bilayer_utils.py b/rascal2/core/bilayer_utils.py new file mode 100644 index 00000000..6de3a47d --- /dev/null +++ b/rascal2/core/bilayer_utils.py @@ -0,0 +1,122 @@ +"""Utilities for parsing bilayer(...) stack tokens in ORSO models.""" + +from __future__ import annotations + +import re + +import molgroups.lipids as lipids +import numpy as np + +RE_BILAYER = re.compile(r"""^bilayer\s*\(\s*inner\s*=\s*([A-Za-z0-9_]+)\s*,\s*outer\s*=\s*([A-Za-z0-9_]+)\s*\)\s*$""") + + +def scalar_nsl(x): + """Convert molgroups nSLs (scalar or array) to a single float.""" + if isinstance(x, list): + arr = np.array(x) + return float(np.sum(arr)) + else: + return float(x) + + +def get_lipid_constants(lipid_name: str): + """Get head/tail volumes and SLDs for a lipid from molgroups.lipids.""" + obj = getattr(lipids, lipid_name, None) + if obj is None: + obj = lipids.DPPC + + try: + head_components = obj.headgroup[1]["components"] + head_vol = sum(getattr(c, "cell_volume", 0.0) for c in head_components) + head_nsl = scalar_nsl([getattr(c, "nSLs", 0.0) for c in head_components]) + except Exception: + head_vol = 0.0 + head_nsl = 0.0 + + if head_vol <= 0: + head_vol = float(getattr(obj, "headgroup_volume", 0.0) or 0.0) + if head_vol <= 0: + head_vol = 330.0 + + head_sld = 0.0 + if head_vol > 0 and head_nsl != 0: + head_sld = head_nsl * 1e-5 / head_vol + + try: + tail = obj.tails + tail_vol = float(getattr(tail, "cell_volume", 0.0) or 0.0) + tail_nsl = scalar_nsl(getattr(tail, "nSLs", 0.0)) + except Exception: + tail_vol = 0.0 + tail_nsl = 0.0 + if tail_vol <= 0: + tail_vol = 800.0 + + tail_sld = 0.0 + if tail_vol > 0 and tail_nsl != 0: + tail_sld = tail_nsl * 1e-5 / tail_vol + + return { + "name": lipid_name, + "head_vol": float(head_vol), + "head_sld": float(head_sld), + "tail_vol": float(tail_vol), + "tail_sld": float(tail_sld), + } + + +def extract_bilayers_from_model(model): + """Extract bilayer(inner=XXX, outer=YYY) tokens from model.stack.""" + stack = getattr(model, "stack", "") + tokens = [t.strip() for t in stack.split("|")] + + bilayers = [] + kept = [] + for t in tokens: + m = RE_BILAYER.match(t) + if m: + bilayers.append({"inner": m.group(1), "outer": m.group(2)}) + else: + kept.append(t) + + model.stack = " | ".join(kept) + return bilayers + + +def _flatten_lipid(prefix: str, consts): + """Expand molgroups lipid constants into flat keys with fallback.""" + if consts is None: + return { + f"v_head_{prefix}": 300.0, + f"v_tail_{prefix}": 800.0, + f"sld_head_{prefix}": 1e-6, + f"sld_tail_{prefix}": 1e-6, + } + return { + f"v_head_{prefix}": consts["head_vol"], + f"sld_head_{prefix}": consts["head_sld"], + f"v_tail_{prefix}": consts["tail_vol"], + f"sld_tail_{prefix}": consts["tail_sld"], + } + + +def build_bilayer_specs(bilayer_specs_raw): + """Build enriched bilayer constants from parsed bilayer stack tokens.""" + bilayer_specs = [] + if not bilayer_specs_raw: + return bilayer_specs + + for spec in bilayer_specs_raw: + inner = spec["inner"] + outer = spec["outer"] + inner_consts = get_lipid_constants(inner) + outer_consts = get_lipid_constants(outer) + bilayer_specs.append( + { + "inner": inner, + "outer": outer, + **_flatten_lipid("inner", inner_consts), + **_flatten_lipid("outer", outer_consts), + } + ) + return bilayer_specs diff --git a/rascal2/core/orso_importer.py b/rascal2/core/orso_importer.py new file mode 100644 index 00000000..7e5d912b --- /dev/null +++ b/rascal2/core/orso_importer.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +import shutil +from pathlib import Path + +import numpy as np +import ratapi as rat +from orsopy.fileio import load_orso +from ratapi.models import CustomFile, Data, Layer, Parameter + +from rascal2.core.bilayer_utils import build_bilayer_specs, extract_bilayers_from_model + +# ----------------------------------------------------------------------------- +# Constants / helpers +# ----------------------------------------------------------------------------- + +KNOWN_BULKS = ["D2O", "H2O", "AuMW", "SiMW", "SMW", "Si", "Air"] + + +def _sanitize_name(s: str, fallback: str) -> str: + s = str(s or "").strip() + s = " ".join(s.split()) + return s if s else fallback + + +def _ensure_dir(p: Path) -> None: + p.mkdir(parents=True, exist_ok=True) + + +def _safe_get_sld(material) -> float: + if material is None: + return 0.0 + try: + if hasattr(material, "get_sld"): + return float(material.get_sld().real) + except Exception: + pass + return 0.0 + + +def _infer_bulk_name_from_layer(layer, fallback: str) -> str: + mat = getattr(layer, "material", None) + for src in ( + getattr(mat, "name", None), + getattr(layer, "original_name", None), + getattr(mat, "formula", None), + ): + if isinstance(src, str): + for kb in KNOWN_BULKS: + if kb.lower() in src.lower(): + return kb + return fallback + + +def _infer_bulk_name_from_text(text: str, fallback: str) -> str: + s = (text or "").lower() + for kb in KNOWN_BULKS: + if kb.lower() in s: + return kb + return fallback + + +def _ensure_bulk_parameter_exists( + project: rat.Project, + which: str, + bulk_name: str, + sld_value: float, +) -> str: + ref = bulk_name if bulk_name.lower().startswith("sld ") else f"SLD {bulk_name}" + table = project.bulk_in if which == "in" else project.bulk_out + + for row in table: + if row.name == ref: + if sld_value != 0.0: + v = float(sld_value) + row.min = min(float(row.min), v) + row.max = max(float(row.max), v) + row.value = v + return ref + + rowcls = table[0].__class__ if table else None + if rowcls is None: + return ref + + if sld_value != 0.0: + v = float(sld_value) + mn, mx = v * 0.95, v * 1.05 + else: + v, mn, mx = 0.0, -1e-6, 1e-6 + + payload = dict(name=ref, min=mn, value=v, max=mx, fit=False) + allowed = getattr(rowcls, "model_fields", {}).keys() + payload = {k: v for k, v in payload.items() if k in allowed} + + table.append(rowcls(**payload)) + return ref + + +def _span(val: float, frac: float = 0.25, floor: float | None = None): + if abs(val) < 1e-12: + return -1e-6, 0.0, 1e-6 + lo = val * (1 - frac) + hi = val * (1 + frac) + if floor is not None: + lo = max(lo, floor) + return min(lo, hi), val, max(lo, hi) + + +def _ensure_parameter( + project: rat.Project, + name: str, + value: float, + frac: float = 0.25, + floor: float | None = None, +) -> str: + pmin, pval, pmax = _span(value, frac, floor) + for p in project.parameters: + if p.name == name: + p.min = min(float(p.min), pmin) + p.max = max(float(p.max), pmax) + p.value = pval + return name + + project.parameters.append(Parameter(name=name, min=pmin, value=pval, max=pmax, fit=True)) + return name + + +def _write_bilayer_custom_model( + project_dir: Path, + filename: str, + base_layers: list[dict], + bilayer_specs: list[dict], +) -> str: + file_path = project_dir / filename + function_name = file_path.stem + payload = repr({"base_layers": base_layers, "bilayer_specs": bilayer_specs}) + + content = f'''import numpy as np + + +MODEL_PAYLOAD = {payload} + + +def {function_name}(params, bulk_in, bulk_out, contrast): + """Auto-generated custom bilayer model from ORSO import.""" + p = list(params) + sub_rough = p.pop(0) if p else 3.0 + + layers = [] + for layer in MODEL_PAYLOAD["base_layers"]: + layers.append([layer["thickness"], layer["sld"], layer["roughness"]]) + + for spec in MODEL_PAYLOAD["bilayer_specs"]: + apm = p.pop(0) if p else 55.0 + head_hyd_inner = p.pop(0) if p else 0.2 + head_hyd_outer = p.pop(0) if p else 0.2 + bilayer_hyd = p.pop(0) if p else 0.1 + rough = p.pop(0) if p else 4.0 + + v_head_i = spec["v_head_inner"] + v_tail_i = spec["v_tail_inner"] + v_head_o = spec["v_head_outer"] + v_tail_o = spec["v_tail_outer"] + + t_head_i = v_head_i / apm if apm else 0.0 + t_tail_i = v_tail_i / apm if apm else 0.0 + t_tail_o = v_tail_o / apm if apm else 0.0 + t_head_o = v_head_o / apm if apm else 0.0 + + sld_w = bulk_out[contrast] + sld_head_i = (head_hyd_inner * sld_w) + ((1 - head_hyd_inner) * spec["sld_head_inner"]) + sld_tail_i = (bilayer_hyd * sld_w) + ((1 - bilayer_hyd) * spec["sld_tail_inner"]) + sld_tail_o = (bilayer_hyd * sld_w) + ((1 - bilayer_hyd) * spec["sld_tail_outer"]) + sld_head_o = (head_hyd_outer * sld_w) + ((1 - head_hyd_outer) * spec["sld_head_outer"]) + + layers.extend( + [ + [t_head_i, sld_head_i, rough], + [t_tail_i, sld_tail_i, rough], + [t_tail_o, sld_tail_o, rough], + [t_head_o, sld_head_o, rough], + ] + ) + + return np.array(layers), sub_rough +''' + file_path.write_text(content) + return function_name + + +# ----------------------------------------------------------------------------- +# Main importer +# ----------------------------------------------------------------------------- + + +def import_ort_to_project( + ort_path: str, + base_project: rat.Project, + project_folder: str, +) -> tuple[rat.Project, rat.Controls | None]: + """Import ORSO (.ort) into RasCAL-2 standard-layers project.""" + ort_file = Path(ort_path).resolve() + proj_dir = Path(project_folder).resolve() + + _ensure_dir(proj_dir) + _ensure_dir(proj_dir / "data") + + copied_ort = proj_dir / "data" / ort_file.name + if copied_ort != ort_file: + shutil.copy2(ort_file, copied_ort) + + project = base_project + + # Clear defaults safely + project.contrasts.clear() + project.data.clear() + project.layers.clear() + + project.model = "standard layers" + + orso = load_orso(str(copied_ort)) + + default_background = "Background 1" + default_resolution = "Resolution 1" + default_scalefactor = "Scalefactor 1" + + # ------------------------------------------------------------ + # Resolve shared layer stack from first dataset + # ------------------------------------------------------------ + + bulk_in_ref = "SLD Air" + bulk_out_ref_default = "SLD D2O" + layer_name_stack: list[str] = [] + bilayer_specs_raw: list[dict] = [] + bilayer_present = False + base_layers_for_custom: list[dict] = [] + + if orso: + sample0 = orso[0].info.data_source.sample + model0 = getattr(sample0, "model", None) + + if model0 is not None: + extract_bilayers_from_model(model0) + try: + resolved = model0.resolve_to_layers() + if len(resolved) >= 2: + bulk_in = resolved[0] + bulk_out = resolved[-1] + + bulk_in_ref = _ensure_bulk_parameter_exists( + project, + "in", + _infer_bulk_name_from_layer(bulk_in, "Air"), + _safe_get_sld(bulk_in.material), + ) + + bulk_out_ref_default = _ensure_bulk_parameter_exists( + project, + "out", + _infer_bulk_name_from_layer(bulk_out, "D2O"), + _safe_get_sld(bulk_out.material), + ) + + for li in resolved[1:-1]: + lname = _sanitize_name( + getattr(li, "original_name", None) or getattr(li.material, "name", None), + "Layer", + ) + + t = float(li.thickness.as_unit("angstrom")) + r = float(li.roughness.as_unit("angstrom")) + s = _safe_get_sld(li.material) + + t_p = _ensure_parameter(project, f"{lname} thickness", t, floor=0.0) + r_p = _ensure_parameter(project, f"{lname} rough", r, floor=0.0) + s_p = _ensure_parameter(project, f"{lname} SLD", s) + + project.layers.append(Layer(name=lname, thickness=t_p, roughness=r_p, SLD_real=s_p)) + layer_name_stack.append(lname) + base_layers_for_custom.append({"name": lname, "thickness": t, "sld": s, "roughness": r}) + except Exception as e: + print("ORSO model resolution failed:", e) + + # ------------------------------------------------------------ + # Data + contrasts + # ------------------------------------------------------------ + + for i, ds in enumerate(orso, start=1): + sample = ds.info.data_source.sample + cname = _sanitize_name(getattr(sample, "name", None), f"Contrast {i}") + + arr = np.asarray(ds.data) + if arr.shape[1] == 2: + q, r = arr.T + dr = np.maximum(1e-12, 0.05 * abs(r)) + arr = np.vstack([q, r, dr]).T + else: + arr = arr[:, :3] + + project.data.append(Data(name=cname, data=arr)) + + bulk_out_ref = bulk_out_ref_default + + model = getattr(sample, "model", None) + if model is not None: + extract_bilayers_from_model(model) + try: + resolved = model.resolve_to_layers() + bulk_out = resolved[-1] + bulk_out_ref = _ensure_bulk_parameter_exists( + project, + "out", + _infer_bulk_name_from_layer(bulk_out, "D2O"), + _safe_get_sld(bulk_out.material), + ) + except Exception: + bulk = _infer_bulk_name_from_text(cname, "D2O") + bulk_out_ref = _ensure_bulk_parameter_exists(project, "out", bulk, 0.0) + + project.contrasts.append( + name=cname, + background=default_background, + resolution=default_resolution, + scalefactor=default_scalefactor, + bulk_in=bulk_in_ref, + bulk_out=bulk_out_ref, + data=cname, + model=layer_name_stack, + ) + + if bilayer_present: + project.model = "custom layers" + project.layers.clear() + project.custom_files.clear() + + bilayer_specs = build_bilayer_specs(bilayer_specs_raw) + _ensure_parameter(project, "Substrate Roughness", 3.0, floor=0.0) + for idx, _ in enumerate(bilayer_specs, start=1): + _ensure_parameter(project, f"Bilayer{idx} APM", 55.0, floor=1.0) + _ensure_parameter(project, f"Bilayer{idx} HeadHyd Inner", 0.2, floor=0.0) + _ensure_parameter(project, f"Bilayer{idx} HeadHyd Outer", 0.2, floor=0.0) + _ensure_parameter(project, f"Bilayer{idx} BilayerHydration", 0.1, floor=0.0) + _ensure_parameter(project, f"Bilayer{idx} Rough", 4.0, floor=0.0) + + custom_filename = "orso_bilayer_model.py" + function_name = _write_bilayer_custom_model( + proj_dir, + custom_filename, + base_layers_for_custom, + bilayer_specs, + ) + project.custom_files.append( + CustomFile( + name="ORSO Bilayer Model", + filename=custom_filename, + language="python", + path=".", + function_name=function_name, + ) + ) + for contrast in project.contrasts: + contrast.model = ["ORSO Bilayer Model"] + + return project, None diff --git a/rascal2/dialogs/startup_dialog.py b/rascal2/dialogs/startup_dialog.py index 66e4d567..a7356e2d 100644 --- a/rascal2/dialogs/startup_dialog.py +++ b/rascal2/dialogs/startup_dialog.py @@ -407,9 +407,9 @@ def block_for_worker(self, disabled: bool): class ImportProjectDialog(StartupDialog): - """Dialog to load a RasCAL-1 project.""" + """Dialog to load a RasCAL-1 project or an ORSO project.""" - def __init__(self, parent, file_extension): + def __init__(self, parent, file_extension="*.mat"): if file_extension == "*.mat": self.project_type = "RasCAL-1" elif file_extension == "*.ort": @@ -420,14 +420,13 @@ def __init__(self, parent, file_extension): p, "Select Project File", filter=file_extension )[0] super().__init__(parent) - print(file_extension) def create_form(self, form_layout): self.setWindowTitle(f"Import {self.project_type} Project") super().create_form(form_layout) self.project_folder_label.setText(f"{self.project_type} file:") - self.project_folder.setPlaceholderText(f"Select {self.project_type} Project file") + self.project_folder.setPlaceholderText(f"Select {self.project_type} file") def create_buttons(self): load_button = QtWidgets.QPushButton("Load", objectName="LoadButton") @@ -443,10 +442,13 @@ def verify_folder(file_path: str): raise ValueError("You do not have permission to create a project in this folder.") def load_project(self): + print("\n=================load project====================") + print(f"\n {self.project_folder.text()=}") + print(f"\n {self.project_type=}") """Load the project if inputs are valid.""" - if ".ort" in self.project_folder.text(): - print("found ort file") - elif ".mat" in self.project_folder.text(): + if self.project_type == "ORSO": + self.load_orso_project() + elif self.project_type == "RasCAL-1": self.load_r1_project() def load_r1_project(self): @@ -462,3 +464,18 @@ def load_r1_project(self): self.loading_bar.hide, ) self.loading_bar.setVisible(True) + + def load_orso_project(self): + print("\n=================load_orso_project====================") + """Load the ORSO project if inputs are valid.""" + if self.project_folder.text() == "": + self.set_folder_error("Please specify a project file.") + if self.project_folder_error.isHidden(): + self.worker = Worker.call( + self.parent().presenter.import_ort_project, + [self.project_folder.text()], + self.project_start_success, + self.project_start_failed, + self.loading_bar.hide, + ) + self.loading_bar.setVisible(True) diff --git a/rascal2/ui/model.py b/rascal2/ui/model.py index 56b581a7..e3c7e5c7 100644 --- a/rascal2/ui/model.py +++ b/rascal2/ui/model.py @@ -9,6 +9,7 @@ from PyQt6 import QtCore from rascal2.config import EXAMPLES_PATH, EXAMPLES_TEMP_PATH +from rascal2.core.orso_importer import import_ort_to_project def copy_example_project(load_path): @@ -219,6 +220,35 @@ def load_r1_project(self, load_path: str): self.controls = rat.Controls() self.save_path = str(Path(load_path).parent) + def load_orso_project(self, load_path: str): + """Load a project from a ORSO file. + + Parameters + ---------- + load_path : str + The path to the ORSO file. + + """ + ort_file = Path(load_path) + proj_name = ort_file.stem.replace("_", " ").strip() or "ORSO Import" + self.save_path = str(Path(load_path).parent) + self.create_project(proj_name, self.save_path) + + imported_project, imported_controls = import_ort_to_project( + load_path, + base_project=self.project, + project_folder=self.save_path, + ) + + self.project = imported_project + if imported_controls is not None: + self.controls = imported_controls + + # Optional preview run (no MATLAB required for standard layers) + print("Imported layers:", len(self.project.layers)) + print("Imported parameters:", len(self.project.parameters)) + print("First layer:", self.project.layers[0] if self.project.layers else None) + def update_controls(self, new_values: dict): """Update the control attributes. diff --git a/rascal2/ui/presenter.py b/rascal2/ui/presenter.py index 5e88b36d..e4a3e7ed 100644 --- a/rascal2/ui/presenter.py +++ b/rascal2/ui/presenter.py @@ -79,6 +79,49 @@ def initialise_ui(self): self.view.undo_stack.clear() self.view.enable_elements() + def import_ort_project(self, load_path: str): + """Load a RAT project from an ORSO project file. + + Parameters + ---------- + load_path : str + The path to the ORSO file. + + """ + self.model.load_orso_project(load_path) + self.model.results = self.quick_run() + + # from rascal2.core.orso_importer import import_ort_to_project + # from pathlib import Path + # from rascal2.settings import update_recent_projects + # + # ort_file = Path(ort_path) + # proj_name = ort_file.stem.replace("_", " ").strip() or "ORSO Import" + # save_path = str(Path(project_folder)) # ✅ use what user selected + # + # self.model.create_project(proj_name, save_path) + # + # imported_project, imported_controls = import_ort_to_project( + # ort_path, + # base_project=self.model.project, + # project_folder=save_path, + # ) + # + # self.model.project = imported_project + # if imported_controls is not None: + # self.model.controls = imported_controls + # + # # Optional preview run (no MATLAB required for standard layers) + # print("Imported layers:", len(self.model.project.layers)) + # print("Imported parameters:", len(self.model.project.parameters)) + # print("First layer:", self.model.project.layers[0] if self.model.project.layers else None) + # + # self.model.results = self.quick_run(self.model.project) + # + # # Persist so it becomes a normal RasCAL-2 project folder + # self.model.save_project(self.model.save_path) + # update_recent_projects(self.model.save_path) + def update_title(self): suffix = " [Example]" if self.model.is_project_example() else f"[{self.model.save_path}]" self.view.setWindowTitle( diff --git a/requirements.txt b/requirements.txt index 652a092c..0fd56255 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,7 @@ PyQt6-Qt6==6.7.3 ratapi==0.0.0.dev14 pydantic==2.8.2 PyQt6-QScintilla==2.14.1 +molgroups==0.2.0a0 +refl1d==1.0.0a1 nexusformat==1.0.7 orsopy==1.2.1 diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 7bb77c52..e4fc3dac 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -173,7 +173,7 @@ def test_menu_element_present(test_view, submenu_name): "&New Project", "", "&Open Project", - "Open &RasCAL-1 Project", + "&Import Project", "", "&Save", "Save To &Folder...", From 56830044dae241667f2af59955e4da39d2bfc76f Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 3 Jun 2026 13:21:54 +0100 Subject: [PATCH 3/3] merge conflict fixes --- rascal2/ui/model.py | 2 +- rascal2/ui/view.py | 1 - rascal2/widgets/startup.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rascal2/ui/model.py b/rascal2/ui/model.py index ad0558ff..597f8c50 100644 --- a/rascal2/ui/model.py +++ b/rascal2/ui/model.py @@ -8,8 +8,8 @@ import ratapi.outputs from PyQt6 import QtCore -from rascal2.config import EXAMPLES_PATH, EXAMPLES_TEMP_PATH from rascal2.core.orso_importer import import_ort_to_project +from rascal2.paths import EXAMPLES_PATH, EXAMPLES_TEMP_PATH def copy_example_project(load_path): diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 595d01fd..cd958533 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -13,7 +13,6 @@ NewProjectDialog, StartupDialog, ) -from rascal2.dialogs.startup_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog, StartupDialog from rascal2.paths import EXAMPLES_PATH, EXAMPLES_TEMP_PATH, path_for from rascal2.settings import MDIGeometries, get_global_settings from rascal2.widgets import ControlsWidget, PlotWidget, TerminalWidget diff --git a/rascal2/widgets/startup.py b/rascal2/widgets/startup.py index a7dec0cd..535c4ccb 100644 --- a/rascal2/widgets/startup.py +++ b/rascal2/widgets/startup.py @@ -1,6 +1,5 @@ from PyQt6 import QtCore, QtGui, QtWidgets - from rascal2.dialogs.startup_dialog import ImportProjectDialog, LoadDialog, NewProjectDialog from rascal2.paths import path_for