Skip to content

layer

USER_CONSTRAINT_FLAG = '_easyreflectometry_user_constraint' module-attribute

DEFAULTS = {'thickness': {'description': 'The thickness of the layer in angstroms', 'url': 'https://github.com/reflectivity/edu_outreach/blob/master/refl_maths/paper.tex', 'value': 10.0, 'unit': 'angstrom', 'min': 0.0, 'max': np.inf, 'fixed': True}, 'roughness': {'description': 'The interfacial roughness, Nevot-Croce, for the layer in angstroms.', 'url': 'https://doi.org/10.1051/rphysap:01980001503076100', 'value': 3.3, 'unit': 'angstrom', 'min': 0.0, 'max': np.inf, 'fixed': True}} module-attribute

BaseCore

Bases: ModelBase

Local base class for sample-tree objects (Material, Layer, assemblies).

Built on top of easyscience.base_classes.ModelBase (the replacement for the deprecated ObjBase). On top of ModelBase this class adds:

  • a name property
  • an interface property whose setter propagates the calculator interface to child objects and (re)generates bindings
  • a yaml-formatted __repr__ driven by an abstract _dict_repr
  • an _get_linkable_attributes compatibility shim used by the calculator's InterfaceFactoryTemplate.generate_bindings
  • an as_dict alias for to_dict

Subclass __init__ convention: 1. Build child Parameters / sub-objects. 2. Call super().__init__(name=..., unique_name=...). 3. Assign children to backing fields (self._sld = sld etc.) or pass them as **kwargs to this base class (transitional path; each kwarg is stored as a plain instance attribute). 4. Last: self.interface = interface (triggers generate_bindings).

