Skip to content

sample

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.

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

RepeatingMultilayer

Bases: Multilayer

A repeating multi layer is build from a Multilayer and which it repeats for a given number of times. This enables a computational efficiency in many reflectometry engines as the operation can be performed for a single Multilayer and cheaply combined for the appropriate number of repetitions.

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

.. _repeating multilayer documentation: ../sample/assemblies_library.html#repeatingmultilayer

Source code in src/easyreflectometry/sample/assemblies/repeating_multilayer.py
 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
class RepeatingMultilayer(Multilayer):
    """A repeating multi layer is build from a `Multilayer` and which it repeats
    for a given number of times. This enables a computational efficiency in many
    reflectometry engines as the operation can be performed for a single
    `Multilayer` and cheaply combined for the appropriate number of
    `repetitions`.

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

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

    def __init__(
        self,
        layers: Union[LayerCollection, Layer, list[Layer], None] = None,
        repetitions: Union[Parameter, int, None] = None,
        name: str = 'EasyRepeatingMultilayer',
        unique_name: Optional[str] = None,
        interface=None,
        populate_if_none: bool = True,
        conformal_thickness: bool = False,
        conformal_roughness: bool = False,
    ):
        """Constructor.

        Parameters
        ----------
        populate_if_none : bool, optional
            By default, True.
        unique_name : Optional[str], optional
            By default, None.
        layers : Union[LayerCollection, Layer, list[Layer], None], optional
            The layers that make up the multi-layer that will be repeated. By default, None.
        repetitions : Union[Parameter, int, None], optional
            Number of repetitions of the given series of layers. By default, None.
        name : str, optional
            Name for the repeating multi layer. By default, 'EasyRepeatingMultilayer'.
        interface :
            Calculator interface. By default, None.
        conformal_thickness : bool, optional
            Tie every layer's thickness to the front layer's (persisted). By default, False.
        conformal_roughness : bool, optional
            Tie every layer's roughness to the front layer's (persisted). By default, False.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

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

        repetitions = get_as_parameter(
            name='repetitions',
            value=repetitions,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Repetitions',
        )

        super().__init__(
            layers=layers,
            name=name,
            unique_name=unique_name,
            interface=None,
            type='Repeating Multi-layer',
            populate_if_none=False,
            conformal_thickness=conformal_thickness,
            conformal_roughness=conformal_roughness,
        )
        self._repetitions = repetitions

        if interface is not None:
            self.interface = interface

    @property
    def repetitions(self) -> Parameter:
        return self._repetitions

    @repetitions.setter
    def repetitions(self, value) -> None:
        self._repetitions.value = value

    # Representation
    @property
    def _dict_repr(self) -> dict:
        """A simplified dict representation."""
        d_dict = {self.name: self.layers._dict_repr}
        d_dict[self.name]['repetitions'] = float(self.repetitions.value)
        return d_dict

__init__(layers=None, repetitions=None, name='EasyRepeatingMultilayer', unique_name=None, interface=None, populate_if_none=True, conformal_thickness=False, conformal_roughness=False)

Constructor.

Parameters:

Name Type Description Default
populate_if_none bool

By default, True.

True
unique_name Optional[str]

By default, None.

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

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

None
repetitions Union[Parameter, int, None]

Number of repetitions of the given series of layers. By default, None.

None
name str

Name for the repeating multi layer. By default, 'EasyRepeatingMultilayer'.

'EasyRepeatingMultilayer'
interface

Calculator interface. By default, None.

None
conformal_thickness bool

Tie every layer's thickness to the front layer's (persisted). By default, False.

False
conformal_roughness bool

Tie every layer's roughness to the front layer's (persisted). By default, False.

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

    Parameters
    ----------
    populate_if_none : bool, optional
        By default, True.
    unique_name : Optional[str], optional
        By default, None.
    layers : Union[LayerCollection, Layer, list[Layer], None], optional
        The layers that make up the multi-layer that will be repeated. By default, None.
    repetitions : Union[Parameter, int, None], optional
        Number of repetitions of the given series of layers. By default, None.
    name : str, optional
        Name for the repeating multi layer. By default, 'EasyRepeatingMultilayer'.
    interface :
        Calculator interface. By default, None.
    conformal_thickness : bool, optional
        Tie every layer's thickness to the front layer's (persisted). By default, False.
    conformal_roughness : bool, optional
        Tie every layer's roughness to the front layer's (persisted). By default, False.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

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

    repetitions = get_as_parameter(
        name='repetitions',
        value=repetitions,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Repetitions',
    )

    super().__init__(
        layers=layers,
        name=name,
        unique_name=unique_name,
        interface=None,
        type='Repeating Multi-layer',
        populate_if_none=False,
        conformal_thickness=conformal_thickness,
        conformal_roughness=conformal_roughness,
    )
    self._repetitions = repetitions

    if interface is not None:
        self.interface = interface

SurfactantLayer

Bases: BaseAssembly

A surfactant layer constructs a series of layers representing the head and tail groups of a surfactant. This assembly allows the definition of a surfactant or lipid using the chemistry of the head (head_layer) and tail (tail_layer) regions, additionally this approach will make the application of constraints such as conformal roughness or area per molecule more straight forward.

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

.. _surfactant documentation: ../sample/assemblies_library.html#surfactantlayer

Source code in src/easyreflectometry/sample/assemblies/surfactant_layer.py
 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
