Skip to content

surfactant_layer

LayerCollection

Bases: BaseCollection

Source code in src/easyreflectometry/sample/collections/layer_collection.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class LayerCollection(BaseCollection):
    def __init__(
        self,
        *layers: Optional[list[Layer]],
        name: str = 'EasyLayerCollection',
        interface=None,
        unique_name: Optional[str] = None,
        populate_if_none: bool = True,
        **kwargs,
    ):
        """Init function."""
        if not layers:
            layers = []

        super().__init__(
            name,
            interface,
            *layers,
            unique_name=unique_name,
            populate_if_none=populate_if_none,
            **kwargs,
        )

    def add_layer(self, layer: Optional[Layer] = None):
        """Add a layer to the collection.

        Parameters
        ----------
        layer : Optional[Layer], optional
            Layer to add. By default, None.
        """
        if layer is None:
            layer = Layer(
                name='EasyLayer added',
                interface=self.interface,
            )
        self.append(layer)

    def duplicate_layer(self, index: int):
        """Duplicate a layer in the collection.

        Parameters
        ----------
        index : int
        layer :
            Assembly to add.
        """
        to_be_duplicated = self[index]
        duplicate = Layer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
        duplicate.name = duplicate.name + ' duplicate'
        self.append(duplicate)

__init__(*layers, name='EasyLayerCollection', interface=None, unique_name=None, populate_if_none=True, **kwargs)

Init function.

Source code in src/easyreflectometry/sample/collections/layer_collection.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def __init__(
    self,
    *layers: Optional[list[Layer]],
    name: str = 'EasyLayerCollection',
    interface=None,
    unique_name: Optional[str] = None,
    populate_if_none: bool = True,
    **kwargs,
):
    """Init function."""
    if not layers:
        layers = []

    super().__init__(
        name,
        interface,
        *layers,
        unique_name=unique_name,
        populate_if_none=populate_if_none,
        **kwargs,
    )

add_layer(layer=None)

Add a layer to the collection.

Parameters:

Name Type Description Default
layer Optional[Layer]

Layer to add. By default, None.

None
Source code in src/easyreflectometry/sample/collections/layer_collection.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def add_layer(self, layer: Optional[Layer] = None):
    """Add a layer to the collection.

    Parameters
    ----------
    layer : Optional[Layer], optional
        Layer to add. By default, None.
    """
    if layer is None:
        layer = Layer(
            name='EasyLayer added',
            interface=self.interface,
        )
    self.append(layer)

duplicate_layer(index)

Duplicate a layer in the collection.

Parameters:

Name Type Description Default
index int
required
layer

Assembly to add.

required
Source code in src/easyreflectometry/sample/collections/layer_collection.py
49
50
51
52
53
54
55
56
57
58
59
60
61
def duplicate_layer(self, index: int):
    """Duplicate a layer in the collection.

    Parameters
    ----------
    index : int
    layer :
        Assembly to add.
    """
    to_be_duplicated = self[index]
    duplicate = Layer.from_dict(to_be_duplicated.as_dict(skip=['unique_name']))
    duplicate.name = duplicate.name + ' duplicate'
    self.append(duplicate)

LayerAreaPerMolecule

Bases: Layer

The LayerAreaPerMolecule class allows a layer to be defined in terms of some molecular formula an area per molecule, and a solvent.

Source code in src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py
 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
