Skip to content

Calculators

The calculator translates an EasyReflectometry model into a backend engine (refl1d or refnx) and computes reflectivity.

Polarized reflectivity

With the refl1d calculator and include_magnetism enabled, all four spin channels are available:

  • polarized_reflectivity_profiles(x_array, model_id) returns the reflectivity of all four channels as a dictionary keyed 'pp', 'pm', 'mp', 'mm' (in that order).
  • polarization_channel selects which channel reflectity_profile — and hence fitting — uses (default 'pp').
  • magnetic_sld_profile(model_id) returns the nuclear and magnetic scattering length density profiles as a tuple z, sld(z), rhoM(z) (magnetic SLD) and thetaM(z) (magnetic angle).

Note that polarization_channel is state of the currently active calculator instance, not of a model or dataset: it affects every subsequent calculation using that calculator, and switching calculators via the factory constructs a fresh instance, which resets the channel (along with include_magnetism). Calculators without magnetism support (refnx) raise NotImplementedError when magnetism is enabled.

polarization

POLARIZATION_CHANNEL_TO_INDEX = {PolarizationChannel.PP: 3, PolarizationChannel.PM: 2, PolarizationChannel.MP: 1, PolarizationChannel.MM: 0} module-attribute

PolarizationChannel

Bases: str, Enum

Spin cross-section channels for polarized neutron reflectometry.

The accepted spellings are exactly the enum values ('pp', 'pm', 'mp', 'mm'); uppercase strings are rejected.

Source code in src/easyreflectometry/calculators/polarization.py
 7
 8
 9
10
11
12
13
14
15
16
17
class PolarizationChannel(str, Enum):
    """Spin cross-section channels for polarized neutron reflectometry.

    The accepted spellings are exactly the enum values ('pp', 'pm', 'mp', 'mm');
    uppercase strings are rejected.
    """

    PP = 'pp'  # non-spin-flip, up-up
    PM = 'pm'  # spin-flip, up-down
    MP = 'mp'  # spin-flip, down-up
    MM = 'mm'  # non-spin-flip, down-down

calculator_base

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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
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

        # Derived, read-only "calculation" parameter (see `total_thickness`).
        # Rebuilt by every constructor call — including `from_dict` — and
        # deliberately *not* serialized: it is a function of the layers.
        self._total_thickness = Parameter(
            name='total_thickness',
            value=0.0,
            unit='angstrom',
            fixed=True,
            description='Total thickness of the film: the sum of all layer thicknesses '
            'between the superphase and the subphase (derived, read-only).',
        )
        self._total_thickness_sources: list[Parameter] = []
        self.refresh_derived()

        # 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

    # ----- derived parameters -----

    def _film_layers(self) -> list:
        """Layers between the superphase and the subphase (first and last layer of the sample)."""
        layers = [layer for assembly in self.sample for layer in assembly.layers]
        return layers[1:-1] if len(layers) >= 3 else []

    def refresh_derived(self) -> None:
        """Re-derive the model's computed parameters from the current layer structure.

        Cheap (a few string operations) and idempotent: the dependency is
        only rebuilt when the set of contributing layers changed. Called on
        every access to :attr:`total_thickness`, so layers added or removed
        through *any* path (`Model.add_assemblies`, `assembly.layers.append`,
        ...) are picked up without explicit notification.
        """
        sources = [layer.thickness for layer in self._film_layers()]
        current = self._total_thickness_sources
        if len(sources) == len(current) and all(a is b for a, b in zip(sources, current)):
            return
        self._total_thickness_sources = sources
        total = self._total_thickness
        if not sources:
            if not total.independent:
                total.make_independent()
            # `make_dependent_on` clears `fixed`, and `make_independent` does not
            # put it back; without this the parameter would resurface as a free
            # fit parameter once the film is emptied.
            total.fixed = True
            total.value = 0.0
            return
        dependency_map = {f't{index}': source for index, source in enumerate(sources)}
        total.make_dependent_on(
            dependency_expression=' + '.join(dependency_map.keys()),
            dependency_map=dependency_map,
        )

    @property
    def total_thickness(self) -> Parameter:
        """Read-only parameter: the summed thickness of the film layers.

        The superphase (first layer of the sample) and the subphase (last
        layer) are semi-infinite and excluded. The parameter is *dependent*:
        it tracks the layer thicknesses, never enters a fit and cannot be
        set; it can be referenced from constraint expressions and
        inequality constraints like any other parameter.
        """
        self.refresh_derived()
        return self._total_thickness

    # ----- 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 has_magnetism(self) -> bool:
        """Whether any layer in the sample carries magnetic properties."""
        return any(getattr(layer, 'magnetism', None) is not None for assembly in self.sample for layer in assembly.layers)

    @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:
        """The sample as an ORSO simple-model (``sample.model``) dictionary.

        Slab representation: lengths in angstrom, SLDs in 1/angstrom^2,
        repeating multilayers via the inline ``N ( ... )`` stack syntax.

        Returns
        -------
        dict
            The ORSO model-language dictionary (the content of an .ort file's
            ``data_source.sample.model`` section).
        """
        # Circular import if hoisted to module-top.
        from orsopy.fileio import Header

        from easyreflectometry.orso_utils import sample_to_orso_model

        return Header.asdict(sample_to_orso_model(self.sample))

    @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

total_thickness property

Read-only parameter: the summed thickness of the film layers.

The superphase (first layer of the sample) and the subphase (last layer) are semi-infinite and excluded. The parameter is dependent: it tracks the layer thicknesses, never enters a fit and cannot be set; it can be referenced from constraint expressions and inequality constraints like any other parameter.

has_magnetism property

Whether any layer in the sample carries magnetic properties.

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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

    # Derived, read-only "calculation" parameter (see `total_thickness`).
    # Rebuilt by every constructor call — including `from_dict` — and
    # deliberately *not* serialized: it is a function of the layers.
    self._total_thickness = Parameter(
        name='total_thickness',
        value=0.0,
        unit='angstrom',
        fixed=True,
        description='Total thickness of the film: the sum of all layer thicknesses '
        'between the superphase and the subphase (derived, read-only).',
    )
    self._total_thickness_sources: list[Parameter] = []
    self.refresh_derived()

    # 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

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
294
295
296
297
298
299
300
301
302
@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)

refresh_derived()

Re-derive the model's computed parameters from the current layer structure.

Cheap (a few string operations) and idempotent: the dependency is only rebuilt when the set of contributing layers changed. Called on every access to :attr:total_thickness, so layers added or removed through any path (Model.add_assemblies, assembly.layers.append, ...) are picked up without explicit notification.

Source code in src/easyreflectometry/model/model.py
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
def refresh_derived(self) -> None:
    """Re-derive the model's computed parameters from the current layer structure.

    Cheap (a few string operations) and idempotent: the dependency is
    only rebuilt when the set of contributing layers changed. Called on
    every access to :attr:`total_thickness`, so layers added or removed
    through *any* path (`Model.add_assemblies`, `assembly.layers.append`,
    ...) are picked up without explicit notification.
    """
    sources = [layer.thickness for layer in self._film_layers()]
    current = self._total_thickness_sources
    if len(sources) == len(current) and all(a is b for a, b in zip(sources, current)):
        return
    self._total_thickness_sources = sources
    total = self._total_thickness
    if not sources:
        if not total.independent:
            total.make_independent()
        # `make_dependent_on` clears `fixed`, and `make_independent` does not
        # put it back; without this the parameter would resurface as a free
        # fit parameter once the film is emptied.
        total.fixed = True
        total.value = 0.0
        return
    dependency_map = {f't{index}': source for index, source in enumerate(sources)}
    total.make_dependent_on(
        dependency_expression=' + '.join(dependency_map.keys()),
        dependency_map=dependency_map,
    )

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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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
236
237
238
239
240
241
242
243
244
245
246
247
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
249
250
251
252
253
254
255
256
257
258
259
260
261
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)

to_dict(skip=None)

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

Source code in src/easyreflectometry/model/model.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
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
342
343
344
def as_dict(self, skip: Optional[list[str]] = None) -> dict:
    """Compatibility alias for :meth:`to_dict`."""
    return self.to_dict(skip=skip)

as_orso()

The sample as an ORSO simple-model (sample.model) dictionary.

Slab representation: lengths in angstrom, SLDs in 1/angstrom^2, repeating multilayers via the inline N ( ... ) stack syntax.

Returns:

Type Description
dict