Source code in src/easyreflectometry/sample/base_core.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
class BaseCore(ModelBase):
    """Local base class for sample-tree objects (Material, Layer, assemblies).

    Built on top of `easyscience.base_classes.ModelBase` (the replacement for
    the deprecated `ObjBase`). On top of `ModelBase` this class adds:

    - a `name` property
    - an `interface` property whose setter propagates the calculator interface
      to child objects and (re)generates bindings
    - a yaml-formatted `__repr__` driven by an abstract `_dict_repr`
    - an `_get_linkable_attributes` compatibility shim used by the calculator's
      `InterfaceFactoryTemplate.generate_bindings`
    - an `as_dict` alias for `to_dict`

    Subclass `__init__` convention:
        1. Build child Parameters / sub-objects.
        2. Call ``super().__init__(name=..., unique_name=...)``.
        3. Assign children to backing fields (``self._sld = sld`` etc.) or pass
           them as ``**kwargs`` to this base class (transitional path; each
           kwarg is stored as a plain instance attribute).
        4. Last: ``self.interface = interface`` (triggers ``generate_bindings``).
    """

    def __init__(
        self,
        name: str,
        interface: Any = None,
        unique_name: Optional[str] = None,
        display_name: Optional[str] = None,
        **kwargs: Any,
    ):
        super().__init__(unique_name=unique_name, display_name=display_name)
        self._name = name
        self._interface = None
        # `user_data` is part of the legacy `BasedBase` API — a free-form dict
        # callers stash arbitrary metadata in. Kept for back-compat with code
        # like `Project.replace_models_from_orso` which stores the ORSO sample
        # name on the model.
        self.user_data: dict = {}

        # Transitional path: subclasses still pass parameter / child objects via
        # **kwargs (legacy `ObjBase` accepted them and stashed in `_kwargs`).
        # Here we simply store each one as a plain instance attribute so
        # `obj.<key>` keeps working. Step 2 of the migration replaces this with
        # explicit assignments in each subclass.
        for key, value in kwargs.items():
            setattr(self, key, value)

        # Assign interface LAST so children exist when generate_bindings runs.
        if interface is not None:
            self.interface = interface

    # ----- name -----

    @property
    def name(self) -> str:
        """Common (display-friendly) name."""
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        self._name = value

    # ----- interface -----

    @property
    def interface(self) -> Any:
        """The calculator interface attached to this object (may be None)."""
        return self._interface

    @interface.setter
    def interface(self, new_interface: Any) -> None:
        self._interface = new_interface
        if new_interface is not None:
            self.generate_bindings()

    def generate_bindings(self) -> None:
        """Propagate the interface to child objects, then bind via the calculator.

        We propagate to any child whose class advertises an ``interface`` property
        with a setter. That includes both the new `BaseCore`-based children and
        legacy `BasedBase`-derived collections (which extend `SerializerComponent`,
        not `NewBase`).
        """
        if self._interface is None:
            raise AttributeError('Interface error for generating bindings. `interface` has to be set.')
        for attr in self._iter_public_children():
            if self._has_interface_setter(type(attr)):
                attr.interface = self._interface
        self._interface.generate_bindings(self)

    def _iter_public_children(self):
        """Yield public child objects from both class-level (dir) and instance-level (__dict__) attrs.

        `NewBase.__dir__` exposes only class attributes, which means plain instance
        attributes (the transitional `setattr(self, key, value)` path in
        `__init__`) are invisible to a pure `dir()` scan. To bridge both worlds —
        legacy subclasses that still use plain attrs, and migrated subclasses that
        expose children via `@property` accessors — this helper unions the two.
        Once all subclasses migrate to `@property`-backed children with `_field`
        backing storage, the `__dict__` branch becomes a no-op (private names are
        skipped).
        """
        seen_ids = {id(self)}
        # Class-level (properties, methods named like sld/isld/material).
        for attr_name in dir(self):
            if attr_name.startswith('_') or attr_name in ('interface', 'name'):
                continue
            try:
                attr = getattr(self, attr_name, None)
            except AttributeError:
                # A subclass @property may legitimately raise AttributeError
                # mid-construction (the `_field` backing isn't set yet); skip
                # those entries silently. Other exceptions should propagate.
                continue
            if attr is None or id(attr) in seen_ids:
                continue
            seen_ids.add(id(attr))
            yield attr
        # Instance-level (plain attrs set via the transitional kwargs path).
        for attr_name, attr in list(self.__dict__.items()):
            if attr_name.startswith('_') or attr_name in ('interface', 'name'):
                continue
            if attr is None or id(attr) in seen_ids:
                continue
            seen_ids.add(id(attr))
            yield attr

    @staticmethod
    def _has_interface_setter(obj_type: type) -> bool:
        for klass in obj_type.__mro__:
            prop = klass.__dict__.get('interface')
            if isinstance(prop, property):
                return prop.fset is not None
        return False

    # ----- compatibility shims -----

    def _get_linkable_attributes(self):
        """Used by `easyscience.fitting.calculators.interface_factory.generate_bindings`.

        Returns the same set as :meth:`get_all_variables` (the modern API on
        :class:`ModelBase`). Kept under the legacy name because the calculator
        in `easyscience` core has not yet been updated.
        """
        return self.get_all_variables()

    def get_parameters(self):
        """Compatibility shim for legacy callers; prefer :meth:`get_all_parameters`."""
        return self.get_all_parameters()

    def _add_component(self, key: str, component: Any) -> None:
        """Compatibility shim for legacy `ObjBase._add_component`.

        Legacy callers (e.g. `LayerAreaPerMolecule`) used this to register an
        additional child after `super().__init__`. In the new world the
        equivalent is simply setting an attribute; we do that here so the
        existing call sites keep working until Step 2 removes them.
        """
        setattr(self, key, component)

    def get_all_variables(self):
        """Discover Parameters/Descriptors across both class-level and instance-level attrs.

        `ModelBase.get_all_variables` walks `dir(self)`, which `NewBase` restricts
        to class attributes only. During the transition some subclasses still
        store child Parameters as plain instance attributes (see the kwargs path
        in :meth:`__init__`); those are invisible to `dir()`. We therefore also
        scan `self.__dict__` for `DescriptorBase` instances and for child
        ModelBase objects whose own `get_all_variables` we recurse into.
        """
        from easyscience.variable.descriptor_base import DescriptorBase

        out: list = []
        seen_param_ids: set[int] = set()
        for attr in self._iter_public_children():
            if isinstance(attr, DescriptorBase):
                if id(attr) not in seen_param_ids:
                    seen_param_ids.add(id(attr))
                    out.append(attr)
            elif hasattr(attr, 'get_all_variables'):
                for v in attr.get_all_variables():
                    if id(v) not in seen_param_ids:
                        seen_param_ids.add(id(v))
                        out.append(v)
        return out

    def to_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]:
        """Serialize, skipping the calculator interface and unique_name by default.

        The calculator (`CalculatorFactory`) is not serializable and is not part
        of the model's persistent state — round-trip code that needs it back
        reattaches it after `from_dict`. The legacy `ObjBase`-based pipeline
        achieved the same by never including `interface` in its `_kwargs`
        encoding; we replicate that here.

        `unique_name` is also stripped by default, matching the legacy
        `BasedBase.as_dict` contract. The installed `SerializerBase` does *not*
        propagate per-object `_default_unique_name` to nested children — if
        we leave it in, child Parameters end up with explicit unique_names in
        the dict (e.g. `Parameter_0`) that subsequently collide on reload when
        the global counter restarts from 0.

        Pass a *copy* of `skip` to super since `NewBase.to_dict` mutates the
        list (appends `unique_name` / `display_name` if those are default).
        """
        skip = list(skip or [])
        if 'interface' not in skip:
            skip.append('interface')
        if 'unique_name' not in skip:
            skip.append('unique_name')
        return super().to_dict(skip=list(skip))

    def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]:
        """Compatibility alias for :meth:`to_dict`."""
        return self.to_dict(skip=skip)

    def _convert_to_dict(self, d: dict, encoder=None, skip: Optional[list[str]] = None, **kwargs) -> dict:
        """Serializer hook, called for this object at any depth: tag it, or refer
        to its first occurrence (see :mod:`easyreflectometry.sample.references`)."""
        return references.tag(self, d)

    @classmethod
    def from_dict(cls, obj_dict: dict) -> BaseCore:
        """Rebuild, resolving shared children; see :mod:`easyreflectometry.sample.references`."""
        if references.is_reference(obj_dict):
            # Unresolvable reference (reported by `references.resolve`): a default object.
            obj_dict = {'@module': obj_dict['@module'], '@class': obj_dict['@class']}
        instance = super().from_dict(references.build_children(obj_dict))
        references.register(obj_dict, instance)
        return instance

    # ----- repr -----

    @property
    @abstractmethod
    def _dict_repr(self) -> dict[str, Any]: ...

    def __repr__(self) -> str:
        """Yaml-formatted multi-line string built from :attr:`_dict_repr`."""
        return yaml_dump(self._dict_repr)