class LayerAreaPerMolecule(Layer):
    """The `LayerAreaPerMolecule` class allows a layer to be defined in terms of some
    molecular formula an area per molecule, and a solvent.
    """

    def __init__(
        self,
        molecular_formula: Union[str, None] = None,
        thickness: Union[Parameter, float, None] = None,
        solvent: Union[Material, None] = None,
        solvent_fraction: Union[Parameter, float, None] = None,
        area_per_molecule: Union[Parameter, float, None] = None,
        roughness: Union[Parameter, float, None] = None,
        name: str = 'EasyLayerAreaPerMolecule',
        unique_name: Optional[str] = None,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        molecular_formula : Union[str, None], optional
            Formula for the molecule in the layer. By default, None.
        thickness : Union[Parameter, float, None], optional
            Layer thickness in Angstrom. By default, None.
        solvent : Union[Material, None], optional
            Solvent containing the molecule. By default, None.
        solvent_fraction : Union[Parameter, float, None], optional
            Fraction of solvent in layer. Fx solvation or surface coverage. By default, None.
        area_per_molecule : Union[Parameter, float, None], optional
            Area per molecule in the layer. By default, None.
        roughness : Union[Parameter, float, None], optional
            Upper roughness on the layer in Angstrom. By default, None.
        name : str, optional
            Name of the layer. By default, 'EasyLayerAreaPerMolecule'.
        interface :
            Interface object. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        if solvent is None:
            solvent = Material(
                sld=6.36,
                isld=0,
                name='D2O',
                unique_name=unique_name + '_MaterialSolvent',
                interface=interface,
            )

        if molecular_formula is None:
            molecular_formula = DEFAULTS['molecular_formula']
        molecule_material = Material(
            sld=0.0,
            isld=0.0,
            name=molecular_formula,
            unique_name=unique_name + '_MaterialMolecule',
            interface=interface,
        )

        thickness = get_as_parameter(
            name='thickness',
            value=thickness,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Thickness',
        )
        area_per_molecule_param = get_as_parameter(
            name='area_per_molecule',
            value=area_per_molecule,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_AreaPerMolecule',
        )
        scattering_length_real = get_as_parameter(
            name='scattering_length_real',
            value=0.0,
            default_dict=DEFAULTS['sl'],
            unique_name_prefix=f'{unique_name}_Sl',
        )
        scattering_length_imag = get_as_parameter(
            name='scattering_length_imag',
            value=0.0,
            default_dict=DEFAULTS['isl'],
            unique_name_prefix=f'{unique_name}_Isl',
        )

        # Constrain molecule.sld / .isld to scattering_length / (thickness * area_per_molecule).
        # `_setup_sld_constraints` rebuilds the same expression after from_dict, so keep the
        # variable names (`a`, `b`, `p`) consistent with that path.
        dependency_expression = 'a / (b*p) * 1e6'
        molecule_material.sld.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map={'a': scattering_length_real, 'b': thickness, 'p': area_per_molecule_param},
        )
        molecule_material.isld.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map={'a': scattering_length_imag, 'b': thickness, 'p': area_per_molecule_param},
        )

        solvated_molecule_material = MaterialSolvated(
            material=molecule_material,
            solvent=solvent,
            solvent_fraction=solvent_fraction,
            unique_name=unique_name + '_MaterialSolvated',
            interface=interface,
        )
        super().__init__(
            material=solvated_molecule_material,
            thickness=thickness,
            roughness=roughness,
            name=name,
            unique_name=unique_name,
            interface=None,
        )
        self._area_per_molecule = area_per_molecule_param
        self._scattering_length_real = scattering_length_real
        self._scattering_length_imag = scattering_length_imag

        scattering_length = neutron_scattering_length(molecular_formula)
        self._scattering_length_real.value = scattering_length.real
        self._scattering_length_imag.value = scattering_length.imag
        self._molecular_formula = molecular_formula

        if interface is not None:
            self.interface = interface

    # ----- constraint plumbing -----

    def _setup_sld_constraints(self) -> None:
        """Wire the inner molecule material's ``sld`` / ``isld`` to depend on
        the current scattering-length, thickness, and area-per-molecule
        parameters.

        Idempotent — called once from ``__init__`` and again from
        ``from_dict`` after the saved Parameter objects replace the
        constructor-time temporaries.
        """
        molecule_material = self.material.material
        for derived in (molecule_material.sld, molecule_material.isld):
            if not derived.independent:
                derived.make_independent()

        dependency_expression = 'a / (b*p) * 1e6'
        molecule_material.sld.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map={
                'a': self._scattering_length_real,
                'b': self._thickness,
                'p': self._area_per_molecule,
            },
        )
        molecule_material.isld.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map={
                'a': self._scattering_length_imag,
                'b': self._thickness,
                'p': self._area_per_molecule,
            },
        )

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

    @classmethod
    def from_dict(cls, obj_dict: dict) -> 'LayerAreaPerMolecule':
        """Re-route the saved ``solvent_fraction`` Parameter and rebuild the
        molecule-SLD constraint chain after :class:`ModelBase.from_dict`
        swaps in the persisted Parameter objects.

        `ModelBase.from_dict` writes the deserialized ``solvent_fraction``
        Parameter to ``self._solvent_fraction`` (orphan — the live property
        delegates to ``self.material.solvent_fraction``, which is
        ``self.material._fraction``). It also reassigns ``self._thickness``
        and ``self._area_per_molecule``, but the constraint graph built in
        ``__init__`` still references the temporary Parameters created from
        the float kwargs. We fix both here.
        """
        instance = super().from_dict(obj_dict)

        saved_solvent_fraction = instance.__dict__.pop('_solvent_fraction', None)
        if saved_solvent_fraction is not None:
            mixture = instance.material
            old = mixture._fraction
            mixture._fraction = saved_solvent_fraction
            try:
                instance._global_object.map.prune(old.unique_name)
            except (AttributeError, KeyError):
                pass
            mixture._materials_constraints()

        instance._setup_sld_constraints()
        return instance

    @property
    def area_per_molecule_parameter(self) -> Parameter:
        """Get the parameter for area per molecule."""
        return self._area_per_molecule

    @property
    def area_per_molecule(self) -> Parameter:
        """The Parameter that controls area per molecule."""
        return self._area_per_molecule

    @area_per_molecule.setter
    def area_per_molecule(self, value: float) -> None:
        if value < 0:
            raise ValueError('area_per_molecule must be greater than 0.0.')
        self._area_per_molecule.value = value

    @property
    def molecule(self) -> Material:
        """Get the molecule material."""
        return self.material.material

    @property
    def solvent(self) -> Material:
        """Get the solvent material."""
        return self.material.solvent

    @solvent.setter
    def solvent(self, new_solvent: Material) -> None:
        self.material.solvent = new_solvent

    @property
    def solvent_fraction_parameter(self) -> Parameter:
        """Get parameter for the fraction of the layer occupied by the solvent."""
        return self.material.solvent_fraction_parameter

    @property
    def solvent_fraction(self) -> Parameter:
        """The Parameter for the fraction of the layer occupied by the solvent."""
        return self.material.solvent_fraction

    @solvent_fraction.setter
    def solvent_fraction(self, value: float) -> None:
        self.material.solvent_fraction = value

    @property
    def molecular_formula(self) -> str:
        """Get the formula of molecule the layer."""
        return self._molecular_formula

    @molecular_formula.setter
    def molecular_formula(self, formula_string: str) -> None:
        self._molecular_formula = formula_string
        scattering_length = neutron_scattering_length(formula_string)
        self._scattering_length_real.value = scattering_length.real
        self._scattering_length_imag.value = scattering_length.imag

        self.molecule.name = formula_string
        self.material._update_name()

    @property
    def _dict_repr(self) -> dict[str, str]:
        """Dictionary representation of the `area_per_molecule` object."""
        dict_repr = super()._dict_repr
        dict_repr['molecular_formula'] = self._molecular_formula
        dict_repr['area_per_molecule'] = f'{self._area_per_molecule.value:.2f} {self._area_per_molecule.unit}'
        return dict_repr

