Skip to content

model

DEFAULTS = {'scale': {'description': 'Scaling of the reflectomety profile', 'url': 'https://github.com/reflectivity/edu_outreach/blob/master/refl_maths/paper.tex', 'value': 1.0, 'min': 0, 'max': np.inf, 'fixed': True}, 'background': {'description': 'Linear background to include in reflectometry data', 'url': 'https://github.com/reflectivity/edu_outreach/blob/master/refl_maths/paper.tex', 'value': 1e-08, 'min': 0.0, 'max': np.inf, 'fixed': True}, 'resolution': {'value': 5.0}} module-attribute

COLORS = ['#0173B2', '#DE8F05', '#029E73', '#D55E00', '#CC78BC', '#CA9161', '#FBAFE4', '#949494', '#ECE133', '#56B4E9'] module-attribute

BaseAssembly

Bases: BaseCore

Assembly of layers.

The front layer (front_layer) is the layer the neutron beam starts in, it has an index of 0. The back layer (back_layer) is the final layer from which the unreflected neutron beam is transmitted, its index number depends on the number of finite layers in the system, but it might be accessed at index -1.

Source code in src/easyreflectometry/sample/assemblies/base_assembly.py
 11
 12
 13
 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
 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
class BaseAssembly(BaseCore):
    """Assembly of layers.

    The front layer (front_layer) is the layer the neutron beam starts in, it has an index of 0.
    The back layer (back_layer) is the final layer from which the unreflected neutron beam is transmitted,
    its index number depends on the number of finite layers in the system, but it might be accessed at index -1.
    """

    def __init__(
        self,
        name: str,
        type: str,
        interface,
        layers: LayerCollection,
        unique_name: Optional[str] = None,
    ):
        super().__init__(name=name, unique_name=unique_name)
        self._layers = layers

        # Type is needed when fitting in easyscience
        self._type = type
        self._roughness_constraints_setup = False
        self._thickness_constraints_setup = False

        if interface is not None:
            self.interface = interface

    @property
    def layers(self) -> LayerCollection:
        return self._layers

    @layers.setter
    def layers(self, value: LayerCollection) -> None:
        self._layers = value

    @property
    def type(self) -> str:
        """Get type of the assembly.

        Needed by the GUI.
        """
        return self._type

    @property
    def front_layer(self) -> Optional[Layer]:
        """Get the front layer in the assembly."""
        if len(self.layers) == 0:
            return None
        return self.layers[0]

    @front_layer.setter
    def front_layer(self, layer: Layer) -> None:
        """Set the front layer in the assembly.

        Parameters
        ----------
        layer : Layer
            Layer to set as the front layer.
        """
        if len(self.layers) == 0:
            self.layers.append(layer)
        else:
            self.layers[0] = layer

    @property
    def back_layer(self) -> Optional[Layer]:
        """Get the back layer in the assembly."""

        if len(self.layers) < 2:
            return None
        return self.layers[-1]

    @back_layer.setter
    def back_layer(self, layer: Layer) -> None:
        """Set the back layer in the assembly.

        Parameters
        ----------
        layer : Layer
            Layer to set as the back layer.
        """

        if len(self.layers) == 0:
            raise Exception('There is no front layer to add the back layer to. Please add a front layer first.')
        if len(self.layers) == 1:
            self.layers.append(layer)
        else:
            self.layers[-1] = layer

    def _setup_thickness_constraints(self) -> None:
        """Setup thickness constraint, front layer is the deciding layer."""
        independent_param = self.front_layer.thickness
        for i in range(1, len(self.layers)):
            self.layers[i].thickness.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_param})
        self._thickness_constraints_setup = True

    def _enable_thickness_constraints(self):
        """Enable the thickness constraint."""
        if self._thickness_constraints_setup:
            # Make sure that the thickness constraint is enabled
            self._setup_thickness_constraints()
            # Make sure that the thickness parameter is enabled
        else:
            raise Exception('Thickness constraints not setup')

    def _disable_thickness_constraints(self):
        """Disable the thickness constraint."""
        if self._thickness_constraints_setup:
            for i in range(1, len(self.layers)):
                self.layers[i].thickness.make_independent()
        else:
            raise Exception('Thickness constraints not setup')

    def _setup_roughness_constraints(self) -> None:
        """Setup roughness constraint, front layer is the deciding layer."""
        independent_parameter = self.front_layer.roughness
        for i in range(1, len(self.layers)):
            self.layers[i].roughness.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_parameter})
        self._roughness_constraints_setup = True

    def _enable_roughness_constraints(self):
        """Enable the roughness constraint."""
        independent_parameter = self.front_layer.roughness
        for i in range(1, len(self.layers)):
            self.layers[i].roughness.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_parameter})

    def _disable_roughness_constraints(self):
        """Disable the roughness constraint."""
        for i in range(1, len(self.layers)):
            self.layers[i].roughness.make_independent()