interface property writable

The calculator interface attached to this object (may be None).

name property writable

Common (display-friendly) name.

generate_bindings()

Propagate the interface to child objects, then bind via the calculator.

We propagate to any child whose class advertises an interface property with a setter. That includes both the new BaseCore-based children and legacy BasedBase-derived collections (which extend SerializerComponent, not NewBase).

Source code in src/easyreflectometry/sample/base_core.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def generate_bindings(self) -> None:
    """Propagate the interface to child objects, then bind via the calculator.

    We propagate to any child whose class advertises an ``interface`` property
    with a setter. That includes both the new `BaseCore`-based children and
    legacy `BasedBase`-derived collections (which extend `SerializerComponent`,
    not `NewBase`).
    """
    if self._interface is None:
        raise AttributeError('Interface error for generating bindings. `interface` has to be set.')
    for attr in self._iter_public_children():
        if self._has_interface_setter(type(attr)):
            attr.interface = self._interface
    self._interface.generate_bindings(self)

get_parameters()

Compatibility shim for legacy callers; prefer :meth:get_all_parameters.

Source code in src/easyreflectometry/sample/base_core.py
163
164
165
def get_parameters(self):
    """Compatibility shim for legacy callers; prefer :meth:`get_all_parameters`."""
    return self.get_all_parameters()

get_all_variables()

Discover Parameters/Descriptors across both class-level and instance-level attrs.

ModelBase.get_all_variables walks dir(self), which NewBase restricts to class attributes only. During the transition some subclasses still store child Parameters as plain instance attributes (see the kwargs path in :meth:__init__); those are invisible to dir(). We therefore also scan self.__dict__ for DescriptorBase instances and for child ModelBase objects whose own get_all_variables we recurse into.

Source code in src/easyreflectometry/sample/base_core.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def get_all_variables(self):
    """Discover Parameters/Descriptors across both class-level and instance-level attrs.

    `ModelBase.get_all_variables` walks `dir(self)`, which `NewBase` restricts
    to class attributes only. During the transition some subclasses still
    store child Parameters as plain instance attributes (see the kwargs path
    in :meth:`__init__`); those are invisible to `dir()`. We therefore also
    scan `self.__dict__` for `DescriptorBase` instances and for child
    ModelBase objects whose own `get_all_variables` we recurse into.
    """
    from easyscience.variable.descriptor_base import DescriptorBase

    out: list = []
    seen_param_ids: set[int] = set()
    for attr in self._iter_public_children():
        if isinstance(attr, DescriptorBase):
            if id(attr) not in seen_param_ids:
                seen_param_ids.add(id(attr))
                out.append(attr)
        elif hasattr(attr, 'get_all_variables'):
            for v in attr.get_all_variables():
                if id(v) not in seen_param_ids:
                    seen_param_ids.add(id(v))
                    out.append(v)
    return out

to_dict(skip=None)

Serialize, skipping the calculator interface and unique_name by default.

The calculator (CalculatorFactory) is not serializable and is not part of the model's persistent state — round-trip code that needs it back reattaches it after from_dict. The legacy ObjBase-based pipeline achieved the same by never including interface in its _kwargs encoding; we replicate that here.

unique_name is also stripped by default, matching the legacy BasedBase.as_dict contract. The installed SerializerBase does not propagate per-object _default_unique_name to nested children — if we leave it in, child Parameters end up with explicit unique_names in the dict (e.g. Parameter_0) that subsequently collide on reload when the global counter restarts from 0.

Pass a copy of skip to super since NewBase.to_dict mutates the list (appends unique_name / display_name if those are default).

Source code in src/easyreflectometry/sample/base_core.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def to_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]:
    """Serialize, skipping the calculator interface and unique_name by default.

    The calculator (`CalculatorFactory`) is not serializable and is not part
    of the model's persistent state — round-trip code that needs it back
    reattaches it after `from_dict`. The legacy `ObjBase`-based pipeline
    achieved the same by never including `interface` in its `_kwargs`
    encoding; we replicate that here.

    `unique_name` is also stripped by default, matching the legacy
    `BasedBase.as_dict` contract. The installed `SerializerBase` does *not*
    propagate per-object `_default_unique_name` to nested children — if
    we leave it in, child Parameters end up with explicit unique_names in
    the dict (e.g. `Parameter_0`) that subsequently collide on reload when
    the global counter restarts from 0.

    Pass a *copy* of `skip` to super since `NewBase.to_dict` mutates the
    list (appends `unique_name` / `display_name` if those are default).
    """
    skip = list(skip or [])
    if 'interface' not in skip:
        skip.append('interface')
    if 'unique_name' not in skip:
        skip.append('unique_name')
    return super().to_dict(skip=list(skip))