class SurfactantLayer(BaseAssembly):
    """A surfactant layer constructs a series of layers representing the
    head and tail groups of a surfactant.
    This assembly allows the definition of a surfactant or lipid using the chemistry
    of the head (head_layer) and tail (tail_layer) regions, additionally
    this approach will make the application of constraints such as conformal roughness
    or area per molecule more straight forward.

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

    .. _`surfactant documentation`: ../sample/assemblies_library.html#surfactantlayer
    """

    def __init__(
        self,
        tail_layer: Optional[LayerAreaPerMolecule] = None,
        head_layer: Optional[LayerAreaPerMolecule] = None,
        name: str = 'EasySurfactantLayer',
        unique_name: Optional[str] = None,
        constrain_area_per_molecule: bool = False,
        conformal_roughness: bool = False,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        tail_layer : Optional[LayerAreaPerMolecule], optional
            Layer representing the tail part of the surfactant layer. By default, None.
        head_layer : Optional[LayerAreaPerMolecule], optional
            Layer representing the head part of the surfactant layer. By default, None.
        name : str, optional
            Name for surfactant layer. By default, 'EasySurfactantLayer'.
        constrain_area_per_molecule : bool, optional
            Constrain the area per molecule. By default, False.
        conformal_roughness : bool, optional
            Constrain the roughness to be the same for both layers. By default, False.
        interface :
            Calculator interface. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        if tail_layer is None:
            air = Material(
                sld=0,
                isld=0,
                name='Air',
                unique_name=unique_name + '_MaterialTail',
                interface=interface,
            )
            tail_layer = LayerAreaPerMolecule(
                molecular_formula='C32D64',
                thickness=16,
                solvent=air,
                solvent_fraction=0.0,
                area_per_molecule=48.2,
                roughness=3,
                name='DPPC Tail',
                unique_name=unique_name + '_LayerAreaPerMoleculeTail',
                interface=interface,
            )
        if head_layer is None:
            d2o = Material(
                sld=6.36,
                isld=0,
                name='D2O',
                unique_name=unique_name + '_MaterialHead',
                interface=interface,
            )
            head_layer = LayerAreaPerMolecule(
                molecular_formula='C10H18NO8P',
                thickness=10.0,
                solvent=d2o,
                solvent_fraction=0.2,
                area_per_molecule=48.2,
                roughness=3.0,
                name='DPPC Head',
                unique_name=unique_name + '_LayerAreaPerMoleculeHead',
                interface=interface,
            )
        surfactant = LayerCollection(
            tail_layer,
            head_layer,
            name='Layers',
            unique_name=unique_name + '_LayerCollection',
            interface=interface,
        )
        super().__init__(
            name=name,
            unique_name=unique_name,
            type='Surfactant Layer',
            layers=surfactant,
            interface=interface,
        )

        self.conformal = False

        if constrain_area_per_molecule:
            self.constrain_area_per_molecule = True
        if conformal_roughness:
            self._enable_roughness_constraints()
            self.conformal = True

    @property
    def tail_layer(self) -> Optional[LayerAreaPerMolecule]:
        """Get the tail layer of the surfactant surface."""
        return self.front_layer

    @tail_layer.setter
    def tail_layer(self, layer: LayerAreaPerMolecule) -> None:
        """Set the tail layer of the surfactant surface."""
        self.front_layer = layer

    @property
    def head_layer(self) -> Optional[LayerAreaPerMolecule]:
        """Get the head layer of the surfactant surface."""
        return self.back_layer

    @head_layer.setter
    def head_layer(self, layer: LayerAreaPerMolecule) -> None:
        """Set the head layer of the surfactant surface."""
        self.back_layer = layer

    @property
    def constrain_area_per_molecule(self) -> bool:
        """Get the area per molecule constraint status."""
        constrained = not self.head_layer._area_per_molecule.independent
        return constrained

    @constrain_area_per_molecule.setter
    def constrain_area_per_molecule(self, status: bool):
        """Set the status for the area per molecule constraint such that the head and tail layers have the
        same area per molecule.

        Parameters
        ----------
        status : bool
            Boolean description the wanted of the constraint.
        """
        if status:
            independent_param = self.tail_layer._area_per_molecule
            self.head_layer._area_per_molecule.make_dependent_on(
                dependency_expression='a', dependency_map={'a': independent_param}
            )
        else:
            self.head_layer._area_per_molecule.make_independent()
        return

    @property
    def conformal_roughness(self) -> bool:
        """Get the roughness constraint status."""
        return self.conformal

    @conformal_roughness.setter
    def conformal_roughness(self, status: bool):
        """Set the status for the roughness to be the same for both layers.

        Parameters
        ----------
        status : bool
            Boolean description the wanted of the constraint.
        """
        if status:
            self._enable_roughness_constraints()
            self.conformal = True
        else:
            self._disable_roughness_constraints()
            self.conformal = False

    def constrain_solvent_roughness(self, solvent_roughness: Parameter):
        """Add the constraint to the solvent roughness.

        Parameters
        ----------
        solvent_roughness : Parameter
            The solvent roughness parameter.
        """
        if not self.conformal_roughness:
            raise ValueError('Roughness must be conformal to use this function.')
        solvent_roughness.value = self.tail_layer.roughness.value
        solvent_roughness.make_dependent_on(dependency_expression='a', dependency_map={'a': self.tail_layer.roughness})

    def constrain_multiple_contrast(
        self,
        another_contrast: SurfactantLayer,
        head_layer_thickness: bool = True,
        tail_layer_thickness: bool = True,
        head_layer_area_per_molecule: bool = True,
        tail_layer_area_per_molecule: bool = True,
        head_layer_fraction: bool = True,
        tail_layer_fraction: bool = True,
    ):
        """Constrain structural parameters between surfactant layer objects.

        Parameters
        ----------
        tail_layer_fraction : bool, optional
            By default, True.
        head_layer_fraction : bool, optional
            By default, True.
        tail_layer_area_per_molecule : bool, optional
            By default, True.
        head_layer_area_per_molecule : bool, optional
            By default, True.
        tail_layer_thickness : bool, optional
            By default, True.
        head_layer_thickness : bool, optional
            By default, True.
        another_contrast : SurfactantLayer
            The surfactant layer to constrain.
        """
        if head_layer_thickness:
            constrain_equal(self.head_layer.thickness, another_contrast.head_layer.thickness)

        if tail_layer_thickness:
            constrain_equal(self.tail_layer.thickness, another_contrast.tail_layer.thickness)

        if head_layer_area_per_molecule:
            constrain_equal(self.head_layer._area_per_molecule, another_contrast.head_layer._area_per_molecule)

        if tail_layer_area_per_molecule:
            constrain_equal(self.tail_layer._area_per_molecule, another_contrast.tail_layer._area_per_molecule)

        if head_layer_fraction:
            constrain_equal(self.head_layer.material._fraction, another_contrast.head_layer.material._fraction)

        if tail_layer_fraction:
            constrain_equal(self.tail_layer.material._fraction, another_contrast.tail_layer.material._fraction)

    @property
    def _dict_repr(self) -> dict:
        """A simplified dict representation."""
        return {
            self.name: {
                'head_layer': self.head_layer._dict_repr,
                'tail_layer': self.tail_layer._dict_repr,
                'area per molecule constrained': self.constrain_area_per_molecule,
                'conformal roughness': self.conformal_roughness,
            }
        }

    def to_dict(self, skip: Optional[list[str]] = None) -> dict:
        """Serialize, dropping the derived ``layers`` field (it is rebuilt
        from ``tail_layer`` and ``head_layer`` in ``__init__``).
        """
        this_dict = super().to_dict(skip=skip)
        this_dict.pop('layers', None)
        return this_dict

    def as_dict(self, skip: Optional[list[str]] = None) -> dict:
        return self.to_dict(skip=skip)

constrain_area_per_molecule property writable

Get the area per molecule constraint status.

tail_layer property writable

Get the tail layer of the surfactant surface.

head_layer property writable

Get the head layer of the surfactant surface.

conformal_roughness property writable

Get the roughness constraint status.

__init__(tail_layer=None, head_layer=None, name='EasySurfactantLayer', unique_name=None, constrain_area_per_molecule=False, conformal_roughness=False, interface=None)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
tail_layer Optional[LayerAreaPerMolecule]

Layer representing the tail part of the surfactant layer. By default, None.

None
head_layer Optional[LayerAreaPerMolecule]

Layer representing the head part of the surfactant layer. By default, None.

None
name str

Name for surfactant layer. By default, 'EasySurfactantLayer'.

'EasySurfactantLayer'
constrain_area_per_molecule bool

Constrain the area per molecule. By default, False.

False
conformal_roughness bool

Constrain the roughness to be the same for both layers. By default, False.

False
interface

Calculator interface. By default, None.

None
Source code in src/easyreflectometry/sample/assemblies/surfactant_layer.py
 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
def __init__(
    self,
    tail_layer: Optional[LayerAreaPerMolecule] = None,
    head_layer: Optional[LayerAreaPerMolecule] = None,
    name: str = 'EasySurfactantLayer',
    unique_name: Optional[str] = None,
    constrain_area_per_molecule: bool = False,
    conformal_roughness: bool = False,
    interface=None,
):
    """Constructor.

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    tail_layer : Optional[LayerAreaPerMolecule], optional
        Layer representing the tail part of the surfactant layer. By default, None.
    head_layer : Optional[LayerAreaPerMolecule], optional
        Layer representing the head part of the surfactant layer. By default, None.
    name : str, optional
        Name for surfactant layer. By default, 'EasySurfactantLayer'.
    constrain_area_per_molecule : bool, optional
        Constrain the area per molecule. By default, False.
    conformal_roughness : bool, optional
        Constrain the roughness to be the same for both layers. By default, False.
    interface :
        Calculator interface. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    if tail_layer is None:
        air = Material(
            sld=0,
            isld=0,
            name='Air',
            unique_name=unique_name + '_MaterialTail',
            interface=interface,
        )
        tail_layer = LayerAreaPerMolecule(
            molecular_formula='C32D64',
            thickness=16,
            solvent=air,
            solvent_fraction=0.0,
            area_per_molecule=48.2,
            roughness=3,
            name='DPPC Tail',
            unique_name=unique_name + '_LayerAreaPerMoleculeTail',
            interface=interface,
        )
    if head_layer is None:
        d2o = Material(
            sld=6.36,
            isld=0,
            name='D2O',
            unique_name=unique_name + '_MaterialHead',
            interface=interface,
        )
        head_layer = LayerAreaPerMolecule(
            molecular_formula='C10H18NO8P',
            thickness=10.0,
            solvent=d2o,
            solvent_fraction=0.2,
            area_per_molecule=48.2,
            roughness=3.0,
            name='DPPC Head',
            unique_name=unique_name + '_LayerAreaPerMoleculeHead',
            interface=interface,
        )
    surfactant = LayerCollection(
        tail_layer,
        head_layer,
        name='Layers',
        unique_name=unique_name + '_LayerCollection',
        interface=interface,
    )
    super().__init__(
        name=name,
        unique_name=unique_name,
        type='Surfactant Layer',
        layers=surfactant,
        interface=interface,
    )

    self.conformal = False

    if constrain_area_per_molecule:
        self.constrain_area_per_molecule = True
    if conformal_roughness:
        self._enable_roughness_constraints()
        self.conformal = True

constrain_solvent_roughness(solvent_roughness)

Add the constraint to the solvent roughness.

Parameters:

Name Type Description Default
solvent_roughness Parameter

The solvent roughness parameter.

required
Source code in src/easyreflectometry/sample/assemblies/surfactant_layer.py
192
193
194
195
196
197
198
199
200
201
202
203
def constrain_solvent_roughness(self, solvent_roughness: Parameter):
    """Add the constraint to the solvent roughness.

    Parameters
    ----------
    solvent_roughness : Parameter
        The solvent roughness parameter.
    """
    if not self.conformal_roughness:
        raise ValueError('Roughness must be conformal to use this function.')
    solvent_roughness.value = self.tail_layer.roughness.value
    solvent_roughness.make_dependent_on(dependency_expression='a', dependency_map={'a': self.tail_layer.roughness})

constrain_multiple_contrast(another_contrast, head_layer_thickness=True, tail_layer_thickness=True, head_layer_area_per_molecule=True, tail_layer_area_per_molecule=True, head_layer_fraction=True, tail_layer_fraction=True)

Constrain structural parameters between surfactant layer objects.

Parameters:

Name Type Description Default
tail_layer_fraction bool

By default, True.

True
head_layer_fraction bool

By default, True.

True
tail_layer_area_per_molecule bool

By default, True.

True
head_layer_area_per_molecule bool

By default, True.

True
tail_layer_thickness bool

By default, True.

True
head_layer_thickness bool

By default, True.

True
another_contrast SurfactantLayer

The surfactant layer to constrain.

required
Source code in src/easyreflectometry/sample/assemblies/surfactant_layer.py
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
def constrain_multiple_contrast(
    self,
    another_contrast: SurfactantLayer,
    head_layer_thickness: bool = True,
    tail_layer_thickness: bool = True,
    head_layer_area_per_molecule: bool = True,
    tail_layer_area_per_molecule: bool = True,
    head_layer_fraction: bool = True,
    tail_layer_fraction: bool = True,
):
    """Constrain structural parameters between surfactant layer objects.

    Parameters
    ----------
    tail_layer_fraction : bool, optional
        By default, True.
    head_layer_fraction : bool, optional
        By default, True.
    tail_layer_area_per_molecule : bool, optional
        By default, True.
    head_layer_area_per_molecule : bool, optional
        By default, True.
    tail_layer_thickness : bool, optional
        By default, True.
    head_layer_thickness : bool, optional
        By default, True.
    another_contrast : SurfactantLayer
        The surfactant layer to constrain.
    """
    if head_layer_thickness:
        constrain_equal(self.head_layer.thickness, another_contrast.head_layer.thickness)

    if tail_layer_thickness:
        constrain_equal(self.tail_layer.thickness, another_contrast.tail_layer.thickness)

    if head_layer_area_per_molecule:
        constrain_equal(self.head_layer._area_per_molecule, another_contrast.head_layer._area_per_molecule)

    if tail_layer_area_per_molecule:
        constrain_equal(self.tail_layer._area_per_molecule, another_contrast.tail_layer._area_per_molecule)

    if head_layer_fraction:
        constrain_equal(self.head_layer.material._fraction, another_contrast.head_layer.material._fraction)

    if tail_layer_fraction:
        constrain_equal(self.tail_layer.material._fraction, another_contrast.tail_layer.material._fraction)

to_dict(skip=None)

Serialize, dropping the derived layers field (it is rebuilt from tail_layer and head_layer in __init__).

Source code in src/easyreflectometry/sample/assemblies/surfactant_layer.py
264
265
266
267
268
269
270
def to_dict(self, skip: Optional[list[str]] = None) -> dict:
    """Serialize, dropping the derived ``layers`` field (it is rebuilt
    from ``tail_layer`` and ``head_layer`` in ``__init__``).
    """
    this_dict = super().to_dict(skip=skip)
    this_dict.pop('layers', None)
    return this_dict

Layer

Bases: BaseCore

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

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

        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        thickness_value = thickness
        thickness = get_as_parameter(
            name='thickness',
            value=thickness,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Thickness',
        )
        thickness.default_limits_pending = not isinstance(thickness_value, Parameter)

        roughness_value = roughness
        roughness = get_as_parameter(
            name='roughness',
            value=roughness,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Roughness',
        )
        roughness.default_limits_pending = not isinstance(roughness_value, Parameter)

        super().__init__(name=name, unique_name=unique_name)
        self._material = material
        self._thickness = thickness
        self._roughness = roughness
        self._magnetism = magnetism
        self._rho_m_coupled = False
        if rho_m_coupled:
            self.rho_m_coupled = True

        if interface is not None:
            self.interface = interface

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

rho_m_coupled property writable

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

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

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

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

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
material Union[Material, None]

The material for the layer. By default, None.

None
thickness Union[Parameter, float, None]

Layer thickness in Angstrom. By default, None.

None
roughness Union[Parameter, float, None]

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

None
magnetism Union[LayerMagnetism, None]

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

None
rho_m_coupled bool

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

False
name str

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

'EasyLayer'
interface

Interface object. By default, None.

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

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

    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    thickness_value = thickness
    thickness = get_as_parameter(
        name='thickness',
        value=thickness,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Thickness',
    )
    thickness.default_limits_pending = not isinstance(thickness_value, Parameter)

    roughness_value = roughness
    roughness = get_as_parameter(
        name='roughness',
        value=roughness,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Roughness',
    )
    roughness.default_limits_pending = not isinstance(roughness_value, Parameter)

    super().__init__(name=name, unique_name=unique_name)
    self._material = material
    self._thickness = thickness
    self._roughness = roughness
    self._magnetism = magnetism
    self._rho_m_coupled = False
    if rho_m_coupled:
        self.rho_m_coupled = True

    if interface is not None:
        self.interface = interface

interface(new_interface)

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

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

assign_material(material)

Assign a material to the layer interface.

Parameters:

Name Type Description Default
material Material

The material to assign to the layer.

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

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

VolumeFractionProfile dataclass

Volume fraction of every component of a sample versus depth.

Attributes:

Name Type Description
z np.ndarray

Depth in Å; z = 0 at the superphase / first layer interface.

fractions dict

Component key -> phi_c(z). Keys are stable identifiers (the unique_name of the material, or of the first member of a merged group), in order of first appearance from the front of the sample.

labels dict

Component key -> display label, unique within the profile.

Source code in src/easyreflectometry/sample/volume_fraction.py
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
@dataclass
class VolumeFractionProfile:
    """Volume fraction of every component of a sample versus depth.

    Attributes
    ----------
    z : np.ndarray
        Depth in Å; ``z = 0`` at the superphase / first layer interface.
    fractions : dict
        Component key -> ``phi_c(z)``. Keys are stable identifiers (the
        ``unique_name`` of the material, or of the first member of a merged
        group), in order of first appearance from the front of the sample.
    labels : dict
        Component key -> display label, unique within the profile.
    """

    z: np.ndarray
    fractions: dict
    labels: dict

    @property
    def components(self) -> list:
        """The component keys, front to back."""
        return list(self.fractions)

    @property
    def total(self) -> np.ndarray:
        """``sum_c phi_c(z)``; ones up to floating point error."""
        if not self.fractions:
            return np.zeros_like(self.z)
        return np.sum(np.array(list(self.fractions.values())), axis=0)

    def grouped(self, groups: Mapping[str, Sequence[str]]) -> 'VolumeFractionProfile':
        """Merge components into named groups for presentation.

        Parameters
        ----------
        groups : Mapping[str, Sequence[str]]
            Group label -> members, given as component keys or labels, e.g.
            ``{'Heads': ['C10H18NO8P'], 'Water': ['D2O', 'H2O']}``. Each
            component may appear at most once over all groups. Components not
            listed keep their own trace. The result lists the groups first, in
            the given order, then the remaining components in their original
            order.

        Returns
        -------
        VolumeFractionProfile
            A new profile; this one is left unchanged.

        Raises
        ------
        ValueError
            A member is unknown, a component is listed twice, a group is
            empty, or a group label collides with a retained component.
        """
        label_to_key = {label: key for key, label in self.labels.items()}
        used: dict = {}
        fractions: dict = {}
        labels: dict = {}
        for group_label, members in groups.items():
            keys = []
            for member in members:
                key = member if member in self.fractions else label_to_key.get(member)
                if key is None:
                    raise ValueError(
                        f'Unknown component {member!r} in group {group_label!r}. '
                        f'Available keys: {list(self.fractions)}; labels: {list(self.labels.values())}.'
                    )
                if key in used:
                    raise ValueError(
                        f'Component {self.labels[key]!r} is listed in group {group_label!r} '
                        f'and already in group {used[key]!r}; groups must not overlap.'
                    )
                used[key] = group_label
                keys.append(key)
            if not keys:
                raise ValueError(f'Group {group_label!r} has no members.')
            fractions[group_label] = np.sum(np.array([self.fractions[key] for key in keys]), axis=0)
            labels[group_label] = group_label
        for key, values in self.fractions.items():
            if key in used:
                continue
            label = self.labels[key]
            if key in fractions or label in labels.values():
                raise ValueError(
                    f'Group label {label!r} collides with the retained component {label!r}; '
                    'include that component in the group or rename the group.'
                )
            fractions[key] = values.copy()
            labels[key] = label
        return VolumeFractionProfile(z=self.z.copy(), fractions=fractions, labels=labels)

components property

The component keys, front to back.

total property

sum_c phi_c(z); ones up to floating point error.

grouped(groups)

Merge components into named groups for presentation.

Parameters:

Name Type Description Default
groups Mapping[str, Sequence[str]]

Group label -> members, given as component keys or labels, e.g. {'Heads': ['C10H18NO8P'], 'Water': ['D2O', 'H2O']}. Each component may appear at most once over all groups. Components not listed keep their own trace. The result lists the groups first, in the given order, then the remaining components in their original order.

required

Returns:

Type Description
VolumeFractionProfile

A new profile; this one is left unchanged.

Raises:

Type Description
ValueError

A member is unknown, a component is listed twice, a group is empty, or a group label collides with a retained component.

Source code in src/easyreflectometry/sample/volume_fraction.py
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
def grouped(self, groups: Mapping[str, Sequence[str]]) -> 'VolumeFractionProfile':
    """Merge components into named groups for presentation.

    Parameters
    ----------
    groups : Mapping[str, Sequence[str]]
        Group label -> members, given as component keys or labels, e.g.
        ``{'Heads': ['C10H18NO8P'], 'Water': ['D2O', 'H2O']}``. Each
        component may appear at most once over all groups. Components not
        listed keep their own trace. The result lists the groups first, in
        the given order, then the remaining components in their original
        order.

    Returns
    -------
    VolumeFractionProfile
        A new profile; this one is left unchanged.

    Raises
    ------
    ValueError
        A member is unknown, a component is listed twice, a group is
        empty, or a group label collides with a retained component.
    """
    label_to_key = {label: key for key, label in self.labels.items()}
    used: dict = {}
    fractions: dict = {}
    labels: dict = {}
    for group_label, members in groups.items():
        keys = []
        for member in members:
            key = member if member in self.fractions else label_to_key.get(member)
            if key is None:
                raise ValueError(
                    f'Unknown component {member!r} in group {group_label!r}. '
                    f'Available keys: {list(self.fractions)}; labels: {list(self.labels.values())}.'
                )
            if key in used:
                raise ValueError(
                    f'Component {self.labels[key]!r} is listed in group {group_label!r} '
                    f'and already in group {used[key]!r}; groups must not overlap.'
                )
            used[key] = group_label
            keys.append(key)
        if not keys:
            raise ValueError(f'Group {group_label!r} has no members.')
        fractions[group_label] = np.sum(np.array([self.fractions[key] for key in keys]), axis=0)
        labels[group_label] = group_label
    for key, values in self.fractions.items():
        if key in used:
            continue
        label = self.labels[key]
        if key in fractions or label in labels.values():
            raise ValueError(
                f'Group label {label!r} collides with the retained component {label!r}; '
                'include that component in the group or rename the group.'
            )
        fractions[key] = values.copy()
        labels[key] = label
    return VolumeFractionProfile(z=self.z.copy(), fractions=fractions, labels=labels)

BaseCollection

Bases: EasyList

Local base for sample-tree collections (Material/Layer/Assembly/Model collections).

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

  • a name property
  • an interface property whose setter propagates the calculator interface to every contained item
  • propagation of the current interface to newly inserted items
  • a populate_if_none flag preserved for serialization round-trip
  • convenience helpers names, move_up, move_down, remove_at
  • a yaml-formatted __repr__ driven by _dict_repr
  • an as_dict alias for to_dict with skip= support and interface excluded by default

Subclasses (LayerCollection, MaterialCollection, Sample, ModelCollection) keep their existing constructor shape — they pass name and interface positionally to this class, items as *args, and additional configuration as kwargs.

Source code in src/easyreflectometry/sample/collections/base_collection.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
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
class BaseCollection(EasyList):
    """Local base for sample-tree collections (Material/Layer/Assembly/Model collections).

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

    - a `name` property
    - an `interface` property whose setter propagates the calculator interface
      to every contained item
    - propagation of the current `interface` to newly inserted items
    - a `populate_if_none` flag preserved for serialization round-trip
    - convenience helpers `names`, `move_up`, `move_down`, `remove_at`
    - a yaml-formatted `__repr__` driven by `_dict_repr`
    - an `as_dict` alias for `to_dict` with `skip=` support and `interface`
      excluded by default

    Subclasses (`LayerCollection`, `MaterialCollection`, `Sample`,
    `ModelCollection`) keep their existing constructor shape — they pass
    `name` and `interface` positionally to this class, items as `*args`, and
    additional configuration as kwargs.
    """

    def __init__(
        self,
        name: str,
        interface: Any = None,
        *args: Any,
        unique_name: Optional[str] = None,
        populate_if_none: bool = False,
        **kwargs: Any,
    ):
        # `_interface` must exist before `super().__init__` because `EasyList`
        # calls `self.append(item)` for each positional arg, which routes
        # through our `insert` override and reads `self._interface`.
        self._interface = None
        self._name = name
        # Legacy `CollectionBase` accepted items either positionally or as a
        # list-valued keyword (e.g. ``LayerCollection(layers=[a, b])``). Pull
        # any list-valued kwarg into the positional stream so callers using
        # that older pattern keep working. `data` and `protected_types` are
        # `EasyList`'s own list-valued arguments (`from_dict` passes the
        # latter as a list of classes) and must reach it untouched.
        extra_items = []
        for key in list(kwargs.keys()):
            if isinstance(kwargs[key], list) and kwargs[key] and key not in ('data', 'protected_types'):
                extra_items.extend(kwargs.pop(key))
        if extra_items:
            args = tuple(args) + tuple(extra_items)
        super().__init__(*args, unique_name=unique_name, **kwargs)
        # `populate_if_none` is a control flag, not state. It is serialized so
        # `from_dict` knows whether the original construction filled in
        # defaults; the value should be `False` once the data is restored.
        self.populate_if_none = populate_if_none

        # Assign interface LAST — propagates to all contained items.
        if interface is not None:
            self.interface = interface

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

    @property
    def name(self) -> str:
        return self._name

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

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

    @property
    def interface(self) -> Any:
        return self._interface

    @interface.setter
    def interface(self, new_interface: Any) -> None:
        self._interface = new_interface
        if new_interface is None:
            return
        # Propagate to existing items.
        for item in self._data:
            if self._has_interface_setter(type(item)):
                item.interface = new_interface
        # Tell the calculator to bind to self (matches the legacy CollectionBase
        # behavior which called `interface.generate_bindings(self)` once per
        # collection).
        if hasattr(new_interface, 'generate_bindings'):
            new_interface.generate_bindings(self)

    def _get_key(self, obj):
        """Use the item's `name` for string-indexed lookups.

        Matches the legacy `CollectionBase.__getitem__` behaviour which
        searched by `item.name`. `EasyList` defaults to `unique_name`; the
        existing `Project` code (and callers) look items up by their pretty
        name (e.g. `materials['Air']`).
        """
        return getattr(obj, 'name', None) or obj.unique_name

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

    # ----- mutable-sequence overrides that propagate interface -----

    def insert(self, index: int, value: Any) -> None:
        """Insert and (if an interface is set) propagate it to the new item.

        Legacy `CollectionBase.insert` did `value.interface = self.interface`
        after registering the item; we replicate the same behaviour here so
        downstream calculator state stays in sync when items are appended
        after the collection's interface was already set.

        The type check from `EasyList.insert` is bypassed: each subclass
        accepts a single item type (Layer / Material / BaseAssembly / Model)
        and enforces it elsewhere, while the EasyList check would require
        every item to be a `NewBase` subclass — which was historically not
        guaranteed and forces an extra coupling we don't need.
        """
        if not isinstance(index, int):
            raise TypeError('Index must be an integer')
        # Skip the EasyList protected-types check; mimic the rest of its insert
        # (duplicate-name warning + append-to-_data).
        import warnings as _warnings

        if value in self:
            _warnings.warn(f'Item with unique name "{self._get_key(value)}" already in collection, it will be ignored')
            return
        self._data.insert(index, value)
        if self._interface is not None and self._has_interface_setter(type(value)):
            value.interface = self._interface

    # ----- helpers -----

    @property
    def names(self) -> list:
        """List of item names."""
        return [getattr(item, 'name', None) for item in self._data]

    @property
    def data(self) -> list:
        """Read-only view of the underlying item list.

        Provided for compatibility with code (and tests) written against the
        legacy `CollectionBase` shape, which exposed items via `.data`.
        """
        return list(self._data)

    def move_up(self, index: int) -> None:
        """Move the element at the given index up in the collection."""
        if index == 0:
            return
        self.insert(index - 1, self.pop(index))

    def move_down(self, index: int) -> None:
        """Move the element at the given index down in the collection."""
        if index == len(self) - 1:
            return
        self.insert(index + 1, self.pop(index))

    def remove_at(self, index: int) -> None:
        """Remove the item at *index* from the collection.

        Renamed from the legacy `BaseCollection.remove(index)` which shadowed
        `MutableSequence.remove(value)` (remove-by-value, inherited from
        `EasyList`). Callers that meant "remove by index" should use this; the
        standard `remove(value)` is still available with its usual semantics.
        """
        self.pop(index)

    # ----- compatibility shims (kept until call sites migrate) -----

    def get_parameters(self) -> List[Parameter]:
        """Compatibility alias for legacy callers; prefer `get_all_parameters`."""
        return self.get_all_parameters()

    def get_all_variables(self) -> List:
        """Flat list of every Parameter/Descriptor across all items.

        Walks each item in the collection and unions whatever each item
        exposes via its own `get_all_variables` (for `ModelBase` /
        `BaseCore` children) or, for objects that lack that hook,
        unions any direct `DescriptorBase` attributes.
        """
        from easyscience.variable.descriptor_base import DescriptorBase

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

    def get_all_parameters(self) -> List[Parameter]:
        return [v for v in self.get_all_variables() if isinstance(v, Parameter)]

    def get_free_parameters(self) -> List[Parameter]:
        return [p for p in self.get_all_parameters() if p.independent and not p.fixed]

    def get_fit_parameters(self) -> List[Parameter]:
        """Alias kept for the minimizer; matches `ModelBase.get_fit_parameters`."""
        return self.get_free_parameters()

    def _get_linkable_attributes(self) -> List[Parameter]:
        """Bridge for `easyscience.fitting.calculators.interface_factory.generate_bindings`."""
        return self.get_all_variables()

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

    @property
    def _dict_repr(self) -> dict:
        """A simplified dict representation."""
        return {self.name: [getattr(i, '_dict_repr', repr(i)) for i in self._data]}

    def __repr__(self) -> str:
        try:
            return yaml_dump(self._dict_repr)
        except Exception:
            return super().__repr__()

    # ----- serialization -----

    def _convert_to_dict(self, d: dict, encoder=None, skip: Optional[List[str]] = None, **kwargs) -> dict:
        """Serializer hook used when this collection is encoded as a *child*
        attribute (e.g. `Multilayer.layers`).

        `SerializerBase._convert_to_dict` iterates `_arg_spec` to populate the
        base dict and then calls `obj._convert_to_dict(d, ...)` if defined.
        Because `data` is supplied via `*args` (VAR_POSITIONAL — not part of
        `_arg_spec`), without this hook the nested encoding would miss the
        items entirely and round-trip would reconstruct an empty collection.
        """
        if skip is None:
            skip = []
        if self._protected_types != [NewBase] and 'protected_types' not in d:
            d['protected_types'] = [{'@module': c.__module__, '@class': c.__name__} for c in self._protected_types]
        # Encode each item. Defer to the encoder's recursive walk so nested
        # ModelBase / NewBase items get properly serialized.
        item_skip = list(skip)
        items: list = []
        for item in self._data:
            if encoder is not None and hasattr(encoder, '_recursive_encoder'):
                items.append(encoder._recursive_encoder(item, skip=item_skip, encoder=encoder, full_encode=False))
            elif hasattr(item, 'to_dict'):
                try:
                    items.append(item.to_dict(skip=list(item_skip)))
                except TypeError:
                    items.append(item.to_dict())
            else:
                items.append(item)
        d['data'] = items
        return d

    def to_dict(self, skip: Optional[List[str]] = None) -> dict:
        """Serialize with `skip` support; `interface` excluded by default.

        `EasyList.to_dict` doesn't accept a `skip` argument and is hard-wired
        to dump `data` plus the parent's `_arg_spec` view. We reimplement here
        so existing callers (`Project`, `Model.as_dict`, etc.) can keep
        passing `skip=['unique_name']` or similar.

        ``NewBase.to_dict`` mutates the ``skip`` list in-place (e.g. it
        appends ``'unique_name'`` when the collection's own unique_name is
        default-generated). We therefore pass a *copy* to it, otherwise the
        mutation would leak into the per-item serialization below and force
        every item Parameter dict to drop its ``unique_name`` — breaking the
        from_dict round-trip.
        """
        skip = list(skip or [])
        if 'interface' not in skip:
            skip.append('interface')
        # Matches legacy `BasedBase.as_dict`: drop unique_name from the
        # serialized form so nested Parameters don't get explicit names that
        # would collide with the auto-generated names produced when the global
        # counter restarts during reconstruction.
        if 'unique_name' not in skip:
            skip.append('unique_name')
        # The items this encodes (through `_convert_to_dict`) are replaced
        # below, so they must not be registered as shared-object occurrences.
        with references.suspended():
            dict_repr = NewBase.to_dict(self, skip=list(skip))
        if self._protected_types != [NewBase]:
            dict_repr['protected_types'] = [{'@module': c.__module__, '@class': c.__name__} for c in self._protected_types]
        dict_repr['data'] = []
        for item in self._data:
            # Items that are ModelBase / BaseCore subclasses accept `skip`;
            # other shapes use their no-arg `to_dict`/`as_dict`.
            if hasattr(item, 'to_dict'):
                try:
                    dict_repr['data'].append(item.to_dict(skip=list(skip)))
                except TypeError:
                    dict_repr['data'].append(item.to_dict())
            else:
                dict_repr['data'].append(item.as_dict(skip=list(skip)))
        return dict_repr

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

    @classmethod
    def from_dict(cls, obj_dict: dict) -> BaseCollection:
        """Rebuild the collection, each item through its own class's ``from_dict``.

        ``EasyList.from_dict`` deserializes the ``data`` list with the
        serializer's generic path, which calls every item's constructor with
        the stored keys. An item whose class restores state in ``from_dict``
        (e.g. ``MaterialDensity`` and its ``sld_coupled`` key, or any nested
        item that does) then fails or loses that state. Apart from the items,
        this mirrors ``EasyList.from_dict``.
        """
        if not isinstance(obj_dict, dict) or '@module' not in obj_dict or '@class' not in obj_dict:
            raise ValueError('Input must be a dictionary representing an EasyScience collection.')
        if obj_dict['@class'] != cls.__name__:
            raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.')
        temp_dict = copy.deepcopy(obj_dict)
        protected_types = None
        if 'protected_types' in temp_dict:
            protected_types = []
            for type_dict in temp_dict.pop('protected_types'):
                if '@module' not in type_dict or '@class' not in type_dict:
                    raise ValueError('Each protected type must be a serialized EasyScience class with @module and @class keys')
                module = __import__(type_dict['@module'], globals(), locals(), [type_dict['@class']], 0)
                if not hasattr(module, type_dict['@class']):
                    raise ImportError(f'Could not import class {type_dict["@class"]} from module {type_dict["@module"]}')
                protected_types.append(getattr(module, type_dict['@class']))
        raw_items = temp_dict.pop('data', [])
        kwargs = SerializerBase.deserialize_dict(temp_dict)
        data = [references.deserialize(item) for item in raw_items]
        return cls(data, protected_types=protected_types, **kwargs)

    def __deepcopy__(self, memo):
        """Round-trip via dict-skip-unique to get a fresh copy.

        `NewBase.__copy__` already does this; the override is kept (rather
        than deleted) to mirror the legacy `BaseCollection.__deepcopy__`
        semantics — callers that relied on `copy.deepcopy(collection)` still
        get a clone built from `from_dict(as_dict(skip=['unique_name']))`.
        """
        return self.from_dict(self.as_dict(skip=['unique_name']))

names property

List of item names.

data property

Read-only view of the underlying item list.

Provided for compatibility with code (and tests) written against the legacy CollectionBase shape, which exposed items via .data.

insert(index, value)

Insert and (if an interface is set) propagate it to the new item.

Legacy CollectionBase.insert did value.interface = self.interface after registering the item; we replicate the same behaviour here so downstream calculator state stays in sync when items are appended after the collection's interface was already set.

The type check from EasyList.insert is bypassed: each subclass accepts a single item type (Layer / Material / BaseAssembly / Model) and enforces it elsewhere, while the EasyList check would require every item to be a NewBase subclass — which was historically not guaranteed and forces an extra coupling we don't need.

Source code in src/easyreflectometry/sample/collections/base_collection.py
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
def insert(self, index: int, value: Any) -> None:
    """Insert and (if an interface is set) propagate it to the new item.

    Legacy `CollectionBase.insert` did `value.interface = self.interface`
    after registering the item; we replicate the same behaviour here so
    downstream calculator state stays in sync when items are appended
    after the collection's interface was already set.

    The type check from `EasyList.insert` is bypassed: each subclass
    accepts a single item type (Layer / Material / BaseAssembly / Model)
    and enforces it elsewhere, while the EasyList check would require
    every item to be a `NewBase` subclass — which was historically not
    guaranteed and forces an extra coupling we don't need.
    """
    if not isinstance(index, int):
        raise TypeError('Index must be an integer')
    # Skip the EasyList protected-types check; mimic the rest of its insert
    # (duplicate-name warning + append-to-_data).
    import warnings as _warnings

    if value in self:
        _warnings.warn(f'Item with unique name "{self._get_key(value)}" already in collection, it will be ignored')
        return
    self._data.insert(index, value)
    if self._interface is not None and self._has_interface_setter(type(value)):
        value.interface = self._interface

move_up(index)

Move the element at the given index up in the collection.

Source code in src/easyreflectometry/sample/collections/base_collection.py
172
173
174
175
176
def move_up(self, index: int) -> None:
    """Move the element at the given index up in the collection."""
    if index == 0:
        return
    self.insert(index - 1, self.pop(index))

move_down(index)

Move the element at the given index down in the collection.

Source code in src/easyreflectometry/sample/collections/base_collection.py
178
179
180
181
182
def move_down(self, index: int) -> None:
    """Move the element at the given index down in the collection."""
    if index == len(self) - 1:
        return
    self.insert(index + 1, self.pop(index))

remove_at(index)

Remove the item at index from the collection.

Renamed from the legacy BaseCollection.remove(index) which shadowed MutableSequence.remove(value) (remove-by-value, inherited from EasyList). Callers that meant "remove by index" should use this; the standard remove(value) is still available with its usual semantics.

Source code in src/easyreflectometry/sample/collections/base_collection.py
184
185
186
187
188
189
190
191
192
def remove_at(self, index: int) -> None:
    """Remove the item at *index* from the collection.

    Renamed from the legacy `BaseCollection.remove(index)` which shadowed
    `MutableSequence.remove(value)` (remove-by-value, inherited from
    `EasyList`). Callers that meant "remove by index" should use this; the
    standard `remove(value)` is still available with its usual semantics.
    """
    self.pop(index)

get_parameters()

Compatibility alias for legacy callers; prefer get_all_parameters.

Source code in src/easyreflectometry/sample/collections/base_collection.py
196
197
198
def get_parameters(self) -> List[Parameter]:
    """Compatibility alias for legacy callers; prefer `get_all_parameters`."""
    return self.get_all_parameters()

get_all_variables()

Flat list of every Parameter/Descriptor across all items.

Walks each item in the collection and unions whatever each item exposes via its own get_all_variables (for ModelBase / BaseCore children) or, for objects that lack that hook, unions any direct DescriptorBase attributes.

Source code in src/easyreflectometry/sample/collections/base_collection.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def get_all_variables(self) -> List:
    """Flat list of every Parameter/Descriptor across all items.

    Walks each item in the collection and unions whatever each item
    exposes via its own `get_all_variables` (for `ModelBase` /
    `BaseCore` children) or, for objects that lack that hook,
    unions any direct `DescriptorBase` attributes.
    """
    from easyscience.variable.descriptor_base import DescriptorBase

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

get_fit_parameters()

Alias kept for the minimizer; matches ModelBase.get_fit_parameters.

Source code in src/easyreflectometry/sample/collections/base_collection.py
230
231
232
def get_fit_parameters(self) -> List[Parameter]:
    """Alias kept for the minimizer; matches `ModelBase.get_fit_parameters`."""
    return self.get_free_parameters()

to_dict(skip=None)

Serialize with skip support; interface excluded by default.

EasyList.to_dict doesn't accept a skip argument and is hard-wired to dump data plus the parent's _arg_spec view. We reimplement here so existing callers (Project, Model.as_dict, etc.) can keep passing skip=['unique_name'] or similar.

NewBase.to_dict mutates the skip list in-place (e.g. it appends 'unique_name' when the collection's own unique_name is default-generated). We therefore pass a copy to it, otherwise the mutation would leak into the per-item serialization below and force every item Parameter dict to drop its unique_name — breaking the from_dict round-trip.

Source code in src/easyreflectometry/sample/collections/base_collection.py
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
def to_dict(self, skip: Optional[List[str]] = None) -> dict:
    """Serialize with `skip` support; `interface` excluded by default.

    `EasyList.to_dict` doesn't accept a `skip` argument and is hard-wired
    to dump `data` plus the parent's `_arg_spec` view. We reimplement here
    so existing callers (`Project`, `Model.as_dict`, etc.) can keep
    passing `skip=['unique_name']` or similar.

    ``NewBase.to_dict`` mutates the ``skip`` list in-place (e.g. it
    appends ``'unique_name'`` when the collection's own unique_name is
    default-generated). We therefore pass a *copy* to it, otherwise the
    mutation would leak into the per-item serialization below and force
    every item Parameter dict to drop its ``unique_name`` — breaking the
    from_dict round-trip.
    """
    skip = list(skip or [])
    if 'interface' not in skip:
        skip.append('interface')
    # Matches legacy `BasedBase.as_dict`: drop unique_name from the
    # serialized form so nested Parameters don't get explicit names that
    # would collide with the auto-generated names produced when the global
    # counter restarts during reconstruction.
    if 'unique_name' not in skip:
        skip.append('unique_name')
    # The items this encodes (through `_convert_to_dict`) are replaced
    # below, so they must not be registered as shared-object occurrences.
    with references.suspended():
        dict_repr = NewBase.to_dict(self, skip=list(skip))
    if self._protected_types != [NewBase]:
        dict_repr['protected_types'] = [{'@module': c.__module__, '@class': c.__name__} for c in self._protected_types]
    dict_repr['data'] = []
    for item in self._data:
        # Items that are ModelBase / BaseCore subclasses accept `skip`;
        # other shapes use their no-arg `to_dict`/`as_dict`.
        if hasattr(item, 'to_dict'):
            try:
                dict_repr['data'].append(item.to_dict(skip=list(skip)))
            except TypeError:
                dict_repr['data'].append(item.to_dict())
        else:
            dict_repr['data'].append(item.as_dict(skip=list(skip)))
    return dict_repr

as_dict(skip=None)

Compatibility alias for :meth:to_dict.

Source code in src/easyreflectometry/sample/collections/base_collection.py
327
328
329
def as_dict(self, skip: Optional[List[str]] = None) -> dict:
    """Compatibility alias for :meth:`to_dict`."""
    return self.to_dict(skip=skip)

from_dict(obj_dict) classmethod

Rebuild the collection, each item through its own class's from_dict.

EasyList.from_dict deserializes the data list with the serializer's generic path, which calls every item's constructor with the stored keys. An item whose class restores state in from_dict (e.g. MaterialDensity and its sld_coupled key, or any nested item that does) then fails or loses that state. Apart from the items, this mirrors EasyList.from_dict.

Source code in src/easyreflectometry/sample/collections/base_collection.py
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
@classmethod
def from_dict(cls, obj_dict: dict) -> BaseCollection:
    """Rebuild the collection, each item through its own class's ``from_dict``.

    ``EasyList.from_dict`` deserializes the ``data`` list with the
    serializer's generic path, which calls every item's constructor with
    the stored keys. An item whose class restores state in ``from_dict``
    (e.g. ``MaterialDensity`` and its ``sld_coupled`` key, or any nested
    item that does) then fails or loses that state. Apart from the items,
    this mirrors ``EasyList.from_dict``.
    """
    if not isinstance(obj_dict, dict) or '@module' not in obj_dict or '@class' not in obj_dict:
        raise ValueError('Input must be a dictionary representing an EasyScience collection.')
    if obj_dict['@class'] != cls.__name__:
        raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.')
    temp_dict = copy.deepcopy(obj_dict)
    protected_types = None
    if 'protected_types' in temp_dict:
        protected_types = []
        for type_dict in temp_dict.pop('protected_types'):
            if '@module' not in type_dict or '@class' not in type_dict:
                raise ValueError('Each protected type must be a serialized EasyScience class with @module and @class keys')
            module = __import__(type_dict['@module'], globals(), locals(), [type_dict['@class']], 0)
            if not hasattr(module, type_dict['@class']):
                raise ImportError(f'Could not import class {type_dict["@class"]} from module {type_dict["@module"]}')
            protected_types.append(getattr(module, type_dict['@class']))
    raw_items = temp_dict.pop('data', [])
    kwargs = SerializerBase.deserialize_dict(temp_dict)
    data = [references.deserialize(item) for item in raw_items]
    return cls(data, protected_types=protected_types, **kwargs)

__deepcopy__(memo)

Round-trip via dict-skip-unique to get a fresh copy.

NewBase.__copy__ already does this; the override is kept (rather than deleted) to mirror the legacy BaseCollection.__deepcopy__ semantics — callers that relied on copy.deepcopy(collection) still get a clone built from from_dict(as_dict(skip=['unique_name'])).

Source code in src/easyreflectometry/sample/collections/base_collection.py
362
363
364
365
366
367
368
369
370
def __deepcopy__(self, memo):
    """Round-trip via dict-skip-unique to get a fresh copy.

    `NewBase.__copy__` already does this; the override is kept (rather
    than deleted) to mirror the legacy `BaseCollection.__deepcopy__`
    semantics — callers that relied on `copy.deepcopy(collection)` still
    get a clone built from `from_dict(as_dict(skip=['unique_name']))`.
    """
    return self.from_dict(self.as_dict(skip=['unique_name']))

Sample

Bases: BaseCollection

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

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

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

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

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

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

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

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

    def duplicate_assembly(self, index: int):
        """Duplicate an assembly of the sample.

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

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

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

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

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

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

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

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

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

    def volume_fraction_profile(
        self,
        z: Optional[np.ndarray] = None,
        *,
        max_delta_z: Optional[float] = None,
        merge_equivalent: bool = True,
    ) -> VolumeFractionProfile:
        """Volume fraction (occupancy) of every component of the sample versus depth.

        A thin wrapper around :func:`easyreflectometry.sample.volume_fraction.volume_fraction_profile`;
        see there for the parameters, the depth convention and the caveats.

        Parameters
        ----------
        z : np.ndarray, optional
            Depths (Å) to evaluate at; ``z = 0`` is the superphase / first layer interface.
            By default a 500-point grid spanning the sample.
        max_delta_z : float, optional
            Maximum spacing of the default grid. Cannot be combined with ``z``.
        merge_equivalent : bool, optional
            Merge materials with equal name and SLD into one component. By default, True.

        Returns
        -------
        VolumeFractionProfile
            The per-component profiles, summing to one at every depth.
        """
        return volume_fraction_profile(self, z, max_delta_z=max_delta_z, merge_equivalent=merge_equivalent)

superphase property

The superphase of the sample.

subphase property

The subphase of the sample.

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

Constructor.

Parameters:

Name Type Description Default
**kwargs
{}
populate_if_none bool

By default, True.

True
unique_name Optional[str]

By default, None.

None
*assemblies Optional[List[BaseAssembly]]

The assemblies in the sample.

()
name str

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

'EasySample'
interface

Calculator interface. By default, None.

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

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

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

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

add_assembly(assembly=None)

Add an assembly to the sample.

Parameters:

Name Type Description Default
assembly Optional[BaseAssembly]

Assembly to add. By default, None.

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

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

duplicate_assembly(index)

Duplicate an assembly of the sample.

Parameters:

Name Type Description Default
index int

Index of the assembly to duplicate.

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

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

move_up(index)

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

Parameters:

Name Type Description Default
index int

Index of the assembly to move up.

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

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

move_down(index)

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

Parameters:

Name Type Description Default
index int

Index of the assembly to move down.

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

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

remove_assembly(index)

Remove the assembly at the given index from the sample.

Parameters:

Name Type Description Default
index int

Index of the assembly to remove.

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

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

volume_fraction_profile(z=None, *, max_delta_z=None, merge_equivalent=True)

Volume fraction (occupancy) of every component of the sample versus depth.

A thin wrapper around :func:easyreflectometry.sample.volume_fraction.volume_fraction_profile; see there for the parameters, the depth convention and the caveats.

Parameters:

Name Type Description Default
z np.ndarray

Depths (Å) to evaluate at; z = 0 is the superphase / first layer interface. By default a 500-point grid spanning the sample.

None
max_delta_z float

Maximum spacing of the default grid. Cannot be combined with z.

None
merge_equivalent bool

Merge materials with equal name and SLD into one component. By default, True.

True

Returns:

Type Description
VolumeFractionProfile

The per-component profiles, summing to one at every depth.

Source code in src/easyreflectometry/sample/collections/sample.py
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
def volume_fraction_profile(
    self,
    z: Optional[np.ndarray] = None,
    *,
    max_delta_z: Optional[float] = None,
    merge_equivalent: bool = True,
) -> VolumeFractionProfile:
    """Volume fraction (occupancy) of every component of the sample versus depth.

    A thin wrapper around :func:`easyreflectometry.sample.volume_fraction.volume_fraction_profile`;
    see there for the parameters, the depth convention and the caveats.

    Parameters
    ----------
    z : np.ndarray, optional
        Depths (Å) to evaluate at; ``z = 0`` is the superphase / first layer interface.
        By default a 500-point grid spanning the sample.
    max_delta_z : float, optional
        Maximum spacing of the default grid. Cannot be combined with ``z``.
    merge_equivalent : bool, optional
        Merge materials with equal name and SLD into one component. By default, True.

    Returns
    -------
    VolumeFractionProfile
        The per-component profiles, summing to one at every depth.
    """
    return volume_fraction_profile(self, z, max_delta_z=max_delta_z, merge_equivalent=merge_equivalent)

volume_fraction_profile(sample, z=None, *, max_delta_z=None, merge_equivalent=True)

Erf-smeared volume fraction of every component of sample versus depth.

Parameters:

Name Type Description Default
sample Sample

The sample. It does not need to be bound to a calculator.

required
z np.ndarray

Depths (Å) at which to evaluate the profile, z = 0 being the superphase / first layer interface. By default a grid of :data:DEFAULT_POINTS points spanning the sample plus four times the outer roughnesses on each side (the refnx convention; the extent only looks at the two outer interfaces, so pass a wider z when a broad internal interface matters).

None
max_delta_z float

Maximum spacing of the default grid; raises the point count above :data:DEFAULT_POINTS when needed. Cannot be combined with z.

None
merge_equivalent bool

Merge materials the calculator cannot tell apart (equal name, SLD and imaginary SLD) into one component, so that the several default solvent objects of a bilayer come out as one "D2O" trace and its two head groups as one trace. False keeps every material object separate. Materials that share a name but differ in SLD are never merged; use :meth:VolumeFractionProfile.grouped for that. By default, True.

True

Returns:

Type Description
VolumeFractionProfile

The profile. sum_c phi_c(z) == 1 at every depth.

Raises:

Type Description
ValueError

The sample has fewer than two layers, both z and max_delta_z are given, a grid argument is invalid, a parameter value is not finite, or a :class:~easyreflectometry.sample.GradientLayer was edited after construction so that its sub-layers no longer carry the linear blend of its end materials.

Warns:

Type Description
VolumeFractionWarning

A fraction dips below -NEGATIVE_FRACTION_TOLERANCE somewhere on the grid (strongly unequal roughness around a thin layer).

Source code in src/easyreflectometry/sample/volume_fraction.py
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
def volume_fraction_profile(
    sample: 'Sample',
    z: Optional[np.ndarray] = None,
    *,
    max_delta_z: Optional[float] = None,
    merge_equivalent: bool = True,
) -> VolumeFractionProfile:
    """Erf-smeared volume fraction of every component of ``sample`` versus depth.

    Parameters
    ----------
    sample : Sample
        The sample. It does not need to be bound to a calculator.
    z : np.ndarray, optional
        Depths (Å) at which to evaluate the profile, ``z = 0`` being the
        superphase / first layer interface. By default a grid of
        :data:`DEFAULT_POINTS` points spanning the sample plus four times the
        outer roughnesses on each side (the refnx convention; the extent only
        looks at the two outer interfaces, so pass a wider ``z`` when a broad
        internal interface matters).
    max_delta_z : float, optional
        Maximum spacing of the default grid; raises the point count above
        :data:`DEFAULT_POINTS` when needed. Cannot be combined with ``z``.
    merge_equivalent : bool, optional
        Merge materials the calculator cannot tell apart (equal name, SLD and
        imaginary SLD) into one component, so that the several default solvent
        objects of a bilayer come out as one "D2O" trace and its two head
        groups as one trace. ``False`` keeps every material object separate.
        Materials that share a name but differ in SLD are never merged; use
        :meth:`VolumeFractionProfile.grouped` for that. By default, True.

    Returns
    -------
    VolumeFractionProfile
        The profile. ``sum_c phi_c(z) == 1`` at every depth.

    Raises
    ------
    ValueError
        The sample has fewer than two layers, both ``z`` and ``max_delta_z``
        are given, a grid argument is invalid, a parameter value is not
        finite, or a :class:`~easyreflectometry.sample.GradientLayer` was
        edited after construction so that its sub-layers no longer carry the
        linear blend of its end materials.

    Warns
    -----
    VolumeFractionWarning
        A fraction dips below ``-NEGATIVE_FRACTION_TOLERANCE`` somewhere on the
        grid (strongly unequal roughness around a thin layer).
    """
    if z is not None and max_delta_z is not None:
        raise ValueError('Give either z or max_delta_z, not both.')
    slabs, labels = _resolve_slabs(sample, merge_equivalent=merge_equivalent)
    grid = _default_grid(slabs, max_delta_z) if z is None else _validated_grid(z)

    thickness = np.array([slab.thickness for slab in slabs], dtype=float)
    interfaces = np.cumsum(thickness[:-1])  # z of the interface above slab 1, 2, ..., M
    sigmas = np.array([slab.roughness for slab in slabs[1:]], dtype=float)
    fractions = {}
    for key in labels:
        values = np.array([slab.composition.get(key, 0.0) for slab in slabs], dtype=float)
        fractions[key] = _smear(grid, interfaces, sigmas, values)
    _warn_if_inadmissible(grid, fractions, labels)
    return VolumeFractionProfile(z=grid, fractions=fractions, labels=labels)

DEFAULT_ELEMENTS(interface)

:meta private:.

Source code in src/easyreflectometry/sample/collections/sample.py
22
23
24
25
26
27
def DEFAULT_ELEMENTS(interface):
    """:meta private:."""
    return (
        Multilayer(interface=interface),
        Multilayer(interface=interface),
    )