type property

Get type of the assembly.

Needed by the GUI.

front_layer property writable

Get the front layer in the assembly.

back_layer property writable

Get the back layer in the assembly.

Sample

Bases: BaseCollection

A sample is a collection of assemblies that represent the structure for which experimental measurements exist.

Source code in src/easyreflectometry/sample/collections/sample.py
 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
class Sample(BaseCollection):
    """A sample is a collection of assemblies that represent the structure for which experimental measurements exist."""

    def __init__(
        self,
        *assemblies: Optional[List[BaseAssembly]],
        name: str = 'EasySample',
        interface=None,
        unique_name: Optional[str] = None,
        populate_if_none: bool = True,
        **kwargs,
    ):
        """Constructor.

        Parameters
        ----------
        **kwargs :
        populate_if_none : bool, optional
            By default, True.
        unique_name : Optional[str], optional
            By default, None.
        *assemblies : Optional[List[BaseAssembly]]
        args :
            The assemblies in the sample.
        name : str, optional
            Name of the sample. By default, 'EasySample'.
        interface :
            Calculator interface. By default, None.
        """
        # `from_dict` (via `EasyList.from_dict`) passes the items as a single
        # list-positional arg; unpack that so validation and super() agree.
        if len(assemblies) == 1 and isinstance(assemblies[0], list):
            assemblies = tuple(assemblies[0])

        if not assemblies:
            if populate_if_none:
                assemblies = DEFAULT_ELEMENTS(interface)
            else:
                assemblies = []

        for assembly in assemblies:
            if not issubclass(type(assembly), BaseAssembly):
                raise ValueError('The elements must be an Assembly.')
        super().__init__(
            name,
            interface,
            *assemblies,
            unique_name=unique_name,
            populate_if_none=populate_if_none,
            **kwargs,
        )

    def add_assembly(self, assembly: Optional[BaseAssembly] = None):
        """Add an assembly to the sample.

        Parameters
        ----------
        assembly : Optional[BaseAssembly], optional
            Assembly to add. By default, None.
        """
        if assembly is None:
            assembly = Multilayer(
                name='EasyMultilayer added',
                interface=self.interface,
            )
        self.append(assembly)

    def duplicate_assembly(self, index: int):
        """Add an assembly to the sample.

        Parameters
        ----------
        index : int
        assembly :
            Assembly to add.
        """
        # Order matters: RepeatingMultilayer and SurfactantLayer are subclasses of
        # BaseAssembly but not Multilayer; however a RepeatingMultilayer IS a
        # Multilayer, so the most-specific check must come first to avoid
        # serialising it through the wrong `from_dict`.
        to_be_duplicated = self[index]
        if isinstance(to_be_duplicated, RepeatingMultilayer):
            duplicate = RepeatingMultilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
        elif isinstance(to_be_duplicated, SurfactantLayer):
            duplicate = SurfactantLayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
        elif isinstance(to_be_duplicated, Multilayer):
            duplicate = Multilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
        else:
            raise TypeError(f'Cannot duplicate assembly of type {type(to_be_duplicated).__name__}')
        duplicate.name = duplicate.name + ' duplicate'
        self.append(duplicate)

    def move_up(self, index: int):
        """Move the assembly at the given index up in the sample.

        Parameters
        ----------
        index : int
            Index of the assembly to move up.
        """
        super().move_up(index)

    def move_down(self, index: int):
        """Move the assembly at the given index down in the sample.

        Parameters
        ----------
        index : int
            Index of the assembly to move down.
        """
        super().move_down(index)

    def remove_assembly(self, index: int):
        """Remove the assembly at the given index from the sample.

        Parameters
        ----------
        index : int
            Index of the assembly to remove.
        """
        self.pop(index)

    @property
    def superphase(self) -> Layer:
        """The superphase of the sample."""
        return self[0].front_layer

    @property
    def subphase(self) -> Layer:
        """The subphase of the sample."""
        # This assembly only got one layer
        if self[-1].back_layer is None:
            return self[-1].front_layer
        else:
            return self[-1].back_layer

