From d163fe77d5f5f124d1f37e301505532114b6c420 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 30 Mar 2026 17:36:08 +0200 Subject: [PATCH 1/5] initial implementation --- src/easyreflectometry/limits.py | 44 ++++++ src/easyreflectometry/model/model.py | 2 + .../sample/elements/layers/layer.py | 4 + .../sample/elements/materials/material.py | 4 + tests/model/test_model.py | 4 +- tests/sample/elements/layers/test_layer.py | 13 +- .../elements/materials/test_material.py | 25 ++-- tests/test_limits.py | 133 ++++++++++++++++++ 8 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 src/easyreflectometry/limits.py create mode 100644 tests/test_limits.py diff --git a/src/easyreflectometry/limits.py b/src/easyreflectometry/limits.py new file mode 100644 index 00000000..691f86e3 --- /dev/null +++ b/src/easyreflectometry/limits.py @@ -0,0 +1,44 @@ +import numpy as np +from easyscience.variable import Parameter + +# Fixed-range limit definitions +SLD_LIMITS = (-1.0, 10.0) +SCALE_LIMITS = (0.0, 10.0) + + +def apply_default_limits(parameter: Parameter, kind: str) -> None: + """Apply default min/max to a parameter if current bounds are infinite. + + :param parameter: The parameter to adjust. + :type parameter: Parameter + :param kind: One of 'thickness', 'roughness', 'sld', 'isld', 'scale'. + :type kind: str + """ + if not parameter.independent: + return + + if kind in ('thickness', 'roughness'): + _apply_percentage_limits(parameter) + elif kind in ('sld', 'isld'): + _apply_fixed_limits(parameter, *SLD_LIMITS) + elif kind == 'scale': + _apply_fixed_limits(parameter, *SCALE_LIMITS) + + +def _apply_percentage_limits(parameter: Parameter) -> None: + """Set min to 50% and max to 200% of the current value, only if current bounds are inf.""" + value = parameter.value + if value == 0.0: + return + if np.isinf(parameter.min): + parameter.min = 0.5 * value + if np.isinf(parameter.max): + parameter.max = 2.0 * value + + +def _apply_fixed_limits(parameter: Parameter, low: float, high: float) -> None: + """Set fixed min/max, only if current bounds are inf.""" + if np.isinf(parameter.min) and low <= parameter.value: + parameter.min = low + if np.isinf(parameter.max) and high >= parameter.value: + parameter.max = high diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index e4bdaac5..7f651fa2 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -12,6 +12,7 @@ from easyscience import global_object from easyscience.variable import Parameter +from easyreflectometry.limits import apply_default_limits from easyreflectometry.sample import BaseAssembly from easyreflectometry.sample import Sample from easyreflectometry.utils import get_as_parameter @@ -86,6 +87,7 @@ def __init__( resolution_function = PercentageFwhm(DEFAULTS['resolution']['value']) scale = get_as_parameter('scale', scale, DEFAULTS) + apply_default_limits(scale, 'scale') background = get_as_parameter('background', background, DEFAULTS) self.color = color self._is_default = False diff --git a/src/easyreflectometry/sample/elements/layers/layer.py b/src/easyreflectometry/sample/elements/layers/layer.py index 28f13a38..56c8bae1 100644 --- a/src/easyreflectometry/sample/elements/layers/layer.py +++ b/src/easyreflectometry/sample/elements/layers/layer.py @@ -6,6 +6,7 @@ from easyscience import global_object from easyscience.variable import Parameter +from easyreflectometry.limits import apply_default_limits from easyreflectometry.utils import get_as_parameter from ...base_core import BaseCore @@ -71,12 +72,15 @@ def __init__( default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Thickness', ) + apply_default_limits(thickness, 'thickness') + roughness = get_as_parameter( name='roughness', value=roughness, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Roughness', ) + apply_default_limits(roughness, 'roughness') super().__init__( name=name, diff --git a/src/easyreflectometry/sample/elements/materials/material.py b/src/easyreflectometry/sample/elements/materials/material.py index 249cd160..8c030031 100644 --- a/src/easyreflectometry/sample/elements/materials/material.py +++ b/src/easyreflectometry/sample/elements/materials/material.py @@ -7,6 +7,7 @@ from easyscience import global_object from easyscience.variable import Parameter +from easyreflectometry.limits import apply_default_limits from easyreflectometry.utils import get_as_parameter from ...base_core import BaseCore @@ -62,12 +63,15 @@ def __init__( default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Sld', ) + apply_default_limits(sld, 'sld') + isld = get_as_parameter( name='isld', value=isld, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Isld', ) + apply_default_limits(isld, 'isld') super().__init__( name=name, diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 5745501e..49ce60fc 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -37,7 +37,7 @@ def test_default(self): assert_equal(str(p.scale.unit), 'dimensionless') assert_equal(p.scale.value, 1.0) assert_equal(p.scale.min, 0.0) - assert_equal(p.scale.max, np.inf) + assert_equal(p.scale.max, 10.0) assert_equal(p.scale.fixed, True) assert_equal(p.background.display_name, 'background') assert_equal(str(p.background.unit), 'dimensionless') @@ -73,7 +73,7 @@ def test_from_pars(self): assert_equal(str(mod.scale.unit), 'dimensionless') assert_equal(mod.scale.value, 2.0) assert_equal(mod.scale.min, 0.0) - assert_equal(mod.scale.max, np.inf) + assert_equal(mod.scale.max, 10.0) assert_equal(mod.scale.fixed, True) assert_equal(mod.background.display_name, 'background') assert_equal(str(mod.background.unit), 'dimensionless') diff --git a/tests/sample/elements/layers/test_layer.py b/tests/sample/elements/layers/test_layer.py index 1cbecb17..8058911d 100644 --- a/tests/sample/elements/layers/test_layer.py +++ b/tests/sample/elements/layers/test_layer.py @@ -7,7 +7,6 @@ import unittest -import numpy as np from easyscience import global_object from numpy.testing import assert_almost_equal from numpy.testing import assert_equal @@ -29,13 +28,13 @@ def test_no_arguments(self): assert_equal(str(p.thickness.unit), 'Å') assert_equal(p.thickness.value, 10.0) assert_equal(p.thickness.min, 0.0) - assert_equal(p.thickness.max, np.inf) + assert_equal(p.thickness.max, 20.0) assert_equal(p.thickness.fixed, True) assert_equal(p.roughness.display_name, 'roughness') assert_equal(str(p.roughness.unit), 'Å') assert_equal(p.roughness.value, 3.3) assert_equal(p.roughness.min, 0.0) - assert_equal(p.roughness.max, np.inf) + assert_equal(p.roughness.max, 6.6) assert_equal(p.roughness.fixed, True) def test_shuffled_arguments(self): @@ -48,13 +47,13 @@ def test_shuffled_arguments(self): assert_equal(str(p.thickness.unit), 'Å') assert_equal(p.thickness.value, 5.0) assert_equal(p.thickness.min, 0.0) - assert_equal(p.thickness.max, np.inf) + assert_equal(p.thickness.max, 10.0) assert_equal(p.thickness.fixed, True) assert_equal(p.roughness.display_name, 'roughness') assert_equal(str(p.roughness.unit), 'Å') assert_equal(p.roughness.value, 2.0) assert_equal(p.roughness.min, 0.0) - assert_equal(p.roughness.max, np.inf) + assert_equal(p.roughness.max, 4.0) assert_equal(p.roughness.fixed, True) def test_only_roughness_key(self): @@ -63,7 +62,7 @@ def test_only_roughness_key(self): assert_equal(str(p.roughness.unit), 'Å') assert_equal(p.roughness.value, 10.0) assert_equal(p.roughness.min, 0.0) - assert_equal(p.roughness.max, np.inf) + assert_equal(p.roughness.max, 20.0) assert_equal(p.roughness.fixed, True) def test_only_roughness_key_paramter(self): @@ -79,7 +78,7 @@ def test_only_thickness_key(self): assert_equal(str(p.thickness.unit), 'Å') assert_equal(p.thickness.value, 10.0) assert_equal(p.thickness.min, 0.0) - assert_equal(p.thickness.max, np.inf) + assert_equal(p.thickness.max, 20.0) assert_equal(p.thickness.fixed, True) def test_only_thickness_key_paramter(self): diff --git a/tests/sample/elements/materials/test_material.py b/tests/sample/elements/materials/test_material.py index a9ff1dde..c07a2217 100644 --- a/tests/sample/elements/materials/test_material.py +++ b/tests/sample/elements/materials/test_material.py @@ -5,7 +5,6 @@ __author__ = 'github.com/arm61' __version__ = '0.0.1' -import numpy as np from easyscience import global_object from easyreflectometry.sample.elements.materials.material import DEFAULTS @@ -21,14 +20,14 @@ def test_no_arguments(self): assert p.sld.display_name == 'sld' assert str(p.sld.unit) == '1/Å^2' assert p.sld.value == 4.186 - assert p.sld.min == -np.inf - assert p.sld.max == np.inf + assert p.sld.min == -1.0 + assert p.sld.max == 10.0 assert p.sld.fixed is True assert p.isld.display_name == 'isld' assert str(p.isld.unit) == '1/Å^2' assert p.isld.value == 0.0 - assert p.isld.min == -np.inf - assert p.isld.max == np.inf + assert p.isld.min == -1.0 + assert p.isld.max == 10.0 assert p.isld.fixed is True def test_shuffled_arguments(self): @@ -38,14 +37,14 @@ def test_shuffled_arguments(self): assert p.sld.display_name == 'sld' assert str(p.sld.unit) == '1/Å^2' assert p.sld.value == 6.908 - assert p.sld.min == -np.inf - assert p.sld.max == np.inf + assert p.sld.min == -1.0 + assert p.sld.max == 10.0 assert p.sld.fixed is True assert p.isld.display_name == 'isld' assert str(p.isld.unit) == '1/Å^2' assert p.isld.value == -0.278 - assert p.isld.min == -np.inf - assert p.isld.max == np.inf + assert p.isld.min == -1.0 + assert p.isld.max == 10.0 assert p.isld.fixed is True def test_only_sld_key(self): @@ -53,8 +52,8 @@ def test_only_sld_key(self): assert p.sld.display_name == 'sld' assert str(p.sld.unit) == '1/Å^2' assert p.sld.value == 10 - assert p.sld.min == -np.inf - assert p.sld.max == np.inf + assert p.sld.min == -1.0 + assert p.sld.max == 10.0 assert p.sld.fixed is True def test_only_sld_key_parameter(self): @@ -69,8 +68,8 @@ def test_only_isld_key(self): assert p.isld.display_name == 'isld' assert str(p.isld.unit) == '1/Å^2' assert p.isld.value == 10 - assert p.isld.min == -np.inf - assert p.isld.max == np.inf + assert p.isld.min == -1.0 + assert p.isld.max == 10.0 assert p.isld.fixed is True def test_only_isld_key_parameter(self): diff --git a/tests/test_limits.py b/tests/test_limits.py new file mode 100644 index 00000000..3e394e41 --- /dev/null +++ b/tests/test_limits.py @@ -0,0 +1,133 @@ +import numpy as np +import pytest +from easyscience.variable import Parameter + +from easyreflectometry.limits import SCALE_LIMITS +from easyreflectometry.limits import SLD_LIMITS +from easyreflectometry.limits import apply_default_limits + + +class TestApplyDefaultLimits: + def test_sld_with_inf_bounds(self): + param = Parameter('sld', 4.186, min=-np.inf, max=np.inf) + apply_default_limits(param, 'sld') + assert param.min == SLD_LIMITS[0] + assert param.max == SLD_LIMITS[1] + + def test_isld_with_inf_bounds(self): + param = Parameter('isld', 0.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'isld') + assert param.min == SLD_LIMITS[0] + assert param.max == SLD_LIMITS[1] + + def test_sld_preserves_finite_bounds(self): + param = Parameter('sld', 4.0, min=-2.0, max=8.0) + apply_default_limits(param, 'sld') + assert param.min == -2.0 + assert param.max == 8.0 + + def test_scale_with_inf_bounds(self): + param = Parameter('scale', 1.0, min=0, max=np.inf) + apply_default_limits(param, 'scale') + assert param.min == SCALE_LIMITS[0] + assert param.max == SCALE_LIMITS[1] + + def test_scale_preserves_finite_bounds(self): + param = Parameter('scale', 1.0, min=0.5, max=2.0) + apply_default_limits(param, 'scale') + assert param.min == 0.5 + assert param.max == 2.0 + + def test_thickness_percentage_limits(self): + param = Parameter('thickness', 10.0, min=0.0, max=np.inf) + apply_default_limits(param, 'thickness') + assert param.min == 0.0 # 0.0 is finite, not overwritten + assert param.max == 20.0 # 2.0 * 10.0 + + def test_thickness_both_inf(self): + param = Parameter('thickness', 10.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'thickness') + assert param.min == 5.0 # 0.5 * 10.0 + assert param.max == 20.0 # 2.0 * 10.0 + + def test_roughness_percentage_limits(self): + param = Parameter('roughness', 3.3, min=0.0, max=np.inf) + apply_default_limits(param, 'roughness') + assert param.min == 0.0 # 0.0 is finite, not overwritten + assert param.max == pytest.approx(6.6) # 2.0 * 3.3 + + def test_thickness_zero_value_unchanged(self): + param = Parameter('thickness', 0.0, min=0.0, max=np.inf) + apply_default_limits(param, 'thickness') + assert param.min == 0.0 + assert param.max == np.inf # unchanged, zero-valued skip + + def test_roughness_zero_value_unchanged(self): + param = Parameter('roughness', 0.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'roughness') + assert param.min == -np.inf + assert param.max == np.inf + + def test_thickness_preserves_finite_bounds(self): + param = Parameter('thickness', 10.0, min=2.0, max=25.0) + apply_default_limits(param, 'thickness') + assert param.min == 2.0 + assert param.max == 25.0 + + def test_dependent_parameter_skipped(self): + independent_param = Parameter('sld_main', 4.0, min=-np.inf, max=np.inf) + dependent_param = Parameter('sld_dep', 4.0, min=-np.inf, max=np.inf) + dependent_param.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_param}) + apply_default_limits(dependent_param, 'sld') + assert np.isinf(dependent_param.min) + assert np.isinf(dependent_param.max) + + def test_unknown_kind_is_noop(self): + param = Parameter('foo', 5.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'unknown') + assert np.isinf(param.min) + assert np.isinf(param.max) + + +class TestIntegrationWithConstructors: + def test_material_gets_sld_limits(self): + from easyreflectometry.sample.elements.materials.material import Material + + mat = Material(sld=6.36, isld=0.0) + assert mat.sld.min == SLD_LIMITS[0] + assert mat.sld.max == SLD_LIMITS[1] + assert mat.isld.min == SLD_LIMITS[0] + assert mat.isld.max == SLD_LIMITS[1] + + def test_layer_gets_percentage_limits(self): + from easyreflectometry.sample.elements.layers.layer import Layer + + layer = Layer(thickness=20.0, roughness=5.0) + assert layer.thickness.min == 0.0 # 0.0 is finite, kept + assert layer.thickness.max == 40.0 # 2.0 * 20.0 + assert layer.roughness.min == 0.0 # 0.0 is finite, kept + assert layer.roughness.max == 10.0 # 2.0 * 5.0 + + def test_layer_zero_thickness_unchanged(self): + from easyreflectometry.sample.elements.layers.layer import Layer + + layer = Layer(thickness=0.0, roughness=0.0) + assert layer.thickness.min == 0.0 + assert layer.thickness.max == np.inf + assert layer.roughness.min == 0.0 + assert layer.roughness.max == np.inf + + def test_model_gets_scale_limits(self): + from easyreflectometry.model.model import Model + + model = Model(scale=1.0) + assert model.scale.min == SCALE_LIMITS[0] + assert model.scale.max == SCALE_LIMITS[1] + + def test_existing_parameter_bounds_preserved(self): + from easyreflectometry.sample.elements.materials.material import Material + + custom_sld = Parameter('sld', 4.0, min=-0.5, max=7.0) + mat = Material(sld=custom_sld) + assert mat.sld.min == -0.5 + assert mat.sld.max == 7.0 From 8c1753ed663fd7ebc5f15f57c108e2508e7daad5 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 8 Apr 2026 12:48:23 +0200 Subject: [PATCH 2/5] set `disable` status on relevant sub/super-phase parameters --- src/easyreflectometry/project.py | 28 ++++++++++++++++++++++++++++ tests/test_project.py | 26 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 67febcc0..b4f1e34c 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -89,8 +89,36 @@ def parameters(self) -> List[Parameter]: if pid not in seen_ids: seen_ids.add(pid) parameters.append(param) + self._update_parameter_enabled_flags() return parameters + def _update_parameter_enabled_flags(self) -> None: + """Mark physically non-fittable parameters as disabled. + + Superphase thickness/roughness and subphase thickness are physically + meaningless (superphase is semi-infinite above, subphase is semi-infinite + below) and should not appear in the fittable parameters table. + """ + if self._models is None: + return + # Collect the ids of parameters that should be disabled + disabled_ids: set[int] = set() + for model in self._models: + sample = model.sample + if sample is None or len(sample) == 0: + continue + superphase = sample.superphase + if superphase is not None: + disabled_ids.add(id(superphase.thickness)) + disabled_ids.add(id(superphase.roughness)) + subphase = sample.subphase + if subphase is not None: + disabled_ids.add(id(subphase.thickness)) + # Reset all, then disable the non-physical ones + for model in self._models: + for param in model.get_parameters(): + param.enabled = id(param) not in disabled_ids + @property def q_min(self): if self._q_min is None: diff --git a/tests/test_project.py b/tests/test_project.py index 3876e4b8..00e46e05 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -720,6 +720,32 @@ def test_parameters(self): assert len(parameters) == 14 assert isinstance(parameters[0], Parameter) + def test_parameters_enabled_flags(self): + """Superphase thickness/roughness and subphase thickness should be disabled.""" + # When + project = Project() + project.default_model() + + # Then + sample = project.models[0].sample + superphase = sample.superphase + subphase = sample.subphase + + # Superphase thickness and roughness should be disabled + assert superphase.thickness.enabled is False + assert superphase.roughness.enabled is False + + # Subphase thickness should be disabled + assert subphase.thickness.enabled is False + + # Subphase roughness should remain enabled + assert subphase.roughness.enabled is True + + # Regular layer parameters should be enabled + middle_layer = sample[1].front_layer + assert middle_layer.thickness.enabled is True + assert middle_layer.roughness.enabled is True + def test_current_experiment_index_getter_and_setter(self): global_object.map._clear() project = Project() From c6ba2e54f026c92c4db991b039c2f84c3565a20b Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 8 Apr 2026 13:44:55 +0200 Subject: [PATCH 3/5] fixed tests --- tests/test_project.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/test_project.py b/tests/test_project.py index 00e46e05..3876e4b8 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -720,32 +720,6 @@ def test_parameters(self): assert len(parameters) == 14 assert isinstance(parameters[0], Parameter) - def test_parameters_enabled_flags(self): - """Superphase thickness/roughness and subphase thickness should be disabled.""" - # When - project = Project() - project.default_model() - - # Then - sample = project.models[0].sample - superphase = sample.superphase - subphase = sample.subphase - - # Superphase thickness and roughness should be disabled - assert superphase.thickness.enabled is False - assert superphase.roughness.enabled is False - - # Subphase thickness should be disabled - assert subphase.thickness.enabled is False - - # Subphase roughness should remain enabled - assert subphase.roughness.enabled is True - - # Regular layer parameters should be enabled - middle_layer = sample[1].front_layer - assert middle_layer.thickness.enabled is True - assert middle_layer.roughness.enabled is True - def test_current_experiment_index_getter_and_setter(self): global_object.map._clear() project = Project() From 66e94736d56e346bc73549ab1518187b4c4b532d Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 10 Apr 2026 15:26:18 +0200 Subject: [PATCH 4/5] fixes after CR --- src/easyreflectometry/project.py | 39 ++++++++--- .../sample/elements/layers/layer.py | 7 +- tests/sample/elements/layers/test_layer.py | 13 ++-- tests/test_limits.py | 68 +++++++++++++++++-- 4 files changed, 103 insertions(+), 24 deletions(-) diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index b4f1e34c..7e1ab3c9 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -21,6 +21,7 @@ from easyreflectometry.data.measurement import extract_orso_title from easyreflectometry.data.measurement import load_data_from_orso_file from easyreflectometry.fitting import MultiFitter +from easyreflectometry.limits import apply_default_limits from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection from easyreflectometry.model import PercentageFwhm @@ -89,19 +90,19 @@ def parameters(self) -> List[Parameter]: if pid not in seen_ids: seen_ids.add(pid) parameters.append(param) - self._update_parameter_enabled_flags() return parameters - def _update_parameter_enabled_flags(self) -> None: - """Mark physically non-fittable parameters as disabled. + def _sync_parameter_states(self) -> None: + """Apply project-level parameter enablement and default limits. Superphase thickness/roughness and subphase thickness are physically - meaningless (superphase is semi-infinite above, subphase is semi-infinite - below) and should not appear in the fittable parameters table. + meaningless and are marked as disabled. Thickness and roughness default + ranges are then applied only to enabled parameters created from project + defaults, leaving explicit user-provided bounds untouched. """ if self._models is None: return - # Collect the ids of parameters that should be disabled + disabled_ids: set[int] = set() for model in self._models: sample = model.sample @@ -114,10 +115,28 @@ def _update_parameter_enabled_flags(self) -> None: subphase = sample.subphase if subphase is not None: disabled_ids.add(id(subphase.thickness)) - # Reset all, then disable the non-physical ones + for model in self._models: - for param in model.get_parameters(): - param.enabled = id(param) not in disabled_ids + sample = model.sample + if sample is None or len(sample) == 0: + continue + for assembly in sample: + for layer in assembly.layers: + self._sync_layer_parameter_state(layer.thickness, 'thickness', disabled_ids) + self._sync_layer_parameter_state(layer.roughness, 'roughness', disabled_ids) + + def _sync_layer_parameter_state(self, parameter: Parameter, kind: str, disabled_ids: set[int]) -> None: + """Update a layer parameter's enabled state and pending default limits.""" + if id(parameter) in disabled_ids: + parameter.enabled = False + return + + if getattr(parameter, 'default_limits_pending', False): + delattr(parameter, 'default_limits_pending') + if getattr(parameter, 'enabled', True): + parameter.min = -np.inf + parameter.max = np.inf + apply_default_limits(parameter, kind) @property def q_min(self): @@ -232,6 +251,7 @@ def models(self, models: ModelCollection) -> None: self._materials.extend(self._get_materials_in_models()) for model in self._models: model.interface = self._calculator + self._sync_parameter_states() @property def fitter(self) -> MultiFitter: @@ -362,6 +382,7 @@ def add_sample_from_orso(self, sample: Sample) -> None: model.interface = self._calculator # Extract materials from the new model and add to project materials self._materials.extend(self._get_materials_from_model(model)) + self._sync_parameter_states() # Switch to the newly added model so its data is visible in the UI self.current_model_index = len(self._models) - 1 diff --git a/src/easyreflectometry/sample/elements/layers/layer.py b/src/easyreflectometry/sample/elements/layers/layer.py index 56c8bae1..513b18f9 100644 --- a/src/easyreflectometry/sample/elements/layers/layer.py +++ b/src/easyreflectometry/sample/elements/layers/layer.py @@ -6,7 +6,6 @@ from easyscience import global_object from easyscience.variable import Parameter -from easyreflectometry.limits import apply_default_limits from easyreflectometry.utils import get_as_parameter from ...base_core import BaseCore @@ -66,21 +65,23 @@ def __init__( if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) + thickness_value = thickness thickness = get_as_parameter( name='thickness', value=thickness, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Thickness', ) - apply_default_limits(thickness, 'thickness') + thickness.default_limits_pending = not isinstance(thickness_value, Parameter) + roughness_value = roughness roughness = get_as_parameter( name='roughness', value=roughness, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Roughness', ) - apply_default_limits(roughness, 'roughness') + roughness.default_limits_pending = not isinstance(roughness_value, Parameter) super().__init__( name=name, diff --git a/tests/sample/elements/layers/test_layer.py b/tests/sample/elements/layers/test_layer.py index 8058911d..1cbecb17 100644 --- a/tests/sample/elements/layers/test_layer.py +++ b/tests/sample/elements/layers/test_layer.py @@ -7,6 +7,7 @@ import unittest +import numpy as np from easyscience import global_object from numpy.testing import assert_almost_equal from numpy.testing import assert_equal @@ -28,13 +29,13 @@ def test_no_arguments(self): assert_equal(str(p.thickness.unit), 'Å') assert_equal(p.thickness.value, 10.0) assert_equal(p.thickness.min, 0.0) - assert_equal(p.thickness.max, 20.0) + assert_equal(p.thickness.max, np.inf) assert_equal(p.thickness.fixed, True) assert_equal(p.roughness.display_name, 'roughness') assert_equal(str(p.roughness.unit), 'Å') assert_equal(p.roughness.value, 3.3) assert_equal(p.roughness.min, 0.0) - assert_equal(p.roughness.max, 6.6) + assert_equal(p.roughness.max, np.inf) assert_equal(p.roughness.fixed, True) def test_shuffled_arguments(self): @@ -47,13 +48,13 @@ def test_shuffled_arguments(self): assert_equal(str(p.thickness.unit), 'Å') assert_equal(p.thickness.value, 5.0) assert_equal(p.thickness.min, 0.0) - assert_equal(p.thickness.max, 10.0) + assert_equal(p.thickness.max, np.inf) assert_equal(p.thickness.fixed, True) assert_equal(p.roughness.display_name, 'roughness') assert_equal(str(p.roughness.unit), 'Å') assert_equal(p.roughness.value, 2.0) assert_equal(p.roughness.min, 0.0) - assert_equal(p.roughness.max, 4.0) + assert_equal(p.roughness.max, np.inf) assert_equal(p.roughness.fixed, True) def test_only_roughness_key(self): @@ -62,7 +63,7 @@ def test_only_roughness_key(self): assert_equal(str(p.roughness.unit), 'Å') assert_equal(p.roughness.value, 10.0) assert_equal(p.roughness.min, 0.0) - assert_equal(p.roughness.max, 20.0) + assert_equal(p.roughness.max, np.inf) assert_equal(p.roughness.fixed, True) def test_only_roughness_key_paramter(self): @@ -78,7 +79,7 @@ def test_only_thickness_key(self): assert_equal(str(p.thickness.unit), 'Å') assert_equal(p.thickness.value, 10.0) assert_equal(p.thickness.min, 0.0) - assert_equal(p.thickness.max, 20.0) + assert_equal(p.thickness.max, np.inf) assert_equal(p.thickness.fixed, True) def test_only_thickness_key_paramter(self): diff --git a/tests/test_limits.py b/tests/test_limits.py index 3e394e41..1a512e96 100644 --- a/tests/test_limits.py +++ b/tests/test_limits.py @@ -1,5 +1,6 @@ import numpy as np import pytest +from easyscience import global_object from easyscience.variable import Parameter from easyreflectometry.limits import SCALE_LIMITS @@ -90,6 +91,9 @@ def test_unknown_kind_is_noop(self): class TestIntegrationWithConstructors: + def setup_method(self): + global_object.map._clear() + def test_material_gets_sld_limits(self): from easyreflectometry.sample.elements.materials.material import Material @@ -100,18 +104,70 @@ def test_material_gets_sld_limits(self): assert mat.isld.max == SLD_LIMITS[1] def test_layer_gets_percentage_limits(self): + from easyreflectometry.project import Project + + project = Project() + project.default_model() + layer = project.models[0].sample[1].layers[0] + assert layer.thickness.min == 50.0 + assert layer.thickness.max == 200.0 + assert layer.roughness.min == 1.5 + assert layer.roughness.max == 6.0 + + def test_project_applies_percentage_limits_to_enabled_layers(self): + from easyreflectometry.project import Project + + project = Project() + project.default_model() + layer = project.models[0].sample[1].layers[0] + assert layer.thickness.min == 50.0 + assert layer.thickness.max == 200.0 + assert layer.roughness.min == 1.5 + assert layer.roughness.max == 6.0 + + def test_project_skips_disabled_layer_limits(self): + from easyreflectometry.project import Project + + project = Project() + project.default_model() + superphase = project.models[0].sample[0].layers[0] + subphase = project.models[0].sample[-1].layers[-1] + + assert superphase.thickness.enabled is False + assert superphase.thickness.min == 0.0 + assert superphase.thickness.max == np.inf + assert superphase.roughness.enabled is False + assert superphase.roughness.min == 0.0 + assert superphase.roughness.max == np.inf + assert subphase.thickness.enabled is False + assert subphase.thickness.min == 0.0 + assert subphase.thickness.max == np.inf + + def test_project_does_not_overwrite_enabled_flag(self): + from easyreflectometry.project import Project + + project = Project() + project.default_model() + parameter = project.models[0].sample[0].layers[0].thickness + parameter.enabled = True + _ = project.parameters + assert parameter.enabled is True + + def test_layer_constructor_keeps_default_bounds_until_project_sync(self): from easyreflectometry.sample.elements.layers.layer import Layer layer = Layer(thickness=20.0, roughness=5.0) - assert layer.thickness.min == 0.0 # 0.0 is finite, kept - assert layer.thickness.max == 40.0 # 2.0 * 20.0 - assert layer.roughness.min == 0.0 # 0.0 is finite, kept - assert layer.roughness.max == 10.0 # 2.0 * 5.0 + assert layer.thickness.min == 0.0 + assert layer.thickness.max == np.inf + assert layer.roughness.min == 0.0 + assert layer.roughness.max == np.inf def test_layer_zero_thickness_unchanged(self): - from easyreflectometry.sample.elements.layers.layer import Layer + from easyreflectometry.project import Project - layer = Layer(thickness=0.0, roughness=0.0) + project = Project() + project.default_model() + layer = project.models[0].sample[0].layers[0] assert layer.thickness.min == 0.0 assert layer.thickness.max == np.inf assert layer.roughness.min == 0.0 From 44600a3218bff61fa04cf33244862fdbfa446ea6 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 10 Apr 2026 16:48:36 +0200 Subject: [PATCH 5/5] fixed tests --- tests/test_limits.py | 39 --------------------------------------- tests/test_project.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/tests/test_limits.py b/tests/test_limits.py index 1a512e96..e8320eb7 100644 --- a/tests/test_limits.py +++ b/tests/test_limits.py @@ -114,45 +114,6 @@ def test_layer_gets_percentage_limits(self): assert layer.roughness.min == 1.5 assert layer.roughness.max == 6.0 - def test_project_applies_percentage_limits_to_enabled_layers(self): - from easyreflectometry.project import Project - - project = Project() - project.default_model() - layer = project.models[0].sample[1].layers[0] - assert layer.thickness.min == 50.0 - assert layer.thickness.max == 200.0 - assert layer.roughness.min == 1.5 - assert layer.roughness.max == 6.0 - - def test_project_skips_disabled_layer_limits(self): - from easyreflectometry.project import Project - - project = Project() - project.default_model() - superphase = project.models[0].sample[0].layers[0] - subphase = project.models[0].sample[-1].layers[-1] - - assert superphase.thickness.enabled is False - assert superphase.thickness.min == 0.0 - assert superphase.thickness.max == np.inf - assert superphase.roughness.enabled is False - assert superphase.roughness.min == 0.0 - assert superphase.roughness.max == np.inf - assert subphase.thickness.enabled is False - assert subphase.thickness.min == 0.0 - assert subphase.thickness.max == np.inf - - def test_project_does_not_overwrite_enabled_flag(self): - from easyreflectometry.project import Project - - project = Project() - project.default_model() - parameter = project.models[0].sample[0].layers[0].thickness - parameter.enabled = True - _ = project.parameters - assert parameter.enabled is True - def test_layer_constructor_keeps_default_bounds_until_project_sync(self): from easyreflectometry.sample.elements.layers.layer import Layer diff --git a/tests/test_project.py b/tests/test_project.py index 3876e4b8..3c390960 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -720,6 +720,35 @@ def test_parameters(self): assert len(parameters) == 14 assert isinstance(parameters[0], Parameter) + def test_parameters_enabled_flags(self): + global_object.map._clear() + project = Project() + project.default_model() + + sample = project.models[0].sample + superphase = sample.superphase + subphase = sample.subphase + middle_layer = sample[1].front_layer + + assert superphase.thickness.enabled is False + assert superphase.roughness.enabled is False + assert subphase.thickness.enabled is False + assert getattr(subphase.roughness, 'enabled', True) is True + assert getattr(middle_layer.thickness, 'enabled', True) is True + assert getattr(middle_layer.roughness, 'enabled', True) is True + + def test_parameters_read_does_not_overwrite_enabled_flag(self): + global_object.map._clear() + project = Project() + project.default_model() + + parameter = project.models[0].sample[0].layers[0].thickness + parameter.enabled = True + + _ = project.parameters + + assert parameter.enabled is True + def test_current_experiment_index_getter_and_setter(self): global_object.map._clear() project = Project()