area_per_molecule_parameter property

Get the parameter for area per molecule.

area_per_molecule property writable

The Parameter that controls area per molecule.

molecule property

Get the molecule material.

solvent property writable

Get the solvent material.

solvent_fraction_parameter property

Get parameter for the fraction of the layer occupied by the solvent.

solvent_fraction property writable

The Parameter for the fraction of the layer occupied by the solvent.

molecular_formula property writable

Get the formula of molecule the layer.

__init__(molecular_formula=None, thickness=None, solvent=None, solvent_fraction=None, area_per_molecule=None, roughness=None, name='EasyLayerAreaPerMolecule', unique_name=None, interface=None)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
molecular_formula Union[str, None]

Formula for the molecule in the layer. By default, None.

None
thickness Union[Parameter, float, None]

Layer thickness in Angstrom. By default, None.

None
solvent Union[Material, None]

Solvent containing the molecule. By default, None.

None
solvent_fraction Union[Parameter, float, None]

Fraction of solvent in layer. Fx solvation or surface coverage. By default, None.

None
area_per_molecule Union[Parameter, float, None]

Area per molecule in the layer. By default, None.

None
roughness Union[Parameter, float, None]

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

None
name str

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

'EasyLayerAreaPerMolecule'
interface

Interface object. By default, None.

None
Source code in src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py
 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