The ORSO model-language dictionary (the content of an .ort file's data_source.sample.model section).

Source code in src/easyreflectometry/model/model.py
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def as_orso(self) -> dict:
    """The sample as an ORSO simple-model (``sample.model``) dictionary.

    Slab representation: lengths in angstrom, SLDs in 1/angstrom^2,
    repeating multilayers via the inline ``N ( ... )`` stack syntax.

    Returns
    -------
    dict
        The ORSO model-language dictionary (the content of an .ort file's
        ``data_source.sample.model`` section).
    """
    # Circular import if hoisted to module-top.
    from orsopy.fileio import Header

    from easyreflectometry.orso_utils import sample_to_orso_model

    return Header.asdict(sample_to_orso_model(self.sample))

from_dict(passed_dict) classmethod

Create a Model from a dictionary.

Source code in src/easyreflectometry/model/model.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
@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

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

    # ----- public constraint toggles -----

    def _layers_follow_front(self, attribute: str) -> bool:
        """True when every layer's `attribute` is tied *equal* to the front layer's.

        Only the conformal idiom counts (expression ``'a'`` over ``{'a': leader}``,
        as set up by ``_setup_*_constraints``); a parameter that merely *uses*
        the front layer's value in some other expression is not conformal.
        """
        if len(self.layers) < 2:
            return False
        leader = getattr(self.front_layer, attribute)
        for layer in list(self.layers)[1:]:
            follower = getattr(layer, attribute)
            if not follows_equal(follower, leader):
                return False
        return True

    @property
    def conformal_thickness(self) -> bool:
        """Whether every layer shares the front layer's thickness."""
        return self._layers_follow_front('thickness')

    @conformal_thickness.setter
    def conformal_thickness(self, status: bool) -> None:
        """Tie (or release) every layer's thickness to the front layer's."""
        if status:
            self._setup_thickness_constraints()
        elif self._layers_follow_front('thickness'):
            for layer in list(self.layers)[1:]:
                layer.thickness.make_independent()

    @property
    def conformal_roughness(self) -> bool:
        """Whether every layer shares the front layer's roughness."""
        return self._layers_follow_front('roughness')

    @conformal_roughness.setter
    def conformal_roughness(self, status: bool) -> None:
        """Tie (or release) every layer's roughness to the front layer's."""
        if status:
            self._setup_roughness_constraints()
        elif self._layers_follow_front('roughness'):
            self._disable_roughness_constraints()

    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.

conformal_thickness property writable

Whether every layer shares the front layer's thickness.

conformal_roughness property writable

Whether every layer shares the front layer's roughness.

Layer

Bases: BaseCore

Source code in src/easyreflectometry/sample/elements/layers/layer.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
 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
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,
    ):
        """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.
        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

        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:
        self._material = value

    @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).
        """
        if value is None and self._magnetism is not None:
            if self.interface is not None:
                self.interface().remove_layer_magnetism(self.unique_name)
            # Detach the calculator callbacks of the removed parameters so later
            # value changes on the detached object 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.
            self._magnetism.rho_m._callback = property()
            self._magnetism.theta_m._callback = property()
        self._magnetism = value
        if value is not None and self.interface is not None:
            self._enable_calculator_magnetism()
            self.generate_bindings()

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

        Parameters
        ----------
        material : Material
            The material to assign to the layer.
        """
        self._material = material
        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

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

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
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
 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
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,
):
    """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.
    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

    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
106
107
108
109
110
111
112
113
@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
175
176
177
178
179
180
181
182
183
184
185
def assign_material(self, material: Material) -> None:
    """Assign a material to the layer interface.

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

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

MaterialMixture

Bases: BaseCore

Source code in src/easyreflectometry/sample/elements/materials/material_mixture.py
 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
class MaterialMixture(BaseCore):
    def __init__(
        self,
        material_a: Union[Material, None] = None,
        material_b: Union[Material, None] = None,
        fraction: Union[Parameter, float, None] = None,
        name: Union[str, None] = None,
        unique_name: Optional[str] = None,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        material_a : Union[Material, None], optional
            The first material. By default, None.
        material_b : Union[Material, None], optional
            The second material. By default, None.
        fraction : Union[Parameter, float, None], optional
            The fraction of material_b in material_a. By default, None.
        name : Union[str, None], optional
            Name of the material. 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 material_a is None:
            material_a = Material(interface=interface)
        if material_b is None:
            material_b = Material(interface=interface)

        fraction = get_as_parameter(
            name='fraction',
            value=fraction,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Fraction',
        )

        sld_value = weighted_average(
            a=material_a.sld.value,
            b=material_b.sld.value,
            p=fraction.value,
        )
        isld_value = weighted_average(
            a=material_a.isld.value,
            b=material_b.isld.value,
            p=fraction.value,
        )

        sld = get_as_parameter(
            name='sld',
            value=sld_value,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Sld',
        )
        isld = get_as_parameter(
            name='isld',
            value=isld_value,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Isld',
        )

        # `name` may be None to signal "derive from material names"; resolve
        # before super().__init__ since BaseCore stores `_name` directly.
        if name is None:
            resolved_name = material_a.name + '/' + material_b.name
        else:
            resolved_name = name

        super().__init__(name=resolved_name, unique_name=unique_name)
        self._material_a = material_a
        self._material_b = material_b
        self._fraction = fraction
        self._sld = sld
        self._isld = isld

        self._materials_constraints()

        if interface is not None:
            self.interface = interface

    # ----- constructor-arg accessors -----

    @property
    def material_a(self) -> Material:
        return self._material_a

    @material_a.setter
    def material_a(self, new_material_a: Material) -> None:
        self._material_a = new_material_a
        self._materials_constraints()
        if self.interface is not None:
            self.interface.generate_bindings(self)
        self._update_name()

    @property
    def material_b(self) -> Material:
        return self._material_b

    @material_b.setter
    def material_b(self, new_material_b: Material) -> None:
        self._material_b = new_material_b
        self._materials_constraints()
        if self.interface is not None:
            self.interface.generate_bindings(self)
        self._update_name()

    @property
    def fraction(self) -> Parameter:
        """The Parameter that controls the mixing fraction of material_b in material_a."""
        return self._fraction

    @fraction.setter
    def fraction(self, value: float) -> None:
        if not isinstance(value, (int, float)):
            raise ValueError('fraction must be a float')
        self._fraction.value = value

    # ----- derived sld / isld parameters (shared shape with Material) -----
    #
    # These are *derived* via the constraints set up in `_materials_constraints`
    # (not constructor arguments) so we expose them as floats to match the
    # legacy MaterialMixture API. The underlying Parameter objects remain
    # available as `self._sld` / `self._isld`.

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

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

    # ----- calculator binding -----

    def _get_linkable_attributes(self):
        """Return the *mixed* sld / isld parameters for calculator binding.

        Override of the inherited `BaseCore._get_linkable_attributes`, which
        walks `get_all_variables()` and would otherwise expose the **child**
        materials' sld/isld (because our own `sld` / `isld` are floats, not
        Parameters). The calculator's `InterfaceFactoryTemplate.generate_bindings`
        matches by parameter `name`; without this override it binds to
        `material_a.sld` and reflectivity is computed off the wrong SLD.
        """
        return [self._sld, self._isld]

    # ----- internal helpers -----

    def _materials_constraints(self):
        """Wire the mixed `_sld` / `_isld` to depend on the current child
        material parameters and the current `_fraction`. Idempotent: callers
        invoke this once from ``__init__`` and again from ``from_dict`` after
        the saved Parameters have been reattached (so the dependency graph
        points at the right objects, not the temporary constructor params)."""
        # Detach any existing dependency before rebuilding so make_dependent_on
        # doesn't chain on top of stale references.
        for derived in (self._sld, self._isld):
            if not derived.independent:
                derived.make_independent()

        dependency_expression = 'a * (1 - p) + b * p'
        dependency_map = {
            'a': self._material_a.sld,
            'b': self._material_b.sld,
            'p': self._fraction,
        }
        self._sld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map)

        dependency_map = {
            'a': self._material_a.isld,
            'b': self._material_b.isld,
            'p': self._fraction,
        }
        self._isld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map)

    def _update_name(self) -> None:
        """Update name."""
        self.name = self._material_a.name + '/' + self._material_b.name

    # ----- deserialization -----

    @classmethod
    def from_dict(cls, obj_dict: dict) -> 'MaterialMixture':
        """Re-attach mixed-sld dependencies after :class:`ModelBase` swaps in
        the saved ``_fraction`` Parameter.

        :class:`ModelBase.from_dict` runs ``__init__`` (which builds the
        ``_sld`` / ``_isld`` constraints against the *temporary* ``_fraction``
        created from the float kwargs) and then re-points ``self._fraction``
        at the persisted Parameter. The constraint graph still references the
        temporary object, so subsequent ``mm.fraction = X`` mutations don't
        propagate to ``_sld`` / ``_isld``. Re-running ``_materials_constraints``
        here points the graph at the live objects.
        """
        instance = super().from_dict(obj_dict)
        instance._materials_constraints()
        return instance

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

fraction property writable

The Parameter that controls the mixing fraction of material_b in material_a.

__init__(material_a=None, material_b=None, fraction=None, name=None, unique_name=None, interface=None)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
material_a Union[Material, None]

The first material. By default, None.

None
material_b Union[Material, None]

The second material. By default, None.

None
fraction Union[Parameter, float, None]

The fraction of material_b in material_a. By default, None.

None
name Union[str, None]

Name of the material. By default, None.

None
interface

Calculator interface. By default, None.

None
Source code in src/easyreflectometry/sample/elements/materials/material_mixture.py
 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
def __init__(
    self,
    material_a: Union[Material, None] = None,
    material_b: Union[Material, None] = None,
    fraction: Union[Parameter, float, None] = None,
    name: Union[str, None] = None,
    unique_name: Optional[str] = None,
    interface=None,
):
    """Constructor.

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    material_a : Union[Material, None], optional
        The first material. By default, None.
    material_b : Union[Material, None], optional
        The second material. By default, None.
    fraction : Union[Parameter, float, None], optional
        The fraction of material_b in material_a. By default, None.
    name : Union[str, None], optional
        Name of the material. 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 material_a is None:
        material_a = Material(interface=interface)
    if material_b is None:
        material_b = Material(interface=interface)

    fraction = get_as_parameter(
        name='fraction',
        value=fraction,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Fraction',
    )

    sld_value = weighted_average(
        a=material_a.sld.value,
        b=material_b.sld.value,
        p=fraction.value,
    )
    isld_value = weighted_average(
        a=material_a.isld.value,
        b=material_b.isld.value,
        p=fraction.value,
    )

    sld = get_as_parameter(
        name='sld',
        value=sld_value,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Sld',
    )
    isld = get_as_parameter(
        name='isld',
        value=isld_value,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Isld',
    )

    # `name` may be None to signal "derive from material names"; resolve
    # before super().__init__ since BaseCore stores `_name` directly.
    if name is None:
        resolved_name = material_a.name + '/' + material_b.name
    else:
        resolved_name = name

    super().__init__(name=resolved_name, unique_name=unique_name)
    self._material_a = material_a
    self._material_b = material_b
    self._fraction = fraction
    self._sld = sld
    self._isld = isld

    self._materials_constraints()

    if interface is not None:
        self.interface = interface