superphase property

The superphase of the sample.

subphase property

The subphase of the sample.

__init__(*assemblies, name='EasySample', interface=None, unique_name=None, populate_if_none=True, **kwargs)

Constructor.

Parameters:

Name Type Description Default
**kwargs
{}
populate_if_none bool

By default, True.

True
unique_name Optional[str]

By default, None.

None
*assemblies Optional[List[BaseAssembly]]
()
args

The assemblies in the sample.

required
name str

Name of the sample. By default, 'EasySample'.

'EasySample'
interface

Calculator interface. By default, None.

None
Source code in src/easyreflectometry/sample/collections/sample.py
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
def __init__(
    self,
    *assemblies: Optional[List[BaseAssembly]],
    name: str = 'EasySample',
    interface=None,
    unique_name: Optional[str] = None,
    populate_if_none: bool = True,
    **kwargs,
):
    """Constructor.

    Parameters
    ----------
    **kwargs :
    populate_if_none : bool, optional
        By default, True.
    unique_name : Optional[str], optional
        By default, None.
    *assemblies : Optional[List[BaseAssembly]]
    args :
        The assemblies in the sample.
    name : str, optional
        Name of the sample. By default, 'EasySample'.
    interface :
        Calculator interface. By default, None.
    """
    # `from_dict` (via `EasyList.from_dict`) passes the items as a single
    # list-positional arg; unpack that so validation and super() agree.
    if len(assemblies) == 1 and isinstance(assemblies[0], list):
        assemblies = tuple(assemblies[0])

    if not assemblies:
        if populate_if_none:
            assemblies = DEFAULT_ELEMENTS(interface)
        else:
            assemblies = []

    for assembly in assemblies:
        if not issubclass(type(assembly), BaseAssembly):
            raise ValueError('The elements must be an Assembly.')
    super().__init__(
        name,
        interface,
        *assemblies,
        unique_name=unique_name,
        populate_if_none=populate_if_none,
        **kwargs,
    )

add_assembly(assembly=None)

Add an assembly to the sample.

Parameters:

Name Type Description Default
assembly Optional[BaseAssembly]

Assembly to add. By default, None.

None
Source code in src/easyreflectometry/sample/collections/sample.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def add_assembly(self, assembly: Optional[BaseAssembly] = None):
    """Add an assembly to the sample.

    Parameters
    ----------
    assembly : Optional[BaseAssembly], optional
        Assembly to add. By default, None.
    """
    if assembly is None:
        assembly = Multilayer(
            name='EasyMultilayer added',
            interface=self.interface,
        )
    self.append(assembly)

duplicate_assembly(index)

Add an assembly to the sample.

Parameters:

Name Type Description Default
index int
required
assembly

Assembly to add.

required
Source code in src/easyreflectometry/sample/collections/sample.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def duplicate_assembly(self, index: int):
    """Add an assembly to the sample.

    Parameters
    ----------
    index : int
    assembly :
        Assembly to add.
    """
    # Order matters: RepeatingMultilayer and SurfactantLayer are subclasses of
    # BaseAssembly but not Multilayer; however a RepeatingMultilayer IS a
    # Multilayer, so the most-specific check must come first to avoid
    # serialising it through the wrong `from_dict`.
    to_be_duplicated = self[index]
    if isinstance(to_be_duplicated, RepeatingMultilayer):
        duplicate = RepeatingMultilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
    elif isinstance(to_be_duplicated, SurfactantLayer):
        duplicate = SurfactantLayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
    elif isinstance(to_be_duplicated, Multilayer):
        duplicate = Multilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
    else:
        raise TypeError(f'Cannot duplicate assembly of type {type(to_be_duplicated).__name__}')
    duplicate.name = duplicate.name + ' duplicate'
    self.append(duplicate)