def __init__(
    self,
    molecular_formula: Union[str, None] = None,
    thickness: Union[Parameter, float, None] = None,
    solvent: Union[Material, None] = None,
    solvent_fraction: Union[Parameter, float, None] = None,
    area_per_molecule: Union[Parameter, float, None] = None,
    roughness: Union[Parameter, float, None] = None,
    name: str = 'EasyLayerAreaPerMolecule',
    unique_name: Optional[str] = None,
    interface=None,
):
    """Constructor.

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    molecular_formula : Union[str, None], optional
        Formula for the molecule in the layer. By default, None.
    thickness : Union[Parameter, float, None], optional
        Layer thickness in Angstrom. By default, None.
    solvent : Union[Material, None], optional
        Solvent containing the molecule. By default, None.
    solvent_fraction : Union[Parameter, float, None], optional
        Fraction of solvent in layer. Fx solvation or surface coverage. By default, None.
    area_per_molecule : Union[Parameter, float, None], optional
        Area per molecule in the layer. By default, None.
    roughness : Union[Parameter, float, None], optional
        Upper roughness on the layer in Angstrom. By default, None.
    name : str, optional
        Name of the layer. By default, 'EasyLayerAreaPerMolecule'.
    interface :
        Interface object. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    if solvent is None:
        solvent = Material(
            sld=6.36,
            isld=0,
            name='D2O',
            unique_name=unique_name + '_MaterialSolvent',
            interface=interface,
        )

    if molecular_formula is None:
        molecular_formula = DEFAULTS['molecular_formula']
    molecule_material = Material(
        sld=0.0,
        isld=0.0,
        name=molecular_formula,
        unique_name=unique_name + '_MaterialMolecule',
        interface=interface,
    )

    thickness = get_as_parameter(
        name='thickness',
        value=thickness,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Thickness',
    )
    area_per_molecule_param = get_as_parameter(
        name='area_per_molecule',
        value=area_per_molecule,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_AreaPerMolecule',
    )
    scattering_length_real = get_as_parameter(
        name='scattering_length_real',
        value=0.0,
        default_dict=DEFAULTS['sl'],
        unique_name_prefix=f'{unique_name}_Sl',
    )
    scattering_length_imag = get_as_parameter(
        name='scattering_length_imag',
        value=0.0,
        default_dict=DEFAULTS['isl'],
        unique_name_prefix=f'{unique_name}_Isl',
    )

    # Constrain molecule.sld / .isld to scattering_length / (thickness * area_per_molecule).
    # `_setup_sld_constraints` rebuilds the same expression after from_dict, so keep the
    # variable names (`a`, `b`, `p`) consistent with that path.
    dependency_expression = 'a / (b*p) * 1e6'
    molecule_material.sld.make_dependent_on(
        dependency_expression=dependency_expression,
        dependency_map={'a': scattering_length_real, 'b': thickness, 'p': area_per_molecule_param},
    )
    molecule_material.isld.make_dependent_on(
        dependency_expression=dependency_expression,
        dependency_map={'a': scattering_length_imag, 'b': thickness, 'p': area_per_molecule_param},
    )

    solvated_molecule_material = MaterialSolvated(
        material=molecule_material,
        solvent=solvent,
        solvent_fraction=solvent_fraction,
        unique_name=unique_name + '_MaterialSolvated',
        interface=interface,
    )
    super().__init__(
        material=solvated_molecule_material,
        thickness=thickness,
        roughness=roughness,
        name=name,
        unique_name=unique_name,
        interface=None,
    )
    self._area_per_molecule = area_per_molecule_param
    self._scattering_length_real = scattering_length_real
    self._scattering_length_imag = scattering_length_imag

    scattering_length = neutron_scattering_length(molecular_formula)
    self._scattering_length_real.value = scattering_length.real
    self._scattering_length_imag.value = scattering_length.imag
    self._molecular_formula = molecular_formula

    if interface is not None:
        self.interface = interface

from_dict(obj_dict) classmethod

Re-route the saved solvent_fraction Parameter and rebuild the molecule-SLD constraint chain after :class:ModelBase.from_dict swaps in the persisted Parameter objects.

ModelBase.from_dict writes the deserialized solvent_fraction Parameter to self._solvent_fraction (orphan — the live property delegates to self.material.solvent_fraction, which is self.material._fraction). It also reassigns self._thickness and self._area_per_molecule, but the constraint graph built in __init__ still references the temporary Parameters created from the float kwargs. We fix both here.

Source code in src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py
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
@classmethod
def from_dict(cls, obj_dict: dict) -> 'LayerAreaPerMolecule':
    """Re-route the saved ``solvent_fraction`` Parameter and rebuild the
    molecule-SLD constraint chain after :class:`ModelBase.from_dict`
    swaps in the persisted Parameter objects.

    `ModelBase.from_dict` writes the deserialized ``solvent_fraction``
    Parameter to ``self._solvent_fraction`` (orphan — the live property
    delegates to ``self.material.solvent_fraction``, which is
    ``self.material._fraction``). It also reassigns ``self._thickness``
    and ``self._area_per_molecule``, but the constraint graph built in
    ``__init__`` still references the temporary Parameters created from
    the float kwargs. We fix both here.
    """
    instance = super().from_dict(obj_dict)

    saved_solvent_fraction = instance.__dict__.pop('_solvent_fraction', None)
    if saved_solvent_fraction is not None:
        mixture = instance.material
        old = mixture._fraction
        mixture._fraction = saved_solvent_fraction
        try:
            instance._global_object.map.prune(old.unique_name)
        except (AttributeError, KeyError):
            pass
        mixture._materials_constraints()

    instance._setup_sld_constraints()
    return instance

Material

Bases: BaseCore

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

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

        sld = get_as_parameter(
            name='sld',
            value=sld,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Sld',
        )
        apply_default_limits(sld, 'sld')

        isld = get_as_parameter(
            name='isld',
            value=isld,
            default_dict=DEFAULTS,
            unique_name_prefix=f'{unique_name}_Isld',
        )
        apply_default_limits(isld, 'isld')

        super().__init__(name=name, unique_name=unique_name)
        self._sld = sld
        self._isld = isld

        if interface is not None:
            self.interface = interface

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

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

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

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

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

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

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
sld Union[Parameter, float, None]

Real scattering length density. By default, None.

None
isld Union[Parameter, float, None]

Imaginary scattering length density. By default, None.

None
name str

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

'EasyMaterial'
interface

Calculator interface. By default, None.

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

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

    sld = get_as_parameter(
        name='sld',
        value=sld,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Sld',
    )
    apply_default_limits(sld, 'sld')

    isld = get_as_parameter(
        name='isld',
        value=isld,
        default_dict=DEFAULTS,
        unique_name_prefix=f'{unique_name}_Isld',
    )
    apply_default_limits(isld, 'isld')

    super().__init__(name=name, unique_name=unique_name)
    self._sld = sld
    self._isld = isld

    if interface is not None:
        self.interface = interface

BaseAssembly

Bases: BaseCore

Assembly of layers.

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

Source code in src/easyreflectometry/sample/assemblies/base_assembly.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class BaseAssembly(BaseCore):
    """Assembly of layers.

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

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

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

        if interface is not None:
            self.interface = interface

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