from_dict(obj_dict) classmethod

Re-attach mixed-sld dependencies after :class:ModelBase swaps in the saved _fraction Parameter.

:class:ModelBase.from_dict runs __init__ (which builds the _sld / _isld constraints against the temporary _fraction created from the float kwargs) and then re-points self._fraction at the persisted Parameter. The constraint graph still references the temporary object, so subsequent mm.fraction = X mutations don't propagate to _sld / _isld. Re-running _materials_constraints here points the graph at the live objects.

Source code in src/easyreflectometry/sample/elements/materials/material_mixture.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@classmethod
def from_dict(cls, obj_dict: dict) -> 'MaterialMixture':
    """Re-attach mixed-sld dependencies after :class:`ModelBase` swaps in
    the saved ``_fraction`` Parameter.

    :class:`ModelBase.from_dict` runs ``__init__`` (which builds the
    ``_sld`` / ``_isld`` constraints against the *temporary* ``_fraction``
    created from the float kwargs) and then re-points ``self._fraction``
    at the persisted Parameter. The constraint graph still references the
    temporary object, so subsequent ``mm.fraction = X`` mutations don't
    propagate to ``_sld`` / ``_isld``. Re-running ``_materials_constraints``
    here points the graph at the live objects.
    """
    instance = super().from_dict(obj_dict)
    instance._materials_constraints()
    return instance

Multilayer

Bases: BaseAssembly

A multi layer is build from a single or a list of Layer or LayerCollection.

The multi layer will arrange the layers as slabs, allowing the reflectometry to be determined from them. The front layer is where the neutron beam starts in, it has an index of 0.

More information about the usage of this assembly is available in the multilayer documentation_

.. _multilayer documentation: ../sample/assemblies_library.html#multilayer

Source code in src/easyreflectometry/sample/assemblies/multilayer.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
 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
class Multilayer(BaseAssembly):
    """A multi layer is build from a single or a list of `Layer` or `LayerCollection`.

    The multi layer will arrange the layers as slabs, allowing the reflectometry to be determined from them.
    The front layer is where the neutron beam starts in, it has an index of 0.

    More information about the usage of this assembly is available in the
    `multilayer documentation`_

    .. _`multilayer documentation`: ../sample/assemblies_library.html#multilayer
    """

    def __init__(
        self,
        layers: Union[Layer, list[Layer], LayerCollection, None] = None,
        name: str = 'EasyMultilayer',
        unique_name: Optional[str] = None,
        interface=None,
        type: str = 'Multi-layer',
        populate_if_none: Optional[bool] = True,
        conformal_thickness: bool = False,
        conformal_roughness: bool = False,
    ):
        """Constructor.

        Parameters
        ----------
        populate_if_none : Optional[bool], optional
            By default, True.
        unique_name : Optional[str], optional
            By default, None.
        layers : Union[Layer, list[Layer], LayerCollection, None], optional
            The layers that make up the multi-layer. By default, None.
        name : str, optional
            Name for multi layer. By default, 'EasyMultilayer'.
        interface :
            Calculator interface. By default, None.
        type : str, optional
            Type of the constructed instance. By default, 'Multi-layer'.
        conformal_thickness : bool, optional
            Tie every layer's thickness to the front layer's. Serialization
            reads the current graph state through the matching property, so
            the ties are rebuilt on ``from_dict``. By default, False.
        conformal_roughness : bool, optional
            Tie every layer's roughness to the front layer's, likewise
            persistent. By default, False.
        """
        if layers is None:
            if populate_if_none:
                layers = LayerCollection([Layer(interface=interface)])
            else:
                layers = LayerCollection()
        elif isinstance(layers, Layer):
            layers = LayerCollection(layers, name=layers.name)
        elif isinstance(layers, list):
            layers = LayerCollection(*layers, name='/'.join([layer.name for layer in layers]))
        # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict
        # Else collisions might occur in global_object.map
        self.populate_if_none = False

        super().__init__(
            name=name,
            type=type,
            interface=interface,
            layers=layers,
            unique_name=unique_name,
        )
        if conformal_thickness:
            self.conformal_thickness = True
        if conformal_roughness:
            self.conformal_roughness = True

    def add_layer(self, *layers: tuple[Layer]) -> None:
        """Add a layer to the multi layer.

        Parameters
        ----------
        *layers : tuple[Layer]
            Layers to add to the multi layer.
        """
        for arg in layers:
            if issubclass(arg.__class__, Layer):
                self.layers.append(arg)
                if self.interface is not None:
                    self.interface().add_layer_to_item(arg.unique_name, self.unique_name)

    def duplicate_layer(self, idx: int) -> None:
        """Duplicate a given layer.

        Parameters
        ----------
        idx : int
            Index of layer to duplicate.
        """
        to_duplicate = self.layers[idx]
        duplicate_layer = Layer(
            material=to_duplicate.material,
            thickness=to_duplicate.thickness.value,
            roughness=to_duplicate.roughness.value,
            name=to_duplicate.name + ' duplicate',
        )
        self.add_layer(duplicate_layer)

    def remove_layer(self, idx: int) -> None:
        """Remove a layer from the item.

        Parameters
        ----------
        idx : int
            Index of layer to remove.
        """
        if self.interface is not None:
            self.interface().remove_layer_from_item(self.layers[idx].unique_name, self.unique_name)
        del self.layers[idx]

    # Representation
    @property
    def _dict_repr(self) -> dict:
        """A simplified dict representation."""
        return {self.name: self.layers._dict_repr}

    @classmethod
    def from_dict(cls, data: dict) -> Multilayer:
        """Create a Multilayer from a dictionary."""
        multilayer = super().from_dict(data)
        return multilayer

__init__(layers=None, name='EasyMultilayer', unique_name=None, interface=None, type='Multi-layer', populate_if_none=True, conformal_thickness=False, conformal_roughness=False)

Constructor.

Parameters:

Name Type Description Default
populate_if_none Optional[bool]

By default, True.

True
unique_name Optional[str]

By default, None.

None
layers Union[Layer, list[Layer], LayerCollection, None]

The layers that make up the multi-layer. By default, None.

None
name str

Name for multi layer. By default, 'EasyMultilayer'.

'EasyMultilayer'
interface

Calculator interface. By default, None.

None
type str

Type of the constructed instance. By default, 'Multi-layer'.

'Multi-layer'
conformal_thickness bool

Tie every layer's thickness to the front layer's. Serialization reads the current graph state through the matching property, so the ties are rebuilt on from_dict. By default, False.

False
conformal_roughness bool

Tie every layer's roughness to the front layer's, likewise persistent. By default, False.

False
Source code in src/easyreflectometry/sample/assemblies/multilayer.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
def __init__(
    self,
    layers: Union[Layer, list[Layer], LayerCollection, None] = None,
    name: str = 'EasyMultilayer',
    unique_name: Optional[str] = None,
    interface=None,
    type: str = 'Multi-layer',
    populate_if_none: Optional[bool] = True,
    conformal_thickness: bool = False,
    conformal_roughness: bool = False,
):
    """Constructor.

    Parameters
    ----------
    populate_if_none : Optional[bool], optional
        By default, True.
    unique_name : Optional[str], optional
        By default, None.
    layers : Union[Layer, list[Layer], LayerCollection, None], optional
        The layers that make up the multi-layer. By default, None.
    name : str, optional
        Name for multi layer. By default, 'EasyMultilayer'.
    interface :
        Calculator interface. By default, None.
    type : str, optional
        Type of the constructed instance. By default, 'Multi-layer'.
    conformal_thickness : bool, optional
        Tie every layer's thickness to the front layer's. Serialization
        reads the current graph state through the matching property, so
        the ties are rebuilt on ``from_dict``. By default, False.
    conformal_roughness : bool, optional
        Tie every layer's roughness to the front layer's, likewise
        persistent. By default, False.
    """
    if layers is None:
        if populate_if_none:
            layers = LayerCollection([Layer(interface=interface)])
        else:
            layers = LayerCollection()
    elif isinstance(layers, Layer):
        layers = LayerCollection(layers, name=layers.name)
    elif isinstance(layers, list):
        layers = LayerCollection(*layers, name='/'.join([layer.name for layer in layers]))
    # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict
    # Else collisions might occur in global_object.map
    self.populate_if_none = False

    super().__init__(
        name=name,
        type=type,
        interface=interface,
        layers=layers,
        unique_name=unique_name,
    )
    if conformal_thickness:
        self.conformal_thickness = True
    if conformal_roughness:
        self.conformal_roughness = True