as_dict(skip=None)

Compatibility alias for :meth:to_dict.

Source code in src/easyreflectometry/sample/base_core.py
229
230
231
def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]:
    """Compatibility alias for :meth:`to_dict`."""
    return self.to_dict(skip=skip)

from_dict(obj_dict) classmethod

Rebuild, resolving shared children; see :mod:easyreflectometry.sample.references.

Source code in src/easyreflectometry/sample/base_core.py
238
239
240
241
242
243
244
245
246
@classmethod
def from_dict(cls, obj_dict: dict) -> BaseCore:
    """Rebuild, resolving shared children; see :mod:`easyreflectometry.sample.references`."""
    if references.is_reference(obj_dict):
        # Unresolvable reference (reported by `references.resolve`): a default object.
        obj_dict = {'@module': obj_dict['@module'], '@class': obj_dict['@class']}
    instance = super().from_dict(references.build_children(obj_dict))
    references.register(obj_dict, instance)
    return instance

__repr__()

Yaml-formatted multi-line string built from :attr:_dict_repr.

Source code in src/easyreflectometry/sample/base_core.py
254
255
256
def __repr__(self) -> str:
    """Yaml-formatted multi-line string built from :attr:`_dict_repr`."""
    return yaml_dump(self._dict_repr)

Material

Bases: BaseCore