move_up(index)

Move the assembly at the given index up in the sample.

Parameters:

Name Type Description Default
index int

Index of the assembly to move up.

required
Source code in src/easyreflectometry/sample/collections/sample.py
118
119
120
121
122
123
124
125
126
def move_up(self, index: int):
    """Move the assembly at the given index up in the sample.

    Parameters
    ----------
    index : int
        Index of the assembly to move up.
    """
    super().move_up(index)

move_down(index)

Move the assembly at the given index down in the sample.

Parameters:

Name Type Description Default
index int

Index of the assembly to move down.

required
Source code in src/easyreflectometry/sample/collections/sample.py
128
129
130
131
132
133
134
135
136
def move_down(self, index: int):
    """Move the assembly at the given index down in the sample.

    Parameters
    ----------
    index : int
        Index of the assembly to move down.
    """
    super().move_down(index)

remove_assembly(index)

Remove the assembly at the given index from the sample.

Parameters:

Name Type Description Default
index int

Index of the assembly to remove.

required
Source code in src/easyreflectometry/sample/collections/sample.py
138
139
140
141
142
143
144
145
146
def remove_assembly(self, index: int):
    """Remove the assembly at the given index from the sample.

    Parameters
    ----------
    index : int
        Index of the assembly to remove.
    """
    self.pop(index)

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

    # ----- 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
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
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
162
163
164
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
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
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
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
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
228
229
230
def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]:
    """Compatibility alias for :meth:`to_dict`."""
    return self.to_dict(skip=skip)

__repr__()

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

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

PercentageFwhm

Bases: ResolutionFunction

Source code in src/easyreflectometry/model/resolution_functions.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class PercentageFwhm(ResolutionFunction):
    def __init__(self, constant: Union[None, float] = None):
        """Init function."""
        if constant is None:
            constant = DEFAULT_RESOLUTION_FWHM_PERCENTAGE
        self.constant = constant

    def smearing(self, q: Union[np.array, float]) -> np.array:
        """Return per-point sigma values from the constant FWHM percentage.

        ``constant`` is a FWHM percentage of ``q``; it is converted to an
        absolute sigma so the smearing() contract is sigma for all types.
        """
        q_array = np.asarray(q, dtype=float)
        fwhm = (self.constant / 100.0) * q_array
        return fwhm / SIGMA_TO_FWHM

    def as_dict(
        self, skip: Optional[List[str]] = None
    ) -> dict[str, str]:  # skip is kept for consistency of the as_dict signature
        """As dict."""
        return {'smearing': 'PercentageFwhm', 'constant': self.constant}

__init__(constant=None)

Init function.

Source code in src/easyreflectometry/model/resolution_functions.py
60
61
62
63
64
def __init__(self, constant: Union[None, float] = None):
    """Init function."""
    if constant is None:
        constant = DEFAULT_RESOLUTION_FWHM_PERCENTAGE
    self.constant = constant

smearing(q)

Return per-point sigma values from the constant FWHM percentage.

constant is a FWHM percentage of q; it is converted to an absolute sigma so the smearing() contract is sigma for all types.

Source code in src/easyreflectometry/model/resolution_functions.py
66
67
68
69
70
71
72
73
74
def smearing(self, q: Union[np.array, float]) -> np.array:
    """Return per-point sigma values from the constant FWHM percentage.

    ``constant`` is a FWHM percentage of ``q``; it is converted to an
    absolute sigma so the smearing() contract is sigma for all types.
    """
    q_array = np.asarray(q, dtype=float)
    fwhm = (self.constant / 100.0) * q_array
    return fwhm / SIGMA_TO_FWHM

