Skip to content
Merged
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
14 changes: 14 additions & 0 deletions EasyReflectometryApp/Backends/Mock/Plotting.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ QtObject {
property double analysisMinY: -40.
property double analysisMaxY: 40.

property double residualMinX: 0.01
property double residualMaxX: 0.30
property double residualMinY: -0.1
property double residualMaxY: 0.1

property int modelCount: 1

// Plot mode properties
Expand Down Expand Up @@ -133,4 +138,13 @@ QtObject {
]
}

function getResidualDataPoints(index) {
console.debug(`getResidualDataPoints ${index}`)
return [
{ 'x': 0.01, 'y': 0.002 },
{ 'x': 0.15, 'y': -0.001 },
{ 'x': 0.30, 'y': 0.003 }
]
}

}
11 changes: 8 additions & 3 deletions EasyReflectometryApp/Backends/Py/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
class Experiment(QObject):
experimentChanged = Signal()
externalExperimentChanged = Signal()
qRangeUpdated = Signal()

def __init__(self, project_lib: ProjectLib, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -59,12 +60,16 @@ def load(self, paths: str) -> None:
if isinstance(paths, str):
paths = paths.split(',')

q_range_changed = False
for path in paths:
generalized = IO.generalizePath(path)
if self._project_logic.count_datasets_in_file(generalized) > 1:
self._project_logic.load_all_experiments_from_file(generalized)
_count, changed = self._project_logic.load_all_experiments_from_file(generalized)
else:
self._project_logic.load_new_experiment(generalized)
changed = self._project_logic.load_new_experiment(generalized)
if changed:
q_range_changed = True
self.experimentChanged.emit()
self.externalExperimentChanged.emit()
pass # debug anchor
if q_range_changed:
self.qRangeUpdated.emit()
2 changes: 1 addition & 1 deletion EasyReflectometryApp/Backends/Py/logic/calculators.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def set_current_index(self, new_value: int) -> None:
if new_value != self._current_index:
self._current_index = new_value
new_calculator = self._list_available_calculators[new_value]
self._project_lib._calculator.switch(new_calculator)
self._project_lib.calculator = new_calculator
return True
return False
14 changes: 10 additions & 4 deletions EasyReflectometryApp/Backends/Py/logic/fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def on_fit_finished(self, results: List[FitResults]) -> None:

@property
def fit_n_pars(self) -> int:
"""Return total number of refined parameters across all fits."""
"""Return the global number of refined parameters for the fit."""
if self._results:
return sum(r.n_pars for r in self._results)
if self._result is None:
Expand All @@ -233,16 +233,22 @@ def fit_n_pars(self) -> int:

@property
def fit_chi2(self) -> float:
"""Return total chi-squared across all fits."""
"""Return reduced chi-squared across all fits (chi2 / degrees of freedom)"""
if self._results:
try:
return float(sum(r.chi2 for r in self._results))
total_chi2 = float(sum(r.chi2 for r in self._results))
total_points = sum(len(r.x) for r in self._results)
n_params = self._results[0].n_pars
total_dof = total_points - n_params
if total_dof <= 0:
return 0.0
return total_chi2 / total_dof
except (ValueError, TypeError):
return 0.0
if self._result is None:
return 0.0
try:
return float(self._result.chi2)
return float(self._result.reduced_chi)
except (ValueError, TypeError):
return 0.0

Expand Down
4 changes: 2 additions & 2 deletions EasyReflectometryApp/Backends/Py/logic/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 5SPDX-FileCopyrightText: 2025 EasyApp contributors
# 5SPDX-FileCopyrightText: 2026 EasyApp contributors
# SPDX-License-Identifier: BSD-3-Clause
# © 2025 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp>
# © 2026 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp>


class IO:
Expand Down
5 changes: 4 additions & 1 deletion EasyReflectometryApp/Backends/Py/logic/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
class Models:
def __init__(self, project_lib: ProjectLib):
self._project_lib = project_lib
self._models = project_lib._models

@property
def _models(self) -> ModelCollection:
return self._project_lib._models

@property
def index(self) -> int:
Expand Down
15 changes: 13 additions & 2 deletions EasyReflectometryApp/Backends/Py/logic/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,22 @@ def _make_alias(name: str) -> str:
return alias

def _get_parameter_display_data(param: Parameter, model_unique_name: str) -> Tuple[str, str]:
"""Extract display name and group from parameter path."""
"""Extract display name and group from parameter path.

For layer parameters (thickness, roughness), uses the assembly name
from the path instead of the layer name, so that renaming an assembly
in the Model Editor is reflected in the Analysis parameters table.
"""
path = global_object.map.find_path(model_unique_name, param.unique_name)
if len(path) >= 2:
parent_name = global_object.map.get_item_by_key(path[-2]).name
param_name = global_object.map.get_item_by_key(path[-1]).name
# For layer parameters the path is:
# Model -> Sample -> Assembly -> LayerCollection -> Layer -> param
# Use the assembly name (path[-4]) instead of the layer name (path[-2])
if _is_layer_parameter(param) and len(path) >= 4:
parent_name = global_object.map.get_item_by_key(path[-4]).name
else:
parent_name = global_object.map.get_item_by_key(path[-2]).name
return f'{parent_name} {param_name}', parent_name
return param.name, '' # Fallback to parameter name without group

Expand Down
53 changes: 49 additions & 4 deletions EasyReflectometryApp/Backends/Py/logic/project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from copy import copy
from pathlib import Path

import numpy as np
from easyreflectometry import Project as ProjectLib


Expand Down Expand Up @@ -107,17 +108,61 @@ def save(self) -> None:
def load(self, path: str) -> None:
self._project_lib.load_from_json(path)

def load_experiment(self, path: str) -> None:
def load_experiment(self, path: str) -> bool:
self._project_lib.load_experiment_for_model_at_index(path, self._project_lib._current_model_index)
return self._sync_q_max_with_loaded_experiments()

def load_new_experiment(self, path: str) -> None:
def load_new_experiment(self, path: str) -> bool:
self._project_lib.load_new_experiment(path)
return self._sync_q_max_with_loaded_experiments()

def count_datasets_in_file(self, path: str) -> int:
return self._project_lib.count_datasets_in_file(path)

def load_all_experiments_from_file(self, path: str) -> int:
return self._project_lib.load_all_experiments_from_file(path)
def load_all_experiments_from_file(self, path: str) -> tuple[int, bool]:
loaded_count = self._project_lib.load_all_experiments_from_file(path)
q_max_changed = self._sync_q_max_with_loaded_experiments()
return loaded_count, q_max_changed

def _sync_q_max_with_loaded_experiments(self) -> bool:
"""Set model q_max to the largest q value found in loaded experiments.

:return: True if q_max was changed, False otherwise.
:rtype: bool
"""
experiments = self._project_lib._experiments
if not experiments:
return False

if hasattr(experiments, 'values'):
experiment_iterable = experiments.values()
else:
experiment_iterable = experiments

q_max_candidates = []
for experiment in experiment_iterable:
x_values = getattr(experiment, 'x', None)
if x_values is None:
continue

q_values = np.asarray(x_values, dtype=float)
if q_values.size == 0:
continue

finite_q_values = q_values[np.isfinite(q_values)]
if finite_q_values.size == 0:
continue

q_max_candidates.append(float(np.max(finite_q_values)))

if not q_max_candidates:
return False

new_q_max = max(q_max_candidates)
if new_q_max != self._project_lib.q_max:
self._project_lib.q_max = new_q_max
return True
return False

def set_sample_from_orso(self, sample) -> None:
self._project_lib.set_sample_from_orso(sample)
Expand Down
Loading
Loading