Source code in src/easyreflectometry/sample/elements/materials/material.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class Material(BaseCore):
    def __init__(
        self,
        sld: Union[Parameter, float, None] = None,
        isld: Union[Parameter, float, None] = None,
        name: str = 'EasyMaterial',
        unique_name: Optional[str] = None,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        sld : Union[Parameter, float, None], optional
            Real scattering length density. By default, None.
        isld : Union[Parameter, float, None], optional
            Imaginary scattering length density. By default, None.
        name : str, optional
            Name of the material. By default, 'EasyMaterial'.
        interface :
            Calculator interface. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        sld = get_as_parameter(
            name='sld',
            value=sld,
            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, unique_name=unique_name)
        self._sld = sld
        self._isld = isld

        if interface is not None:
            self.interface = interface

    @property
    def sld(self) -> Parameter:
        return self._sld

    @sld.setter
    def sld(self, value: float) -> None:
        self._sld.value = value

    @property
    def isld(self) -> Parameter:
        return self._isld

    @isld.setter
    def isld(self, value: float) -> None:
        self._isld.value = value

    # Representation
    @property
    def _dict_repr(self) -> dict[str, str]:
        """A simplified dict representation."""
        return {
            self.name: {
                'sld': f'{self._sld.value:.3f}e-6 {self._sld.unit}',
                'isld': f'{self._isld.value:.3f}e-6 {self._isld.unit}',
            }
        }

__init__(sld=None, isld=None, name='EasyMaterial', unique_name=None, interface=None)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
sld Union[Parameter, float, None]

Real scattering length density. By default, None.

None
isld Union[Parameter, float, None]

Imaginary scattering length density. By default, None.

None
name str

Name of the material. By default, 'EasyMaterial'.

'EasyMaterial'
interface

Calculator interface. By default, None.

None
Source code in src/easyreflectometry/sample/elements/materials/material.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(
    self,
    sld: Union[Parameter, float, None] = None,
    isld: Union[Parameter, float, None] = None,
    name: str = 'EasyMaterial',
    unique_name: Optional[str] = None,
    interface=None,
):
    """Constructor.

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    sld : Union[Parameter, float, None], optional
        Real scattering length density. By default, None.
    isld : Union[Parameter, float, None], optional
        Imaginary scattering length density. By default, None.
    name : str, optional
        Name of the material. By default, 'EasyMaterial'.
    interface :
        Calculator interface. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    sld = get_as_parameter(
        name='sld',
        value=sld,
        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, unique_name=unique_name)
    self._sld = sld
    self._isld = isld

    if interface is not None:
        self.interface = interface

LayerMagnetism

Bases: BaseCore

Magnetic properties of a layer: magnetic SLD amplitude and in-plane moment angle.

Attach to a Layer via its magnetism argument/property to make the layer magnetic. Both attributes are Parameter objects, so they can be fitted and serialized like any other sample parameter. A calculator that supports magnetism (refl1d) binds them as magnetism_rhoM / magnetism_thetaM on the corresponding slab.

Source code in src/easyreflectometry/sample/elements/layers/layer_magnetism.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class LayerMagnetism(BaseCore):
    """Magnetic properties of a layer: magnetic SLD amplitude and in-plane moment angle.

    Attach to a `Layer` via its `magnetism` argument/property to make the layer magnetic.
    Both attributes are `Parameter` objects, so they can be fitted and serialized like any
    other sample parameter. A calculator that supports magnetism (refl1d) binds them as
    `magnetism_rhoM` / `magnetism_thetaM` on the corresponding slab.
    """

    def __init__(
        self,
        rho_m: Union[Parameter, float, None] = None,
        theta_m: Union[Parameter, float, None] = None,
        name: str = 'EasyLayerMagnetism',
        unique_name: Optional[str] = None,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        rho_m : Union[Parameter, float, None], optional
            Magnetic scattering length density in e-6 per squared angstrom. By default, 0.
        theta_m : Union[Parameter, float, None], optional
            In-plane moment angle in degrees. By default, 270 (aligned with the default
            guide field, i.e. no spin-flip).
        name : str, optional
            Name of the magnetism element. By default, 'EasyLayerMagnetism'.
        unique_name : Optional[str], optional
            By default, None.
        interface :
            Calculator interface. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        rho_m_value = rho_m
        rho_m = get_as_parameter(
            name='rho_m',
            value=rho_m,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_RhoM',
        )
        # The default bounds are infinite; `Project._sync_parameter_states`
        # narrows them to the shared SLD window unless the caller passed an
        # explicit Parameter with its own bounds (same contract as `Layer`).
        rho_m.default_limits_pending = not isinstance(rho_m_value, Parameter)
        theta_m = get_as_parameter(
            name='theta_m',
            value=theta_m,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_ThetaM',
        )

        super().__init__(name=name, unique_name=unique_name)
        self._rho_m = rho_m
        self._theta_m = theta_m

        if interface is not None:
            self.interface = interface

    @property
    def rho_m(self) -> Parameter:
        return self._rho_m

    @rho_m.setter
    def rho_m(self, value: float) -> None:
        self._rho_m.value = value

    @property
    def theta_m(self) -> Parameter:
        return self._theta_m

    @theta_m.setter
    def theta_m(self, value: float) -> None:
        self._theta_m.value = value

    # Representation
    @property
    def _dict_repr(self) -> dict[str, str]:
        """A simplified dict representation."""
        return {
            self.name: {
                'rho_m': f'{self._rho_m.value:.3f}e-6 {self._rho_m.unit}',
                'theta_m': f'{self._theta_m.value:.3f} {self._theta_m.unit}',
            }
        }

__init__(rho_m=None, theta_m=None, name='EasyLayerMagnetism', unique_name=None, interface=None)

Constructor.

Parameters:

Name Type Description Default
rho_m Union[Parameter, float, None]

Magnetic scattering length density in e-6 per squared angstrom. By default, 0.

None
theta_m Union[Parameter, float, None]

In-plane moment angle in degrees. By default, 270 (aligned with the default guide field, i.e. no spin-flip).

None
name str

Name of the magnetism element. By default, 'EasyLayerMagnetism'.

'EasyLayerMagnetism'
unique_name Optional[str]

By default, None.

None
interface

Calculator interface. By default, None.

None
Source code in src/easyreflectometry/sample/elements/layers/layer_magnetism.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def __init__(
    self,
    rho_m: Union[Parameter, float, None] = None,
    theta_m: Union[Parameter, float, None] = None,
    name: str = 'EasyLayerMagnetism',
    unique_name: Optional[str] = None,
    interface=None,
):
    """Constructor.

    Parameters
    ----------
    rho_m : Union[Parameter, float, None], optional
        Magnetic scattering length density in e-6 per squared angstrom. By default, 0.
    theta_m : Union[Parameter, float, None], optional
        In-plane moment angle in degrees. By default, 270 (aligned with the default
        guide field, i.e. no spin-flip).
    name : str, optional
        Name of the magnetism element. By default, 'EasyLayerMagnetism'.
    unique_name : Optional[str], optional
        By default, None.
    interface :
        Calculator interface. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    rho_m_value = rho_m
    rho_m = get_as_parameter(
        name='rho_m',
        value=rho_m,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_RhoM',
    )
    # The default bounds are infinite; `Project._sync_parameter_states`
    # narrows them to the shared SLD window unless the caller passed an
    # explicit Parameter with its own bounds (same contract as `Layer`).
    rho_m.default_limits_pending = not isinstance(rho_m_value, Parameter)
    theta_m = get_as_parameter(
        name='theta_m',
        value=theta_m,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_ThetaM',
    )

    super().__init__(name=name, unique_name=unique_name)
    self._rho_m = rho_m
    self._theta_m = theta_m

    if interface is not None:
        self.interface = interface

Layer

Bases: BaseCore

Source code in src/easyreflectometry/sample/elements/layers/layer.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class Layer(BaseCore):
    def __init__(
        self,
        material: Union[Material, None] = None,
        thickness: Union[Parameter, float, None] = None,
        roughness: Union[Parameter, float, None] = None,
        name: str = 'EasyLayer',
        unique_name: Optional[str] = None,
        interface=None,
        magnetism: Union[LayerMagnetism, None] = None,
        rho_m_coupled: bool = False,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        material : Union[Material, None], optional
            The material for the layer. By default, None.
        thickness : Union[Parameter, float, None], optional
            Layer thickness in Angstrom. By default, None.
        roughness : Union[Parameter, float, None], optional
            Upper roughness on the layer in Angstrom. By default, None.
        magnetism : Union[LayerMagnetism, None], optional
            Magnetic properties of the layer; None for a non-magnetic layer.
            By default, None.
        rho_m_coupled : bool, optional
            Take the magnetic SLD from the material (see :attr:`rho_m_coupled`).
            By default, False.
        name : str, optional
            Name of the layer. By default, 'EasyLayer'.
        interface :
            Interface object. By default, None.
        """
        if material is None:
            material = Material(interface=interface)

        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, unique_name=unique_name)
        self._material = material
        self._thickness = thickness
        self._roughness = roughness
        self._magnetism = magnetism
        self._rho_m_coupled = False
        if rho_m_coupled:
            self.rho_m_coupled = True

        if interface is not None:
            self.interface = interface

    # ----- interface (override BaseCore's to switch on calculator magnetism) -----

    @BaseCore.interface.setter
    def interface(self, new_interface) -> None:
        """Set the interface; runs `generate_bindings` and, for a magnetic layer,
        enables magnetism on the calculator (raising if it does not support it).
        """
        BaseCore.interface.fset(self, new_interface)
        if new_interface is not None and self._magnetism is not None:
            self._enable_calculator_magnetism()

    def _enable_calculator_magnetism(self) -> None:
        """Turn on magnetism on the attached calculator.

        Raises `NotImplementedError` when the active calculator cannot model
        magnetic samples (e.g. refnx or bornagain).
        """
        self.interface().include_magnetism = True

    @property
    def material(self) -> Material:
        return self._material

    @material.setter
    def material(self, value: Material) -> None:
        coupled = self.rho_m_coupled
        self._material = value
        self._reconcile_rho_m_coupling(coupled)

    @property
    def thickness(self) -> Parameter:
        return self._thickness

    @thickness.setter
    def thickness(self, value: float) -> None:
        self._thickness.value = value

    @property
    def roughness(self) -> Parameter:
        return self._roughness

    @roughness.setter
    def roughness(self, value: float) -> None:
        self._roughness.value = value

    @property
    def magnetism(self) -> Optional[LayerMagnetism]:
        return self._magnetism

    @magnetism.setter
    def magnetism(self, value: Optional[LayerMagnetism]) -> None:
        """Attach or remove the magnetic properties of this layer.

        Attaching regenerates the calculator bindings so `rho_m`/`theta_m` become
        live on the backend; removing drops the layer's magnetic state from the
        backend (and switches calculator magnetism off entirely when this was the
        last magnetic layer).
        """
        previous = self._magnetism
        coupled = self.rho_m_coupled
        if previous is not None and previous is not value:
            if value is None and self.interface is not None:
                self.interface().remove_layer_magnetism(self.unique_name)
            # The detached object must not follow the material any more...
            if coupled:
                _release(previous.rho_m)
            # ...and its value changes must no longer reach the backend.
            # `property()` (fget/fset/fdel all None) is easyscience's own "no
            # callback" sentinel -- `Parameter.__copy__` sets it the same way --
            # so a later `fset` guard in `Parameter` cleanly no-ops.
            previous.rho_m._callback = property()
            previous.theta_m._callback = property()
        self._magnetism = value
        self._reconcile_rho_m_coupling(coupled)
        if value is not None and self.interface is not None:
            self._enable_calculator_magnetism()
            self.generate_bindings()

    @property
    def rho_m_coupled(self) -> bool:
        """Whether the layer's magnetic SLD is the material's ``rho_m``
        (a :class:`~easyreflectometry.sample.MaterialMagnetic`).

        When True, ``magnetism.rho_m`` is a dependent parameter following the
        current material; the layer keeps its own ``theta_m``. Opt-in: layers of
        one material may carry different net moments. Setting it attaches a
        default :class:`LayerMagnetism` when the layer has none, and requires a
        material with ``rho_m``. Uncoupling keeps the current value. Removing
        the magnetism, or a user ``unconstrain`` of ``rho_m``, uncouples.

        Only the layer's own wiring counts: a user constraint placed on
        ``rho_m`` (:func:`~easyreflectometry.constraints.constrain`) reads as
        uncoupled, is saved as the user constraint it is, and is left alone
        when the material is later replaced. Setting this to True explicitly
        replaces such a constraint with the coupling.
        """
        if not self._rho_m_coupled or self._magnetism is None:
            return False
        rho_m = self._magnetism.rho_m
        if rho_m.independent or getattr(rho_m, USER_CONSTRAINT_FLAG, False):
            return False
        source = getattr(self._material, 'rho_m', None)
        dependency_map = rho_m.dependency_map
        return source is not None and len(dependency_map) == 1 and dependency_map.get('rho_m') is source

    @rho_m_coupled.setter
    def rho_m_coupled(self, couple: bool) -> None:
        couple = bool(couple)
        if couple and getattr(self._material, 'rho_m', None) is None:
            raise ValueError(f'The material of layer {self.name!r} has no magnetic SLD to couple to.')
        if couple and self._magnetism is None:
            self.magnetism = LayerMagnetism()
        if not couple and self.rho_m_coupled:
            _release(self._magnetism.rho_m)
        self._reconcile_rho_m_coupling(couple)

    def _reconcile_rho_m_coupling(self, couple: bool) -> None:
        """Re-establish the invariant of :attr:`rho_m_coupled` after a change of
        material, magnetism or coupling: coupled means the current magnetism's
        ``rho_m`` follows the current material's ``rho_m``.

        `couple` is the state to keep, read *before* the change (afterwards the
        dependency may point at a replaced object).
        """
        self._rho_m_coupled = False
        if not couple or self._magnetism is None:
            return
        rho_m = self._magnetism.rho_m
        source = getattr(self._material, 'rho_m', None)
        if source is None:
            warnings.warn(
                f'The material of layer {self.name!r} has no magnetic SLD; '
                'the layer keeps its current rho_m and is no longer coupled.',
                UserWarning,
                stacklevel=3,
            )
            if not rho_m.independent:
                _release(rho_m)
            return
        # Drops any previous dependency, including a stale user-constraint marker
        # that would make the project persist this internal wiring.
        unconstrain(rho_m)
        rho_m.make_dependent_on(dependency_expression='rho_m', dependency_map={'rho_m': source})
        self._rho_m_coupled = True

    def assign_material(self, material: Material) -> None:
        """Assign a material to the layer interface.

        Parameters
        ----------
        material : Material
            The material to assign to the layer.
        """
        coupled = self.rho_m_coupled
        self._material = material
        self._reconcile_rho_m_coupling(coupled)
        if self.interface is not None:
            self.interface().assign_material_to_layer(self.material.unique_name, self.unique_name)

    # Representation
    @property
    def _dict_repr(self) -> dict[str, str]:
        """A simplified dict representation."""
        this_dict = {
            self.name: {
                'material': self.material._dict_repr,
                'thickness': f'{self.thickness.value:.3f} {self.thickness.unit}',
                'roughness': f'{self.roughness.value:.3f} {self.roughness.unit}',
            }
        }
        if self._magnetism is not None:
            this_dict[self.name]['magnetism'] = self._magnetism._dict_repr
        return this_dict

rho_m_coupled property writable

Whether the layer's magnetic SLD is the material's rho_m (a :class:~easyreflectometry.sample.MaterialMagnetic).

When True, magnetism.rho_m is a dependent parameter following the current material; the layer keeps its own theta_m. Opt-in: layers of one material may carry different net moments. Setting it attaches a default :class:LayerMagnetism when the layer has none, and requires a material with rho_m. Uncoupling keeps the current value. Removing the magnetism, or a user unconstrain of rho_m, uncouples.

Only the layer's own wiring counts: a user constraint placed on rho_m (:func:~easyreflectometry.constraints.constrain) reads as uncoupled, is saved as the user constraint it is, and is left alone when the material is later replaced. Setting this to True explicitly replaces such a constraint with the coupling.

__init__(material=None, thickness=None, roughness=None, name='EasyLayer', unique_name=None, interface=None, magnetism=None, rho_m_coupled=False)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
material Union[Material, None]

The material for the layer. By default, None.

None
thickness Union[Parameter, float, None]

Layer thickness in Angstrom. By default, None.

None
roughness Union[Parameter, float, None]

Upper roughness on the layer in Angstrom. By default, None.

None
magnetism Union[LayerMagnetism, None]

Magnetic properties of the layer; None for a non-magnetic layer. By default, None.

None
rho_m_coupled bool

Take the magnetic SLD from the material (see :attr:rho_m_coupled). By default, False.

False
name str

Name of the layer. By default, 'EasyLayer'.

'EasyLayer'
interface

Interface object. By default, None.

None
Source code in src/easyreflectometry/sample/elements/layers/layer.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def __init__(
    self,
    material: Union[Material, None] = None,
    thickness: Union[Parameter, float, None] = None,
    roughness: Union[Parameter, float, None] = None,
    name: str = 'EasyLayer',
    unique_name: Optional[str] = None,
    interface=None,
    magnetism: Union[LayerMagnetism, None] = None,
    rho_m_coupled: bool = False,
):
    """Constructor.

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    material : Union[Material, None], optional
        The material for the layer. By default, None.
    thickness : Union[Parameter, float, None], optional
        Layer thickness in Angstrom. By default, None.
    roughness : Union[Parameter, float, None], optional
        Upper roughness on the layer in Angstrom. By default, None.
    magnetism : Union[LayerMagnetism, None], optional
        Magnetic properties of the layer; None for a non-magnetic layer.
        By default, None.
    rho_m_coupled : bool, optional
        Take the magnetic SLD from the material (see :attr:`rho_m_coupled`).
        By default, False.
    name : str, optional
        Name of the layer. By default, 'EasyLayer'.
    interface :
        Interface object. By default, None.
    """
    if material is None:
        material = Material(interface=interface)

    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, unique_name=unique_name)
    self._material = material
    self._thickness = thickness
    self._roughness = roughness
    self._magnetism = magnetism
    self._rho_m_coupled = False
    if rho_m_coupled:
        self.rho_m_coupled = True

    if interface is not None:
        self.interface = interface

interface(new_interface)

Set the interface; runs generate_bindings and, for a magnetic layer, enables magnetism on the calculator (raising if it does not support it).

Source code in src/easyreflectometry/sample/elements/layers/layer.py
115
116
117
118
119
120
121
122
@BaseCore.interface.setter
def interface(self, new_interface) -> None:
    """Set the interface; runs `generate_bindings` and, for a magnetic layer,
    enables magnetism on the calculator (raising if it does not support it).
    """
    BaseCore.interface.fset(self, new_interface)
    if new_interface is not None and self._magnetism is not None:
        self._enable_calculator_magnetism()

assign_material(material)

Assign a material to the layer interface.

Parameters:

Name Type Description Default
material Material

The material to assign to the layer.

required
Source code in src/easyreflectometry/sample/elements/layers/layer.py
258
259
260
261
262
263
264
265
266
267
268
269
270
def assign_material(self, material: Material) -> None:
    """Assign a material to the layer interface.

    Parameters
    ----------
    material : Material
        The material to assign to the layer.
    """
    coupled = self.rho_m_coupled
    self._material = material
    self._reconcile_rho_m_coupling(coupled)
    if self.interface is not None:
        self.interface().assign_material_to_layer(self.material.unique_name, self.unique_name)

unconstrain(parameter)

Remove any constraint from parameter.

Idempotent: calling it on an already-independent parameter is a no-op. The parameter keeps its current (last evaluated) value and becomes fittable again. Its bounds, unit, variance and fixed state are not restored to their pre-constraint values. Review and reset the bounds before fitting.

Parameters:

Name Type Description Default
parameter Parameter

Parameter to make independent again.

required
Source code in src/easyreflectometry/constraints.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def unconstrain(parameter: Parameter) -> None:
    """Remove any constraint from `parameter`.

    Idempotent: calling it on an already-independent parameter is a
    no-op. The parameter keeps its current (last evaluated) value and
    becomes fittable again. Its bounds, unit, variance and `fixed` state
    are not restored to their pre-constraint values. Review and reset
    the bounds before fitting.

    Parameters
    ----------
    parameter : Parameter
        Parameter to make independent again.
    """
    if not parameter.independent:
        parameter.make_independent()
    if hasattr(parameter, USER_CONSTRAINT_FLAG):
        delattr(parameter, USER_CONSTRAINT_FLAG)

get_as_parameter(name, value, default_dict, unique_name_prefix=None)

This function creates a parameter for the variable name.

A parameter has a value and metadata.

If the value already is a parameter, it is returned. If the value is a number, a parameter is created with this value and metadata from the dictionary. If the value is None, a parameter is created with the default value and metadata from the dictionary.

param value: The value to use for the parameter.  If None, the default value in the dictionary is used.
param name: The name of the parameter
param default_dict: Dictionary with entry for `name` containing the default value and metadata for the parameter
Source code in src/easyreflectometry/utils.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def get_as_parameter(
    name: str,
    value: Union[Parameter, Number, None],
    default_dict: dict,
    unique_name_prefix: Optional[str] = None,
) -> Parameter:
    """This function creates a parameter for the variable `name`.

        A parameter has a value and metadata.
    If the value already is a parameter, it is returned.
        If the value is a number, a parameter is created with this value and metadata from the dictionary.
        If the value is None, a parameter is created with the default value and metadata from the dictionary.

        param value: The value to use for the parameter.  If None, the default value in the dictionary is used.
        param name: The name of the parameter
        param default_dict: Dictionary with entry for `name` containing the default value and metadata for the parameter
    """
    # This is a parameter, return it
    if isinstance(value, Parameter):
        return value

    # Ensure we got the dictionary for the parameter with the given name
    # Should leave the passed dictionary unchanged
    if name not in default_dict:
        parameter_dict = deepcopy(default_dict)
    else:
        parameter_dict = deepcopy(default_dict[name])

    # Add specific unique name prefix if requested
    if unique_name_prefix is not None:
        parameter_dict['unique_name'] = global_object.generate_unique_name(unique_name_prefix + 'Parameter')

    if value is None:
        # Create a default parameter using both value and metadata from dictionary
        return Parameter(name, **parameter_dict)
    elif isinstance(value, Number):
        # Create a parameter using provided value and metadata from dictionary
        del parameter_dict['value']
        return Parameter(name, value, **parameter_dict)

    raise ValueError(f'{name} must be a Parameter, a number, or None.')

_release(rho_m)

Make a coupled magnetic SLD an ordinary parameter again, keeping its value.

The dependency cleared fixed and narrowed the bounds; give back what a new LayerMagnetism starts with (fixed, project default limits), so uncoupling never silently adds a free parameter to the next fit.

Source code in src/easyreflectometry/sample/elements/layers/layer.py
288
289
290
291
292
293
294
295
296
297
298
def _release(rho_m: Parameter) -> None:
    """Make a coupled magnetic SLD an ordinary parameter again, keeping its value.

    The dependency cleared `fixed` and narrowed the bounds; give back what a new
    `LayerMagnetism` starts with (fixed, project default limits), so uncoupling
    never silently adds a free parameter to the next fit.
    """
    rho_m.make_independent()
    rho_m.fixed = True
    rho_m.min, rho_m.max = -np.inf, np.inf
    rho_m.default_limits_pending = True