as_dict(skip=None)

As dict.

Source code in src/easyreflectometry/model/resolution_functions.py
76
77
78
79
80
def as_dict(
    self, skip: Optional[List[str]] = None
) -> dict[str, str]:  # skip is kept for consistency of the as_dict signature
    """As dict."""
    return {'smearing': 'PercentageFwhm', 'constant': self.constant}

ResolutionFunction

Source code in src/easyreflectometry/model/resolution_functions.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class ResolutionFunction:
    @abstractmethod
    def smearing(self, q: Union[np.array, float]) -> np.array:
        """Return the resolution as sigma (standard deviation) at each ``q``."""
        ...

    @abstractmethod
    def as_dict(self, skip: Optional[List[str]] = None) -> dict: ...

    @classmethod
    def from_dict(cls, data: dict) -> ResolutionFunction:
        """Smearing function."""
        if data['smearing'] == 'PercentageFwhm':
            return PercentageFwhm(data['constant'])
        if data['smearing'] == 'LinearSpline':
            return LinearSpline(data['q_data_points'], data['fwhm_values'])
        if data['smearing'] == 'Pointwise':
            return Pointwise([
                data['q_data_points'],
                data['R_data_points'],
                data['sQz_data_points'],
            ])
        raise ValueError('Unknown resolution function type')

smearing(q) abstractmethod

Return the resolution as sigma (standard deviation) at each q.

Source code in src/easyreflectometry/model/resolution_functions.py
35
36
37
38
@abstractmethod
def smearing(self, q: Union[np.array, float]) -> np.array:
    """Return the resolution as sigma (standard deviation) at each ``q``."""
    ...

from_dict(data) classmethod

Smearing function.

Source code in src/easyreflectometry/model/resolution_functions.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@classmethod
def from_dict(cls, data: dict) -> ResolutionFunction:
    """Smearing function."""
    if data['smearing'] == 'PercentageFwhm':
        return PercentageFwhm(data['constant'])
    if data['smearing'] == 'LinearSpline':
        return LinearSpline(data['q_data_points'], data['fwhm_values'])
    if data['smearing'] == 'Pointwise':
        return Pointwise([
            data['q_data_points'],
            data['R_data_points'],
            data['sQz_data_points'],
        ])
    raise ValueError('Unknown resolution function type')

Model

Bases: BaseCore

Model is the class that represents the experiment.

It is used to store the information about the experiment and to perform the calculations.

Source code in src/easyreflectometry/model/model.py
 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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