add_layer(*layers)

Add a layer to the multi layer.

Parameters:

Name Type Description Default
*layers tuple[Layer]

Layers to add to the multi layer.

()
Source code in src/easyreflectometry/sample/assemblies/multilayer.py
86
87
88
89
90
91
92
93
94
95
96
97
98
def add_layer(self, *layers: tuple[Layer]) -> None:
    """Add a layer to the multi layer.

    Parameters
    ----------
    *layers : tuple[Layer]
        Layers to add to the multi layer.
    """
    for arg in layers:
        if issubclass(arg.__class__, Layer):
            self.layers.append(arg)
            if self.interface is not None:
                self.interface().add_layer_to_item(arg.unique_name, self.unique_name)

duplicate_layer(idx)

Duplicate a given layer.

Parameters:

Name Type Description Default
idx int

Index of layer to duplicate.

required
Source code in src/easyreflectometry/sample/assemblies/multilayer.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def duplicate_layer(self, idx: int) -> None:
    """Duplicate a given layer.

    Parameters
    ----------
    idx : int
        Index of layer to duplicate.
    """
    to_duplicate = self.layers[idx]
    duplicate_layer = Layer(
        material=to_duplicate.material,
        thickness=to_duplicate.thickness.value,
        roughness=to_duplicate.roughness.value,
        name=to_duplicate.name + ' duplicate',
    )
    self.add_layer(duplicate_layer)

remove_layer(idx)

Remove a layer from the item.

Parameters:

Name Type Description Default
idx int

Index of layer to remove.

required
Source code in src/easyreflectometry/sample/assemblies/multilayer.py
117
118
119
120
121
122
123
124
125
126
127
def remove_layer(self, idx: int) -> None:
    """Remove a layer from the item.

    Parameters
    ----------
    idx : int
        Index of layer to remove.
    """
    if self.interface is not None:
        self.interface().remove_layer_from_item(self.layers[idx].unique_name, self.unique_name)
    del self.layers[idx]

from_dict(data) classmethod

Create a Multilayer from a dictionary.

Source code in src/easyreflectometry/sample/assemblies/multilayer.py
135
136
137
138
139
@classmethod
def from_dict(cls, data: dict) -> Multilayer:
    """Create a Multilayer from a dictionary."""
    multilayer = super().from_dict(data)
    return multilayer

WrapperBase

Source code in src/easyreflectometry/calculators/wrapper_base.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
 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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
class WrapperBase:
    #: Whether this calculator backend can model magnetic samples.
    supports_magnetism = False

    def __init__(self):
        """Constructor."""
        self._magnetism = False
        self._polarization_channel = PolarizationChannel.PP
        self.storage = {
            'material': {},
            'layer': {},
            'item': {},
            'model': {},
        }
        self._resolution_function = PercentageFwhm()

    def reset_storage(self):
        """Reset the storage area to blank."""
        self.storage = {
            'material': {},
            'layer': {},
            'item': {},
            'model': {},
        }

    @abstractmethod
    def create_material(self, name: str):
        """Create a material using SLD.

        Parameters
        ----------
        name : str
            The name of the material.
        """
        ...

    @abstractmethod
    def create_layer(self, name: str):
        """Create a layer using Slab.

        Parameters
        ----------
        name : str
            The name of the layer.
        """
        ...

    @abstractmethod
    def create_item(self, name: str):
        """Create an item using Stack.

        Parameters
        ----------
        name : str
            The name of the item.
        """
        ...

    @abstractmethod
    def create_model(self, name: str):
        """Create a model for analysis.

        Parameters
        ----------
        name : str
            Name for the model.
        """
        ...

    @abstractmethod
    def update_model(self, name: str, **kwargs):
        """Update the non-structural parameters of the model.

        Parameters
        ----------
        name : str
            Name for the model.
        **kwargs :
        """
        ...

    @abstractmethod
    def get_model_value(self, name: str, key: str) -> float:
        """A function to get a given model value.

        Parameters
        ----------
        name : str
            Name for the model.
        key : str
            The given value keys.
        """
        ...

    @abstractmethod
    def assign_material_to_layer(self, material_name: str, layer_name: str):
        """Assign a material to a layer.

        Parameters
        ----------
        material_name : str
            The material name.
        layer_name : str
            The layer name.
        """
        ...

    @abstractmethod
    def add_layer_to_item(self, layer_name: str, item_name: str):
        """Create a layer from the material of the same name, in a given item.

        Parameters
        ----------
        layer_name : str
            The layer name.
        item_name : str
            The item name.
        """
        ...

    @abstractmethod
    def add_item(self, item_name: str, model_name: str):
        """Add an item to the model.

        Parameters
        ----------
        item_name : str
            Items to add to model.
        model_name : str
            Name for the model.
        """
        ...

    @abstractmethod
    def remove_layer_from_item(self, layer_name: str, item_name: str):
        """Remove a layer in a given item.

        Parameters
        ----------
        layer_name : str
            The layer name.
        item_name : str
            The item name.
        """
        ...

    @abstractmethod
    def remove_item(self, item_name: str, model_name: str):
        """Remove a given item.

        Parameters
        ----------
        item_name : str
            The item name.
        model_name : str
            Name of the model.
        """
        ...

    @abstractmethod
    def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray:
        """For a given q array calculate the corresponding reflectivity.

        Parameters
        ----------
        q_array : np.ndarray
            Array of data points to be calculated.
        model_name : str
            The model name.

        Returns
        -------
        np.ndarray
            Reflectivity calculated at q.
        """
        ...

    @abstractmethod
    def sld_profile(self, model_name: str) -> tuple[np.ndarray, np.ndarray]:
        """Return the scattering length density profile.

        Parameters
        ----------
        model_name : str
            Name for the model.

        Returns
        -------
        tuple[np.ndarray, np.ndarray]
            Z and sld(z).
        """
        ...

    def update_material(self, name: str, **kwargs):
        """Update a material.

        Parameters
        ----------
        name : str
            The name of the material.
        **kwargs :
            Key-value pairs of attributes to update.
        """
        material = self.storage['material'][name]
        for key in kwargs.keys():
            item = getattr(material, key)
            setattr(item, 'value', kwargs[key])

    def get_material_value(self, name: str, key: str) -> float:
        """A function to get a given material value.

        Parameters
        ----------
        name : str
            The material name.
        key : str
            The given value keys.

        Returns
        -------
        float
            The desired value.
        """
        material = self.storage['material'][name]
        item = getattr(material, key)
        return getattr(item, 'value')

    def update_layer(self, name: str, **kwargs):
        """Update a layer in a given item.

        Parameters
        ----------
        name : str
            The layer name.
        **kwargs :
        """
        layer = self.storage['layer'][name]
        for key in kwargs.keys():
            ii = getattr(layer, key)
            setattr(ii, 'value', kwargs[key])

    def get_layer_value(self, name: str, key: str) -> float:
        """A function to get a given layer value.

        Parameters
        ----------
        name : str
            The layer name.
        key : str
            The given value keys.
        """
        layer = self.storage['layer'][name]
        ii = getattr(layer, key)
        return getattr(ii, 'value')

    def update_item(self, name: str, **kwargs):
        """Update a layer.

        Parameters
        ----------
        **kwargs :
        name : str
            The item name.
        """
        item = self.storage['item'][name]
        for key in kwargs.keys():
            ii = getattr(item, key)
            setattr(ii, 'value', kwargs[key])

    def get_item_value(self, name: str, key: str) -> float:
        """A function to get a given item value.

        Parameters
        ----------
        name : str
            The item name.
        key : str
            The given value keys.

        Returns
        -------
        float
            The desired value.
        """
        item = self.storage['item'][name]
        item = getattr(item, key)
        return getattr(item, 'value')

    def set_resolution_function(self, resolution_function: ResolutionFunction) -> None:
        """Set the resolution function for the calculator.

        Parameters
        ----------
        resolution_function : ResolutionFunction
            The resolution function.
        """
        self._resolution_function = resolution_function

    @property
    def magnetism(self) -> bool:
        """Magnetism function."""
        return self._magnetism

    @magnetism.setter
    def magnetism(self, magnetism: bool) -> None:
        """Set the magnetism flag.

        Parameters
        ----------
        magnetism : bool
            The magnetism flag.
        """
        if magnetism and not self.supports_magnetism:
            raise NotImplementedError(f'Magnetism is not supported by {self.__class__.__name__}')
        self._magnetism = magnetism
        if magnetism:
            # Attach any magnetic values set (or restored) while magnetism was off.
            self._apply_magnetism_to_layers()
        else:
            # A non-pp channel is only meaningful on the polarized probe path.
            self._polarization_channel = PolarizationChannel.PP
            # Leave no magnetic residue behind: the unpolarized calculation path
            # must work on the layers that already exist.
            self._remove_magnetism_from_layers()

    def _remove_magnetism_from_layers(self) -> None:
        """Strip backend magnetism state from existing layers when magnetism is disabled.

        No-op by default; overridden by backends that attach magnetic objects to layers.
        """

    def _apply_magnetism_to_layers(self) -> None:
        """Attach stored magnetic values to existing layers when magnetism is enabled.

        No-op by default; overridden by backends that attach magnetic objects to layers.
        """

    def remove_layer_magnetism(self, name: str) -> None:
        """Remove the magnetic state of one layer; disable magnetism when none is left.

        No-op by default; overridden by backends that support magnetism.

        Parameters
        ----------
        name : str
            The layer name.
        """

    @property
    def polarization_channel(self) -> PolarizationChannel:
        """The spin channel returned by `calculate` when magnetism is enabled."""
        return self._polarization_channel

    @polarization_channel.setter
    def polarization_channel(self, channel: PolarizationChannel | str) -> None:
        """Set the spin channel returned by `calculate`.

        Parameters
        ----------
        channel : PolarizationChannel | str
            One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).
        """
        channel = PolarizationChannel(channel)
        if channel is not PolarizationChannel.PP and not self._magnetism:
            raise ValueError(f"Selecting the '{channel.value}' channel requires magnetism to be enabled.")
        self._polarization_channel = channel

    def calculate_channel(self, q_array: np.ndarray, model_name: str, channel: PolarizationChannel | str) -> np.ndarray:
        """For a given q array calculate the reflectivity of one explicit spin channel.

        Unlike the `polarization_channel` property (global calculator state used by
        `calculate`), the channel is passed explicitly, so several channels can be
        evaluated against the same model, e.g. one per dataset in a simultaneous
        multi-channel fit.

        Parameters
        ----------
        q_array : np.ndarray
            Array of data points to be calculated.
        model_name : str
            The model name.
        channel : PolarizationChannel | str
            One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).

        Returns
        -------
        np.ndarray
            Reflectivity of the requested channel at q.
        """
        channel = PolarizationChannel(channel)
        if not self._magnetism:
            if channel is PolarizationChannel.PP:
                # No explicit `.copy()` needed here: `calculate()` always returns
                # a fresh array (a cached, shared array only exists on the
                # magnetism-enabled `calculate_polarized` path below).
                return self.calculate(q_array, model_name)
            raise ValueError(f"Calculating the '{channel.value}' channel requires magnetism to be enabled.")
        return self.calculate_polarized(q_array, model_name)[channel.value]

    def calculate_polarized(self, q_array: np.ndarray, model_name: str) -> dict[str, np.ndarray]:
        """For a given q array calculate the reflectivity of all four spin channels.

        Parameters
        ----------
        q_array : np.ndarray
            Array of data points to be calculated.
        model_name : str
            The model name.

        Returns
        -------
        dict[str, np.ndarray]
            Reflectivity per spin channel, keyed 'pp', 'pm', 'mp', 'mm' (in that order).
        """
        raise NotImplementedError(f'{self.__class__.__name__} does not support polarized reflectivity.')

    def magnetic_sld_profile(self, model_name: str) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        """Return the nuclear and magnetic scattering length density profiles.

        Parameters
        ----------
        model_name : str
            The model name.

        Returns
        -------
            z, sld(z), magnetic sld rhoM(z) and magnetic angle thetaM(z).
        """
        raise NotImplementedError(f'{self.__class__.__name__} does not support magnetic sld profiles.')

