Skip to content
Open
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
44 changes: 44 additions & 0 deletions src/easyreflectometry/limits.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/easyreflectometry/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions src/easyreflectometry/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,6 +92,52 @@ def parameters(self) -> List[Parameter]:
parameters.append(param)
return parameters

def _sync_parameter_states(self) -> None:
"""Apply project-level parameter enablement and default limits.

Superphase thickness/roughness and subphase thickness are physically
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

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))

for model in self._models:
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):
if self._q_min is None:
Expand Down Expand Up @@ -204,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:
Expand Down Expand Up @@ -334,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

Expand Down
5 changes: 5 additions & 0 deletions src/easyreflectometry/sample/elements/layers/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +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',
)
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',
)
roughness.default_limits_pending = not isinstance(roughness_value, Parameter)

super().__init__(
name=name,
Expand Down
4 changes: 4 additions & 0 deletions src/easyreflectometry/sample/elements/materials/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/model/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
25 changes: 12 additions & 13 deletions tests/sample/elements/materials/test_material.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -38,23 +37,23 @@ 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):
p = Material(sld=10)
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):
Expand All @@ -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):
Expand Down
Loading
Loading