class Model(BaseCore):
    """Model is the class that represents the experiment.

    It is used to store the information about the experiment and to perform the calculations.
    """

    def __init__(
        self,
        sample: Union[Sample, None] = None,
        scale: Union[Parameter, Number, None] = None,
        background: Union[Parameter, Number, None] = None,
        resolution_function: Union[ResolutionFunction, None] = None,
        name: str = 'Model',
        color: str = COLORS[0],
        unique_name: Optional[str] = None,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        color : str, optional
            By default, COLORS[0].
        sample : Union[Sample, None], optional
            The sample being modelled. By default, None.
        scale : Union[Parameter, Number, None], optional
            Scaling factor of profile. By default, None.
        background : Union[Parameter, Number, None], optional
            Linear background magnitude. By default, None.
        name : str, optional
            Name of the model. By default, 'Model'.
        resolution_function : Union[ResolutionFunction, None], optional
            Resolution function. By default, None.
        interface :
            Calculator interface. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        if sample is None:
            sample = Sample(interface=interface)
        if resolution_function is None:
            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
        self._resolution_function = resolution_function

        super().__init__(name=name, unique_name=unique_name)
        self._sample = sample
        self._scale = scale
        self._background = background

        # Set interface last — propagates to children via BaseCore.generate_bindings
        # and then sets the resolution function on the calculator (see setter).
        if interface is not None:
            self.interface = interface

    # ----- @property accessors for serialization round-trip -----

    @property
    def sample(self) -> Sample:
        return self._sample

    @sample.setter
    def sample(self, value: Sample) -> None:
        self._sample = value

    @property
    def scale(self) -> Parameter:
        return self._scale

    @scale.setter
    def scale(self, value: float) -> None:
        self._scale.value = value

    @property
    def background(self) -> Parameter:
        return self._background

    @background.setter
    def background(self, value: float) -> None:
        self._background.value = value

    # ----- assembly management -----

    def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None:
        """Add assemblies to the model sample.

        Parameters
        ----------
        *assemblies : list[BaseAssembly]
            Assemblies to add to model sample.
        """
        if not assemblies:
            self.sample.add_assembly()
            if self.interface is not None:
                self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name)
        else:
            for assembly in assemblies:
                if issubclass(assembly.__class__, BaseAssembly):
                    self.sample.add_assembly(assembly)
                    if self.interface is not None:
                        self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name)
                else:
                    raise ValueError(f'Object {assembly} is not a valid type, must be a child of BaseAssembly.')

    def duplicate_assembly(self, index: int) -> None:
        """Duplicate a given item or layer in a sample.

        Parameters
        ----------
        index : int
        idx :
            Index of the item or layer to duplicate.
        """
        self.sample.duplicate_assembly(index)
        if self.interface is not None:
            self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name)

    def remove_assembly(self, index: int) -> None:
        """Remove an assembly from the model.

        Parameters
        ----------
        index : int
        idx :
            Index of the item to remove.
        """
        assembly_unique_name = self.sample[index].unique_name
        self.sample.remove_assembly(index)
        if self.interface is not None:
            self.interface().remove_item_from_model(assembly_unique_name, self.unique_name)

    @property
    def is_default(self) -> bool:
        """Whether this model was created as a default placeholder."""
        return self._is_default

    @is_default.setter
    def is_default(self, value: bool) -> None:
        """Set whether this model is a default placeholder."""
        self._is_default = value

    # ----- resolution function -----

    @property
    def resolution_function(self) -> ResolutionFunction:
        """Return the resolution function."""
        return self._resolution_function

    @resolution_function.setter
    def resolution_function(self, resolution_function: ResolutionFunction) -> None:
        """Set the resolution function for the model."""
        self._resolution_function = resolution_function
        if self.interface is not None:
            self.interface().set_resolution_function(self._resolution_function)

    # ----- interface (override BaseCore's to add resolution-function side effect) -----

    @BaseCore.interface.setter
    def interface(self, new_interface) -> None:
        """Set the interface; runs `generate_bindings` and then refreshes the
        calculator's resolution function.
        """
        # Call BaseCore.interface.setter for the binding propagation.
        BaseCore.interface.fset(self, new_interface)
        if new_interface is not None:
            new_interface().set_resolution_function(self._resolution_function)

    # ----- representation -----

    @property
    def _dict_repr(self) -> dict[str, dict[str, str]]:
        """A simplified dict representation."""
        if isinstance(self._resolution_function, PercentageFwhm):
            resolution_value = self._resolution_function.as_dict()['constant']
            resolution = f'{resolution_value} %'
        else:
            resolution = 'function of Q'

        return {
            self.name: {
                'scale': float(self.scale.value),
                'background': float(self.background.value),
                'resolution': resolution,
                'color': self.color,
                'sample': self.sample._dict_repr,
            }
        }

    # ----- serialization (custom because resolution_function + interface need special handling) -----

    def to_dict(self, skip: Optional[list[str]] = None) -> dict:
        """Serialize the model, encoding the resolution function and interface name."""
        if skip is None:
            skip = []
        # Sample/resolution_function/interface get bespoke encoding below.
        skip_for_super = list(skip) + ['sample', 'resolution_function', 'interface']
        this_dict = super().to_dict(skip=skip_for_super)
        this_dict['sample'] = self.sample.as_dict(skip=skip)
        this_dict['resolution_function'] = self.resolution_function.as_dict(skip=skip)
        if self.interface is None:
            this_dict['interface'] = None
        else:
            this_dict['interface'] = self.interface().name
        return this_dict

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

    def as_orso(self) -> dict:
        """Convert the model to a dictionary suitable for ORSO."""
        return self.as_dict()

    @classmethod
    def from_dict(cls, passed_dict: dict) -> Model:
        """Create a Model from a dictionary."""
        # Circular import if hoisted to module-top.
        from easyreflectometry.calculators import CalculatorFactory

        this_dict = copy.deepcopy(passed_dict)
        resolution_function = ResolutionFunction.from_dict(this_dict.pop('resolution_function'))
        interface_name = this_dict.pop('interface')
        if interface_name is not None:
            interface = CalculatorFactory()
            interface.switch(interface_name)
        else:
            interface = None

        model = super().from_dict(this_dict)

        model.resolution_function = resolution_function
        model.interface = interface
        return model