type property

Get type of the assembly.

Needed by the GUI.

front_layer property writable

Get the front layer in the assembly.

back_layer property writable

Get the back layer in the assembly.

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
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
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:
            self.head_layer.thickness.make_dependent_on(
                dependency_expression='a',
                dependency_map={'a': another_contrast.head_layer.thickness},
            )

        if tail_layer_thickness:
            self.tail_layer.thickness.make_dependent_on(
                dependency_expression='a',
                dependency_map={'a': another_contrast.tail_layer.thickness},
            )

        if head_layer_area_per_molecule:
            self.head_layer._area_per_molecule.make_dependent_on(
                dependency_expression='a',
                dependency_map={'a': another_contrast.head_layer._area_per_molecule},
            )

        if tail_layer_area_per_molecule:
            self.tail_layer._area_per_molecule.make_dependent_on(
                dependency_expression='a',
                dependency_map={'a': another_contrast.tail_layer._area_per_molecule},
            )

        if head_layer_fraction:
            self.head_layer.material._fraction.make_dependent_on(
                dependency_expression='a',
                dependency_map={'a': another_contrast.head_layer.material._fraction},
            )

        if tail_layer_fraction:
            self.tail_layer.material._fraction.make_dependent_on(
                dependency_expression='a',
                dependency_map={'a': 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)

tail_layer property writable

Get the tail layer of the surfactant surface.

head_layer property writable

Get the head layer of the surfactant surface.

constrain_area_per_molecule property writable

Get the area per molecule constraint status.

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
 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
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
190
191
192
193
194
195
196
197
198
199
200
201
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
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
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:
        self.head_layer.thickness.make_dependent_on(
            dependency_expression='a',
            dependency_map={'a': another_contrast.head_layer.thickness},
        )

    if tail_layer_thickness:
        self.tail_layer.thickness.make_dependent_on(
            dependency_expression='a',
            dependency_map={'a': another_contrast.tail_layer.thickness},
        )

    if head_layer_area_per_molecule:
        self.head_layer._area_per_molecule.make_dependent_on(
            dependency_expression='a',
            dependency_map={'a': another_contrast.head_layer._area_per_molecule},
        )

    if tail_layer_area_per_molecule:
        self.tail_layer._area_per_molecule.make_dependent_on(
            dependency_expression='a',
            dependency_map={'a': another_contrast.tail_layer._area_per_molecule},
        )

    if head_layer_fraction:
        self.head_layer.material._fraction.make_dependent_on(
            dependency_expression='a',
            dependency_map={'a': another_contrast.head_layer.material._fraction},
        )

    if tail_layer_fraction:
        self.tail_layer.material._fraction.make_dependent_on(
            dependency_expression='a',
            dependency_map={'a': 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
280
281
282
283
284
285
286
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