magnetism property writable

Magnetism function.

polarization_channel property writable

The spin channel returned by calculate when magnetism is enabled.

__init__()

Constructor.

Source code in src/easyreflectometry/calculators/wrapper_base.py
18
19
20
21
22
23
24
25
26
27
28
def __init__(self):
    """Constructor."""
    self._magnetism = False
    self._polarization_channel = PolarizationChannel.PP
    self.storage = {
        'material': {},
        'layer': {},
        'item': {},
        'model': {},
    }
    self._resolution_function = PercentageFwhm()

reset_storage()

Reset the storage area to blank.

Source code in src/easyreflectometry/calculators/wrapper_base.py
30
31
32
33
34
35
36
37
def reset_storage(self):
    """Reset the storage area to blank."""
    self.storage = {
        'material': {},
        'layer': {},
        'item': {},
        'model': {},
    }

create_material(name) abstractmethod

Create a material using SLD.

Parameters:

Name Type Description Default
name str

The name of the material.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
39
40
41
42
43
44
45
46
47
48
@abstractmethod
def create_material(self, name: str):
    """Create a material using SLD.

    Parameters
    ----------
    name : str
        The name of the material.
    """
    ...

create_layer(name) abstractmethod

Create a layer using Slab.

Parameters:

Name Type Description Default
name str

The name of the layer.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
50
51
52
53
54
55
56
57
58
59
@abstractmethod
def create_layer(self, name: str):
    """Create a layer using Slab.

    Parameters
    ----------
    name : str
        The name of the layer.
    """
    ...

create_item(name) abstractmethod

Create an item using Stack.

Parameters:

Name Type Description Default
name str

The name of the item.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
61
62
63
64
65
66
67
68
69
70
@abstractmethod
def create_item(self, name: str):
    """Create an item using Stack.

    Parameters
    ----------
    name : str
        The name of the item.
    """
    ...

create_model(name) abstractmethod

Create a model for analysis.

Parameters:

Name Type Description Default
name str

Name for the model.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
72
73
74
75
76
77
78
79
80
81
@abstractmethod
def create_model(self, name: str):
    """Create a model for analysis.

    Parameters
    ----------
    name : str
        Name for the model.
    """
    ...

update_model(name, **kwargs) abstractmethod

Update the non-structural parameters of the model.

Parameters:

Name Type Description Default
name str

Name for the model.

required
**kwargs
{}
Source code in src/easyreflectometry/calculators/wrapper_base.py
83
84
85
86
87
88
89
90
91
92
93
@abstractmethod
def update_model(self, name: str, **kwargs):
    """Update the non-structural parameters of the model.

    Parameters
    ----------
    name : str
        Name for the model.
    **kwargs :
    """
    ...

get_model_value(name, key) abstractmethod

A function to get a given model value.

Parameters:

Name Type Description Default
name str

Name for the model.

required
key str

The given value keys.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@abstractmethod
def get_model_value(self, name: str, key: str) -> float:
    """A function to get a given model value.

    Parameters
    ----------
    name : str
        Name for the model.
    key : str
        The given value keys.
    """
    ...

assign_material_to_layer(material_name, layer_name) abstractmethod

Assign a material to a layer.

Parameters:

Name Type Description Default
material_name str

The material name.

required
layer_name str

The layer name.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
108
109
110
111
112
113
114
115
116
117
118
119
@abstractmethod
def assign_material_to_layer(self, material_name: str, layer_name: str):
    """Assign a material to a layer.

    Parameters
    ----------
    material_name : str
        The material name.
    layer_name : str
        The layer name.
    """
    ...

add_layer_to_item(layer_name, item_name) abstractmethod

Create a layer from the material of the same name, in a given item.

Parameters:

Name Type Description Default
layer_name str

The layer name.

required
item_name str

The item name.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
121
122
123
124
125
126
127
128
129
130
131
132
@abstractmethod
def add_layer_to_item(self, layer_name: str, item_name: str):
    """Create a layer from the material of the same name, in a given item.

    Parameters
    ----------
    layer_name : str
        The layer name.
    item_name : str
        The item name.
    """
    ...

add_item(item_name, model_name) abstractmethod

Add an item to the model.

Parameters:

Name Type Description Default
item_name str

Items to add to model.

required
model_name str

Name for the model.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
134
135
136
137
138
139
140
141
142
143
144
145
@abstractmethod
def add_item(self, item_name: str, model_name: str):
    """Add an item to the model.

    Parameters
    ----------
    item_name : str
        Items to add to model.
    model_name : str
        Name for the model.
    """
    ...

remove_layer_from_item(layer_name, item_name) abstractmethod

Remove a layer in a given item.

Parameters:

Name Type Description Default
layer_name str

The layer name.

required
item_name str

The item name.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
147
148
149
150
151
152
153
154
155
156
157
158
@abstractmethod
def remove_layer_from_item(self, layer_name: str, item_name: str):
    """Remove a layer in a given item.

    Parameters
    ----------
    layer_name : str
        The layer name.
    item_name : str
        The item name.
    """
    ...

remove_item(item_name, model_name) abstractmethod

Remove a given item.

Parameters:

Name Type Description Default
item_name str

The item name.

required
model_name str

Name of the model.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
160
161
162
163
164
165
166
167
168
169
170
171
@abstractmethod
def remove_item(self, item_name: str, model_name: str):
    """Remove a given item.

    Parameters
    ----------
    item_name : str
        The item name.
    model_name : str
        Name of the model.
    """
    ...

calculate(q_array, model_name) abstractmethod

For a given q array calculate the corresponding reflectivity.

Parameters:

Name Type Description Default
q_array np.ndarray

Array of data points to be calculated.

required
model_name str

The model name.

required

Returns:

Type Description
np.ndarray

Reflectivity calculated at q.

Source code in src/easyreflectometry/calculators/wrapper_base.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
@abstractmethod
def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray:
    """For a given q array calculate the corresponding reflectivity.

    Parameters
    ----------
    q_array : np.ndarray
        Array of data points to be calculated.
    model_name : str
        The model name.

    Returns
    -------
    np.ndarray
        Reflectivity calculated at q.
    """
    ...

sld_profile(model_name) abstractmethod

Return the scattering length density profile.

Parameters:

Name Type Description Default
model_name str

Name for the model.

required

Returns:

Type Description
tuple[np.ndarray, np.ndarray]

Z and sld(z).

Source code in src/easyreflectometry/calculators/wrapper_base.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
@abstractmethod
def sld_profile(self, model_name: str) -> tuple[np.ndarray, np.ndarray]:
    """Return the scattering length density profile.

    Parameters
    ----------
    model_name : str
        Name for the model.

    Returns
    -------
    tuple[np.ndarray, np.ndarray]
        Z and sld(z).
    """
    ...