is_default property writable

Whether this model was created as a default placeholder.

resolution_function property writable

Return the resolution function.

__init__(sample=None, scale=None, background=None, resolution_function=None, name='Model', color=COLORS[0], unique_name=None, interface=None)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
color str

By default, COLORS[0].

COLORS[0]
sample Union[Sample, None]

The sample being modelled. By default, None.

None
scale Union[Parameter, Number, None]

Scaling factor of profile. By default, None.

None
background Union[Parameter, Number, None]

Linear background magnitude. By default, None.

None
name str

Name of the model. By default, 'Model'.

'Model'
resolution_function Union[ResolutionFunction, None]

Resolution function. By default, None.

None
interface

Calculator interface. By default, None.

None
Source code in src/easyreflectometry/model/model.py
 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
def __init__(
    self,
    sample: Union[Sample, None] = None,
    scale: Union[Parameter, Number, None] = None,
    background: Union[Parameter, Number, None] = None,
    resolution_function: Union[ResolutionFunction, None] = None,
    name: str = 'Model',
    color: str = COLORS[0],
    unique_name: Optional[str] = None,
    interface=None,
):
    """Constructor.

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    color : str, optional
        By default, COLORS[0].
    sample : Union[Sample, None], optional
        The sample being modelled. By default, None.
    scale : Union[Parameter, Number, None], optional
        Scaling factor of profile. By default, None.
    background : Union[Parameter, Number, None], optional
        Linear background magnitude. By default, None.
    name : str, optional
        Name of the model. By default, 'Model'.
    resolution_function : Union[ResolutionFunction, None], optional
        Resolution function. By default, None.
    interface :
        Calculator interface. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    if sample is None:
        sample = Sample(interface=interface)
    if resolution_function is None:
        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
    self._resolution_function = resolution_function

    super().__init__(name=name, unique_name=unique_name)
    self._sample = sample
    self._scale = scale
    self._background = background

    # Set interface last — propagates to children via BaseCore.generate_bindings
    # and then sets the resolution function on the calculator (see setter).
    if interface is not None:
        self.interface = interface

add_assemblies(*assemblies)

Add assemblies to the model sample.

Parameters:

Name Type Description Default
*assemblies list[BaseAssembly]

Assemblies to add to model sample.

()
Source code in src/easyreflectometry/model/model.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None:
    """Add assemblies to the model sample.

    Parameters
    ----------
    *assemblies : list[BaseAssembly]
        Assemblies to add to model sample.
    """
    if not assemblies:
        self.sample.add_assembly()
        if self.interface is not None:
            self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name)
    else:
        for assembly in assemblies:
            if issubclass(assembly.__class__, BaseAssembly):
                self.sample.add_assembly(assembly)
                if self.interface is not None:
                    self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name)
            else:
                raise ValueError(f'Object {assembly} is not a valid type, must be a child of BaseAssembly.')

duplicate_assembly(index)

Duplicate a given item or layer in a sample.

Parameters:

Name Type Description Default
index int
required
idx

Index of the item or layer to duplicate.

required
Source code in src/easyreflectometry/model/model.py
172
173
174
175
176
177
178
179
180
181
182
183
def duplicate_assembly(self, index: int) -> None:
    """Duplicate a given item or layer in a sample.

    Parameters
    ----------
    index : int
    idx :
        Index of the item or layer to duplicate.
    """
    self.sample.duplicate_assembly(index)
    if self.interface is not None:
        self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name)

remove_assembly(index)

Remove an assembly from the model.

Parameters:

Name Type Description Default
index int
required
idx

Index of the item to remove.

required
Source code in src/easyreflectometry/model/model.py
185
186
187
188
189
190
191
192
193
194
195
196
197
def remove_assembly(self, index: int) -> None:
    """Remove an assembly from the model.

    Parameters
    ----------
    index : int
    idx :
        Index of the item to remove.
    """
    assembly_unique_name = self.sample[index].unique_name
    self.sample.remove_assembly(index)
    if self.interface is not None:
        self.interface().remove_item_from_model(assembly_unique_name, self.unique_name)

interface(new_interface)

Set the interface; runs generate_bindings and then refreshes the calculator's resolution function.

Source code in src/easyreflectometry/model/model.py
225
226
227
228
229
230
231
232
233
@BaseCore.interface.setter
def interface(self, new_interface) -> None:
    """Set the interface; runs `generate_bindings` and then refreshes the
    calculator's resolution function.
    """
    # Call BaseCore.interface.setter for the binding propagation.
    BaseCore.interface.fset(self, new_interface)
    if new_interface is not None:
        new_interface().set_resolution_function(self._resolution_function)

to_dict(skip=None)

Serialize the model, encoding the resolution function and interface name.

Source code in src/easyreflectometry/model/model.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def to_dict(self, skip: Optional[list[str]] = None) -> dict:
    """Serialize the model, encoding the resolution function and interface name."""
    if skip is None:
        skip = []
    # Sample/resolution_function/interface get bespoke encoding below.
    skip_for_super = list(skip) + ['sample', 'resolution_function', 'interface']
    this_dict = super().to_dict(skip=skip_for_super)
    this_dict['sample'] = self.sample.as_dict(skip=skip)
    this_dict['resolution_function'] = self.resolution_function.as_dict(skip=skip)
    if self.interface is None:
        this_dict['interface'] = None
    else:
        this_dict['interface'] = self.interface().name
    return this_dict

as_dict(skip=None)

Compatibility alias for :meth:to_dict.

Source code in src/easyreflectometry/model/model.py
273
274
275
def as_dict(self, skip: Optional[list[str]] = None) -> dict:
    """Compatibility alias for :meth:`to_dict`."""
    return self.to_dict(skip=skip)

as_orso()

Convert the model to a dictionary suitable for ORSO.

Source code in src/easyreflectometry/model/model.py
277
278
279
def as_orso(self) -> dict:
    """Convert the model to a dictionary suitable for ORSO."""
    return self.as_dict()

from_dict(passed_dict) classmethod

Create a Model from a dictionary.

Source code in src/easyreflectometry/model/model.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
@classmethod
def from_dict(cls, passed_dict: dict) -> Model:
    """Create a Model from a dictionary."""
    # Circular import if hoisted to module-top.
    from easyreflectometry.calculators import CalculatorFactory

    this_dict = copy.deepcopy(passed_dict)
    resolution_function = ResolutionFunction.from_dict(this_dict.pop('resolution_function'))
    interface_name = this_dict.pop('interface')
    if interface_name is not None:
        interface = CalculatorFactory()
        interface.switch(interface_name)
    else:
        interface = None

    model = super().from_dict(this_dict)

    model.resolution_function = resolution_function
    model.interface = interface
    return model

apply_default_limits(parameter, kind)

Apply default min/max to a parameter if current bounds are infinite.

Parameters:

Name Type Description Default
parameter Parameter

The parameter to adjust.

required
kind str

One of 'thickness', 'roughness', 'sld', 'isld', 'scale'.

required
Source code in src/easyreflectometry/limits.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def apply_default_limits(parameter: Parameter, kind: str) -> None:
    """Apply default min/max to a parameter if current bounds are infinite.

    Parameters
    ----------
    parameter : Parameter
        The parameter to adjust.
    kind : str
        One of 'thickness', 'roughness', 'sld', 'isld', 'scale'.
    """
    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)

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