update_material(name, **kwargs)

Update a material.

Parameters:

Name Type Description Default
name str

The name of the material.

required
**kwargs

Key-value pairs of attributes to update.

{}
Source code in src/easyreflectometry/calculators/wrapper_base.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def update_material(self, name: str, **kwargs):
    """Update a material.

    Parameters
    ----------
    name : str
        The name of the material.
    **kwargs :
        Key-value pairs of attributes to update.
    """
    material = self.storage['material'][name]
    for key in kwargs.keys():
        item = getattr(material, key)
        setattr(item, 'value', kwargs[key])

get_material_value(name, key)

A function to get a given material value.

Parameters:

Name Type Description Default
name str

The material name.

required
key str

The given value keys.

required

Returns:

Type Description
float

The desired value.

Source code in src/easyreflectometry/calculators/wrapper_base.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def get_material_value(self, name: str, key: str) -> float:
    """A function to get a given material value.

    Parameters
    ----------
    name : str
        The material name.
    key : str
        The given value keys.

    Returns
    -------
    float
        The desired value.
    """
    material = self.storage['material'][name]
    item = getattr(material, key)
    return getattr(item, 'value')

update_layer(name, **kwargs)

Update a layer in a given item.

Parameters:

Name Type Description Default
name str

The layer name.

required
**kwargs
{}
Source code in src/easyreflectometry/calculators/wrapper_base.py
241
242
243
244
245
246
247
248
249
250
251
252
253
def update_layer(self, name: str, **kwargs):
    """Update a layer in a given item.

    Parameters
    ----------
    name : str
        The layer name.
    **kwargs :
    """
    layer = self.storage['layer'][name]
    for key in kwargs.keys():
        ii = getattr(layer, key)
        setattr(ii, 'value', kwargs[key])

get_layer_value(name, key)

A function to get a given layer value.

Parameters:

Name Type Description Default
name str

The layer name.

required
key str

The given value keys.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
255
256
257
258
259
260
261
262
263
264
265
266
267
def get_layer_value(self, name: str, key: str) -> float:
    """A function to get a given layer value.

    Parameters
    ----------
    name : str
        The layer name.
    key : str
        The given value keys.
    """
    layer = self.storage['layer'][name]
    ii = getattr(layer, key)
    return getattr(ii, 'value')

update_item(name, **kwargs)

Update a layer.

Parameters:

Name Type Description Default
**kwargs
{}
name str

The item name.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
269
270
271
272
273
274
275
276
277
278
279
280
281
def update_item(self, name: str, **kwargs):
    """Update a layer.

    Parameters
    ----------
    **kwargs :
    name : str
        The item name.
    """
    item = self.storage['item'][name]
    for key in kwargs.keys():
        ii = getattr(item, key)
        setattr(ii, 'value', kwargs[key])

get_item_value(name, key)

A function to get a given item value.

Parameters:

Name Type Description Default
name str

The item name.

required
key str

The given value keys.

required

Returns:

Type Description
float

The desired value.

Source code in src/easyreflectometry/calculators/wrapper_base.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def get_item_value(self, name: str, key: str) -> float:
    """A function to get a given item value.

    Parameters
    ----------
    name : str
        The item name.
    key : str
        The given value keys.

    Returns
    -------
    float
        The desired value.
    """
    item = self.storage['item'][name]
    item = getattr(item, key)
    return getattr(item, 'value')

set_resolution_function(resolution_function)

Set the resolution function for the calculator.

Parameters:

Name Type Description Default
resolution_function ResolutionFunction

The resolution function.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
302
303
304
305
306
307
308
309
310
def set_resolution_function(self, resolution_function: ResolutionFunction) -> None:
    """Set the resolution function for the calculator.

    Parameters
    ----------
    resolution_function : ResolutionFunction
        The resolution function.
    """
    self._resolution_function = resolution_function

remove_layer_magnetism(name)

Remove the magnetic state of one layer; disable magnetism when none is left.

No-op by default; overridden by backends that support magnetism.

Parameters:

Name Type Description Default
name str

The layer name.

required
Source code in src/easyreflectometry/calculators/wrapper_base.py
351
352
353
354
355
356
357
358
359
360
def remove_layer_magnetism(self, name: str) -> None:
    """Remove the magnetic state of one layer; disable magnetism when none is left.

    No-op by default; overridden by backends that support magnetism.

    Parameters
    ----------
    name : str
        The layer name.
    """

calculate_channel(q_array, model_name, channel)

For a given q array calculate the reflectivity of one explicit spin channel.

Unlike the polarization_channel property (global calculator state used by calculate), the channel is passed explicitly, so several channels can be evaluated against the same model, e.g. one per dataset in a simultaneous multi-channel fit.

Parameters:

Name Type Description Default
q_array np.ndarray

Array of data points to be calculated.

required
model_name str

The model name.

required
channel PolarizationChannel | str

One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).

required

Returns:

Type Description
np.ndarray

Reflectivity of the requested channel at q.

Source code in src/easyreflectometry/calculators/wrapper_base.py
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def calculate_channel(self, q_array: np.ndarray, model_name: str, channel: PolarizationChannel | str) -> np.ndarray:
    """For a given q array calculate the reflectivity of one explicit spin channel.

    Unlike the `polarization_channel` property (global calculator state used by
    `calculate`), the channel is passed explicitly, so several channels can be
    evaluated against the same model, e.g. one per dataset in a simultaneous
    multi-channel fit.

    Parameters
    ----------
    q_array : np.ndarray
        Array of data points to be calculated.
    model_name : str
        The model name.
    channel : PolarizationChannel | str
        One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).

    Returns
    -------
    np.ndarray
        Reflectivity of the requested channel at q.
    """
    channel = PolarizationChannel(channel)
    if not self._magnetism:
        if channel is PolarizationChannel.PP:
            # No explicit `.copy()` needed here: `calculate()` always returns
            # a fresh array (a cached, shared array only exists on the
            # magnetism-enabled `calculate_polarized` path below).
            return self.calculate(q_array, model_name)
        raise ValueError(f"Calculating the '{channel.value}' channel requires magnetism to be enabled.")
    return self.calculate_polarized(q_array, model_name)[channel.value]

calculate_polarized(q_array, model_name)

For a given q array calculate the reflectivity of all four spin channels.

Parameters:

Name Type Description Default
q_array np.ndarray

Array of data points to be calculated.

required
model_name str

The model name.

required

Returns:

Type Description
dict[str, np.ndarray]

Reflectivity per spin channel, keyed 'pp', 'pm', 'mp', 'mm' (in that order).

Source code in src/easyreflectometry/calculators/wrapper_base.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def calculate_polarized(self, q_array: np.ndarray, model_name: str) -> dict[str, np.ndarray]:
    """For a given q array calculate the reflectivity of all four spin channels.

    Parameters
    ----------
    q_array : np.ndarray
        Array of data points to be calculated.
    model_name : str
        The model name.

    Returns
    -------
    dict[str, np.ndarray]
        Reflectivity per spin channel, keyed 'pp', 'pm', 'mp', 'mm' (in that order).
    """
    raise NotImplementedError(f'{self.__class__.__name__} does not support polarized reflectivity.')

magnetic_sld_profile(model_name)

Return the nuclear and magnetic scattering length density profiles.

Parameters:

Name Type Description Default
model_name str

The model name.

required

Returns:

Type Description
z, sld(z), magnetic sld rhoM(z) and magnetic angle thetaM(z).
Source code in src/easyreflectometry/calculators/wrapper_base.py
430
431
432
433
434
435
436
437
438
439
440
441
442
def magnetic_sld_profile(self, model_name: str) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Return the nuclear and magnetic scattering length density profiles.

    Parameters
    ----------
    model_name : str
        The model name.

    Returns
    -------
        z, sld(z), magnetic sld rhoM(z) and magnetic angle thetaM(z).
    """
    raise NotImplementedError(f'{self.__class__.__name__} does not support magnetic sld profiles.')

CalculatorBase

Bases: SerializerComponent

This class is a template and defines all properties that a calculator should have.

Source code in src/easyreflectometry/calculators/calculator_base.py
 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
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
class CalculatorBase(SerializerComponent, metaclass=ABCMeta):
    """This class is a template and defines all properties that a calculator should have."""

    _calculators: list[CalculatorBase] = []  # class variable to store all calculators
    _material_link: dict[str, str]
    _layer_link: dict[str, str]
    _item_link: dict[str, str]
    _model_link: dict[str, str]

    def __init_subclass__(cls, is_abstract: bool = False, **kwargs) -> None:
        r"""Initialise all subclasses so that they can be created in the factory.

        Parameters
        ----------
        cls :
        is_abstract : bool, optional
            Is this a subclass which shouldn't be dded. By default, False.
        **kwargs :
            Key word arguments.
        """
        super().__init_subclass__(**kwargs)
        if not is_abstract:
            cls._calculators.append(cls)

    def __init__(self):
        """Init function."""
        self._namespace = {}
        self._wrapper: WrapperBase

    def reset_storage(self) -> None:
        r"""Reset the storage area of the calculator."""
        self._wrapper.reset_storage()

    def create(self, model: Material | Layer | Multilayer | Model) -> list[ItemContainer]:
        """Creation function.

        Parameters
        ----------
        model : Material | Layer | Multilayer | Model
            Object to be created.
        """
        r_list = []
        t_ = type(model)
        if issubclass(t_, Material):
            key = model.unique_name
            if key not in self._wrapper.storage['material'].keys():
                self._wrapper.create_material(key)
            r_list.append(
                ItemContainer(
                    key,
                    self._material_link,
                    self._wrapper.get_material_value,
                    self._wrapper.update_material,
                )
            )
        elif issubclass(t_, MaterialMixture):
            key = model.unique_name
            if key not in self._wrapper.storage['material'].keys():
                self._wrapper.create_material(key)
            r_list.append(
                ItemContainer(
                    key,
                    self._material_link,
                    self._wrapper.get_material_value,
                    self._wrapper.update_material,
                )
            )
        elif issubclass(t_, Layer):
            key = model.unique_name
            if key not in self._wrapper.storage['layer'].keys():
                self._wrapper.create_layer(key)
            r_list.append(
                ItemContainer(
                    key,
                    self._layer_link,
                    self._wrapper.get_layer_value,
                    self._wrapper.update_layer,
                )
            )
            self.assign_material_to_layer(model.material.unique_name, key)
        elif issubclass(t_, BaseAssembly):
            key = model.unique_name
            self._wrapper.create_item(key)
            r_list.append(
                ItemContainer(
                    key,
                    self._item_link,
                    self._wrapper.get_item_value,
                    self._wrapper.update_item,
                )
            )
            for i in model.layers:
                self.add_layer_to_item(i.unique_name, model.unique_name)
        elif issubclass(t_, Model):
            key = model.unique_name
            self._wrapper.create_model(key)
            r_list.append(
                ItemContainer(
                    key,
                    self._model_link,
                    self._wrapper.get_model_value,
                    self._wrapper.update_model,
                )
            )
            for i in model.sample:
                self.add_item_to_model(i.unique_name, key)
        return r_list

    def assign_material_to_layer(self, material_id: str, layer_id: str) -> None:
        """Assign a material to a layer.

        Parameters
        ----------
        material_id : str
            The material name.
        layer_id : str
            The layer name.
        """
        self._wrapper.assign_material_to_layer(material_id, layer_id)

    def add_layer_to_item(self, layer_id: str, item_id: str) -> None:
        """Add a layer to the item stack.

        Parameters
        ----------
        item_id : str
            The item id.
        layer_id : str
            The layer id.
        """
        self._wrapper.add_layer_to_item(layer_id, item_id)

    def remove_layer_from_item(self, layer_id: str, item_id: str) -> None:
        """Remove a layer from an item stack.

        Parameters
        ----------
        item_id : str
            The item id.
        layer_id : str
            The layer id.
        """
        self._wrapper.remove_layer_from_item(layer_id, item_id)

    def add_item_to_model(self, item_id: str, model_id: str) -> None:
        """Add a layer to the item stack.

        Parameters
        ----------
        item_id : str
            The item id.
        model_id : str
            The model id.
        """
        self._wrapper.add_item(item_id, model_id)

    def remove_item_from_model(self, item_id: str, model_id: str) -> None:
        """Remove an item from the model.

        Parameters
        ----------
        item_id : str
            The item id.
        model_id : str
            The model id.
        """
        self._wrapper.remove_item(item_id, model_id)

    def reflectity_profile(self, x_array: np.ndarray, model_id: str) -> np.ndarray:
        """Determines the reflectivity profile for the given range and model.

        Parameters
        ----------
        x_array : np.ndarray
            Points to be calculated at.
        model_id : str
            The model id.
        """
        return self._wrapper.calculate(x_array, model_id)

    def reflectivity_profile_channel(self, x_array: np.ndarray, model_id: str, channel) -> np.ndarray:
        """Determine the reflectivity profile of one explicit spin channel.

        Unlike `polarization_channel` (global calculator state), the channel is an
        argument, so several channels can be evaluated against the same model —
        one per dataset in a simultaneous multi-channel fit.

        Parameters
        ----------
        x_array : np.ndarray
            Points to be calculated at.
        model_id : str
            The model id.
        channel : PolarizationChannel | str
            One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).

        Returns
        -------
        np.ndarray
            Reflectivity of the requested channel at q.
        """
        return self._wrapper.calculate_channel(x_array, model_id, channel)

    def polarized_reflectivity_profiles(self, x_array: np.ndarray, model_id: str) -> dict[str, np.ndarray]:
        """Determines the reflectivity profiles of all four spin channels for the given range and model.

        Requires `include_magnetism` to be enabled and a calculator that supports it (refl1d).

        Parameters
        ----------
        x_array : np.ndarray
            Points to be calculated at.
        model_id : str
            The model id.

        Returns
        -------
        dict[str, np.ndarray]
            Reflectivity per spin channel, keyed 'pp', 'pm', 'mp', 'mm' (in that order).
        """
        return self._wrapper.calculate_polarized(x_array, model_id)

    def sld_profile(self, model_id: str) -> tuple[np.ndarray, np.ndarray]:
        """Return the scattering length density profile.

        Parameters
        ----------
        model_id : str
            The model id.

        Returns
        -------
        tuple[np.ndarray, np.ndarray]
            z and sld(z).
        """
        return self._wrapper.sld_profile(model_id)

    def magnetic_sld_profile(self, model_id: str) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        """Return the nuclear and magnetic scattering length density profiles.

        Requires `include_magnetism` to be enabled and a calculator that supports it (refl1d).

        Parameters
        ----------
        model_id : str
            The model id.

        Returns
        -------
        tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
            z, sld(z), magnetic sld rhoM(z) and magnetic angle thetaM(z).
        """
        return self._wrapper.magnetic_sld_profile(model_id)

    def set_resolution_function(self, resolution_function: Callable[[np.array], np.array]) -> None:
        """Set resolution function."""
        return self._wrapper.set_resolution_function(resolution_function)

    @property
    def supports_magnetism(self) -> bool:
        """Whether this calculator backend can model magnetic samples."""
        return self._wrapper.supports_magnetism

    def remove_layer_magnetism(self, layer_id: str) -> None:
        """Remove the magnetic state of one layer; disables `include_magnetism`
        when no magnetic layer is left.

        Parameters
        ----------
        layer_id : str
            The layer id.
        """
        self._wrapper.remove_layer_magnetism(layer_id)

    @property
    def include_magnetism(self):
        """Include magnetism."""
        return self._wrapper.magnetism

    @include_magnetism.setter
    def include_magnetism(self, magnetism: bool):
        """Set the magnetism flag for the calculator.

        Parameters
        ----------
        magnetism : bool
            True if the calculator should include magnetism.
        """
        self._wrapper.magnetism = magnetism

    @property
    def polarization_channel(self):
        """The spin channel ('pp', 'pm', 'mp' or 'mm') used by `reflectity_profile` when magnetism is enabled.

        Note: this state belongs to the currently-active calculator instance; switching
        calculators via the factory constructs a fresh instance and resets it.
        """
        return self._wrapper.polarization_channel

    @polarization_channel.setter
    def polarization_channel(self, channel) -> None:
        """Set the spin channel for reflectivity calculations.

        Parameters
        ----------
        channel : PolarizationChannel | str
            One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).
        """
        self._wrapper.polarization_channel = channel

supports_magnetism property

Whether this calculator backend can model magnetic samples.

include_magnetism property writable

Include magnetism.

polarization_channel property writable

The spin channel ('pp', 'pm', 'mp' or 'mm') used by reflectity_profile when magnetism is enabled.

Note: this state belongs to the currently-active calculator instance; switching calculators via the factory constructs a fresh instance and resets it.

__init_subclass__(is_abstract=False, **kwargs)

Initialise all subclasses so that they can be created in the factory.

Parameters:

Name Type Description Default
cls
required
is_abstract bool

Is this a subclass which shouldn't be dded. By default, False.

False
**kwargs

Key word arguments.

{}
Source code in src/easyreflectometry/calculators/calculator_base.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init_subclass__(cls, is_abstract: bool = False, **kwargs) -> None:
    r"""Initialise all subclasses so that they can be created in the factory.

    Parameters
    ----------
    cls :
    is_abstract : bool, optional
        Is this a subclass which shouldn't be dded. By default, False.
    **kwargs :
        Key word arguments.
    """
    super().__init_subclass__(**kwargs)
    if not is_abstract:
        cls._calculators.append(cls)

__init__()

Init function.

Source code in src/easyreflectometry/calculators/calculator_base.py
48
49
50
51
def __init__(self):
    """Init function."""
    self._namespace = {}
    self._wrapper: WrapperBase

reset_storage()

Reset the storage area of the calculator.

Source code in src/easyreflectometry/calculators/calculator_base.py
53
54
55
def reset_storage(self) -> None:
    r"""Reset the storage area of the calculator."""
    self._wrapper.reset_storage()

create(model)

Creation function.

Parameters:

Name Type Description Default
model Material | Layer | Multilayer | Model

Object to be created.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
 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
def create(self, model: Material | Layer | Multilayer | Model) -> list[ItemContainer]:
    """Creation function.

    Parameters
    ----------
    model : Material | Layer | Multilayer | Model
        Object to be created.
    """
    r_list = []
    t_ = type(model)
    if issubclass(t_, Material):
        key = model.unique_name
        if key not in self._wrapper.storage['material'].keys():
            self._wrapper.create_material(key)
        r_list.append(
            ItemContainer(
                key,
                self._material_link,
                self._wrapper.get_material_value,
                self._wrapper.update_material,
            )
        )
    elif issubclass(t_, MaterialMixture):
        key = model.unique_name
        if key not in self._wrapper.storage['material'].keys():
            self._wrapper.create_material(key)
        r_list.append(
            ItemContainer(
                key,
                self._material_link,
                self._wrapper.get_material_value,
                self._wrapper.update_material,
            )
        )
    elif issubclass(t_, Layer):
        key = model.unique_name
        if key not in self._wrapper.storage['layer'].keys():
            self._wrapper.create_layer(key)
        r_list.append(
            ItemContainer(
                key,
                self._layer_link,
                self._wrapper.get_layer_value,
                self._wrapper.update_layer,
            )
        )
        self.assign_material_to_layer(model.material.unique_name, key)
    elif issubclass(t_, BaseAssembly):
        key = model.unique_name
        self._wrapper.create_item(key)
        r_list.append(
            ItemContainer(
                key,
                self._item_link,
                self._wrapper.get_item_value,
                self._wrapper.update_item,
            )
        )
        for i in model.layers:
            self.add_layer_to_item(i.unique_name, model.unique_name)
    elif issubclass(t_, Model):
        key = model.unique_name
        self._wrapper.create_model(key)
        r_list.append(
            ItemContainer(
                key,
                self._model_link,
                self._wrapper.get_model_value,
                self._wrapper.update_model,
            )
        )
        for i in model.sample:
            self.add_item_to_model(i.unique_name, key)
    return r_list

assign_material_to_layer(material_id, layer_id)

Assign a material to a layer.

Parameters:

Name Type Description Default
material_id str

The material name.

required
layer_id str

The layer name.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
132
133
134
135
136
137
138
139
140
141
142
def assign_material_to_layer(self, material_id: str, layer_id: str) -> None:
    """Assign a material to a layer.

    Parameters
    ----------
    material_id : str
        The material name.
    layer_id : str
        The layer name.
    """
    self._wrapper.assign_material_to_layer(material_id, layer_id)

add_layer_to_item(layer_id, item_id)

Add a layer to the item stack.

Parameters:

Name Type Description Default
item_id str

The item id.

required
layer_id str

The layer id.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
144
145
146
147
148
149
150
151
152
153
154
def add_layer_to_item(self, layer_id: str, item_id: str) -> None:
    """Add a layer to the item stack.

    Parameters
    ----------
    item_id : str
        The item id.
    layer_id : str
        The layer id.
    """
    self._wrapper.add_layer_to_item(layer_id, item_id)

remove_layer_from_item(layer_id, item_id)

Remove a layer from an item stack.

Parameters:

Name Type Description Default
item_id str

The item id.

required
layer_id str

The layer id.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
156
157
158
159
160
161
162
163
164
165
166
def remove_layer_from_item(self, layer_id: str, item_id: str) -> None:
    """Remove a layer from an item stack.

    Parameters
    ----------
    item_id : str
        The item id.
    layer_id : str
        The layer id.
    """
    self._wrapper.remove_layer_from_item(layer_id, item_id)

add_item_to_model(item_id, model_id)

Add a layer to the item stack.

Parameters:

Name Type Description Default
item_id str

The item id.

required
model_id str

The model id.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
168
169
170
171
172
173
174
175
176
177
178
def add_item_to_model(self, item_id: str, model_id: str) -> None:
    """Add a layer to the item stack.

    Parameters
    ----------
    item_id : str
        The item id.
    model_id : str
        The model id.
    """
    self._wrapper.add_item(item_id, model_id)

remove_item_from_model(item_id, model_id)

Remove an item from the model.

Parameters:

Name Type Description Default
item_id str

The item id.

required
model_id str

The model id.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
180
181
182
183
184
185
186
187
188
189
190
def remove_item_from_model(self, item_id: str, model_id: str) -> None:
    """Remove an item from the model.

    Parameters
    ----------
    item_id : str
        The item id.
    model_id : str
        The model id.
    """
    self._wrapper.remove_item(item_id, model_id)

reflectity_profile(x_array, model_id)

Determines the reflectivity profile for the given range and model.

Parameters:

Name Type Description Default
x_array np.ndarray

Points to be calculated at.

required
model_id str

The model id.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
192
193
194
195
196
197
198
199
200
201
202
def reflectity_profile(self, x_array: np.ndarray, model_id: str) -> np.ndarray:
    """Determines the reflectivity profile for the given range and model.

    Parameters
    ----------
    x_array : np.ndarray
        Points to be calculated at.
    model_id : str
        The model id.
    """
    return self._wrapper.calculate(x_array, model_id)

reflectivity_profile_channel(x_array, model_id, channel)

Determine the reflectivity profile of one explicit spin channel.

Unlike polarization_channel (global calculator state), the channel is an argument, so several channels can be evaluated against the same model — one per dataset in a simultaneous multi-channel fit.

Parameters:

Name Type Description Default
x_array np.ndarray

Points to be calculated at.

required
model_id str

The model id.

required
channel PolarizationChannel | str

One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).

required

Returns:

Type Description
np.ndarray

Reflectivity of the requested channel at q.

Source code in src/easyreflectometry/calculators/calculator_base.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def reflectivity_profile_channel(self, x_array: np.ndarray, model_id: str, channel) -> np.ndarray:
    """Determine the reflectivity profile of one explicit spin channel.

    Unlike `polarization_channel` (global calculator state), the channel is an
    argument, so several channels can be evaluated against the same model —
    one per dataset in a simultaneous multi-channel fit.

    Parameters
    ----------
    x_array : np.ndarray
        Points to be calculated at.
    model_id : str
        The model id.
    channel : PolarizationChannel | str
        One of 'pp', 'pm', 'mp', 'mm' (or the corresponding enum member).

    Returns
    -------
    np.ndarray
        Reflectivity of the requested channel at q.
    """
    return self._wrapper.calculate_channel(x_array, model_id, channel)

polarized_reflectivity_profiles(x_array, model_id)

Determines the reflectivity profiles of all four spin channels for the given range and model.

Requires include_magnetism to be enabled and a calculator that supports it (refl1d).

Parameters:

Name Type Description Default
x_array np.ndarray

Points to be calculated at.

required
model_id str

The model id.

required

Returns:

Type Description
dict[str, np.ndarray]

Reflectivity per spin channel, keyed 'pp', 'pm', 'mp', 'mm' (in that order).

Source code in src/easyreflectometry/calculators/calculator_base.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def polarized_reflectivity_profiles(self, x_array: np.ndarray, model_id: str) -> dict[str, np.ndarray]:
    """Determines the reflectivity profiles of all four spin channels for the given range and model.

    Requires `include_magnetism` to be enabled and a calculator that supports it (refl1d).

    Parameters
    ----------
    x_array : np.ndarray
        Points to be calculated at.
    model_id : str
        The model id.

    Returns
    -------
    dict[str, np.ndarray]
        Reflectivity per spin channel, keyed 'pp', 'pm', 'mp', 'mm' (in that order).
    """
    return self._wrapper.calculate_polarized(x_array, model_id)

sld_profile(model_id)

Return the scattering length density profile.

Parameters:

Name Type Description Default
model_id str

The model id.

required

Returns:

Type Description
tuple[np.ndarray, np.ndarray]

z and sld(z).

Source code in src/easyreflectometry/calculators/calculator_base.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def sld_profile(self, model_id: str) -> tuple[np.ndarray, np.ndarray]:
    """Return the scattering length density profile.

    Parameters
    ----------
    model_id : str
        The model id.

    Returns
    -------
    tuple[np.ndarray, np.ndarray]
        z and sld(z).
    """
    return self._wrapper.sld_profile(model_id)

magnetic_sld_profile(model_id)

Return the nuclear and magnetic scattering length density profiles.

Requires include_magnetism to be enabled and a calculator that supports it (refl1d).

Parameters:

Name Type Description Default
model_id str

The model id.

required

Returns:

Type Description
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]

z, sld(z), magnetic sld rhoM(z) and magnetic angle thetaM(z).

Source code in src/easyreflectometry/calculators/calculator_base.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def magnetic_sld_profile(self, model_id: str) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Return the nuclear and magnetic scattering length density profiles.

    Requires `include_magnetism` to be enabled and a calculator that supports it (refl1d).

    Parameters
    ----------
    model_id : str
        The model id.

    Returns
    -------
    tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
        z, sld(z), magnetic sld rhoM(z) and magnetic angle thetaM(z).
    """
    return self._wrapper.magnetic_sld_profile(model_id)

set_resolution_function(resolution_function)

Set resolution function.

Source code in src/easyreflectometry/calculators/calculator_base.py
278
279
280
def set_resolution_function(self, resolution_function: Callable[[np.array], np.array]) -> None:
    """Set resolution function."""
    return self._wrapper.set_resolution_function(resolution_function)

remove_layer_magnetism(layer_id)

Remove the magnetic state of one layer; disables include_magnetism when no magnetic layer is left.

Parameters:

Name Type Description Default
layer_id str

The layer id.

required
Source code in src/easyreflectometry/calculators/calculator_base.py
287
288
289
290
291
292
293
294
295
296
def remove_layer_magnetism(self, layer_id: str) -> None:
    """Remove the magnetic state of one layer; disables `include_magnetism`
    when no magnetic layer is left.

    Parameters
    ----------
    layer_id : str
        The layer id.
    """
    self._wrapper.remove_layer_magnetism(layer_id)