Skip to content

material_solvated

DEFAULTS = {'solvent_fraction': {'description': 'Fraction of solvent in layer.', 'value': 0.2, 'unit': 'dimensionless', 'min': 0, 'max': 1, 'fixed': True}} module-attribute

Material

Bases: BaseCore

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

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

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

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

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

        if interface is not None:
            self.interface = interface

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

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

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

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

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

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

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
sld Union[Parameter, float, None]

Real scattering length density. By default, None.

None
isld Union[Parameter, float, None]

Imaginary scattering length density. By default, None.

None
name str

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

'EasyMaterial'
interface

Calculator interface. By default, None.

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

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

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

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

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

    if interface is not None:
        self.interface = interface

MaterialMixture

Bases: BaseCore

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

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        material_a : Union[Material, None], optional
            The first material. By default, None.
        material_b : Union[Material, None], optional
            The second material. By default, None.
        fraction : Union[Parameter, float, None], optional
            The fraction of material_b in material_a. By default, None.
        name : Union[str, None], optional
            Name of the material. By default, None.
        interface :
            Calculator interface. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        if material_a is None:
            material_a = Material(interface=interface)
        if material_b is None:
            material_b = Material(interface=interface)

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

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

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

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

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

        self._materials_constraints()

        if interface is not None:
            self.interface = interface

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

fraction property writable

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

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

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
material_a Union[Material, None]

The first material. By default, None.

None
material_b Union[Material, None]

The second material. By default, None.

None
fraction Union[Parameter, float, None]

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

None
name Union[str, None]

Name of the material. By default, None.

None
interface

Calculator interface. By default, None.

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

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    material_a : Union[Material, None], optional
        The first material. By default, None.
    material_b : Union[Material, None], optional
        The second material. By default, None.
    fraction : Union[Parameter, float, None], optional
        The fraction of material_b in material_a. By default, None.
    name : Union[str, None], optional
        Name of the material. By default, None.
    interface :
        Calculator interface. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    if material_a is None:
        material_a = Material(interface=interface)
    if material_b is None:
        material_b = Material(interface=interface)

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

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

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

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

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

    self._materials_constraints()

    if interface is not None:
        self.interface = interface

from_dict(obj_dict) classmethod

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

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

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

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

MaterialSolvated

Bases: MaterialMixture

Source code in src/easyreflectometry/sample/elements/materials/material_solvated.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
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
class MaterialSolvated(MaterialMixture):
    def __init__(
        self,
        material: Union[Material, None] = None,
        solvent: Union[Material, None] = None,
        solvent_fraction: Union[Parameter, float, None] = None,
        name=None,
        unique_name: Optional[str] = None,
        interface=None,
    ):
        """Constructor.

        Parameters
        ----------
        unique_name : Optional[str], optional
            By default, None.
        material : Union[Material, None], optional
            The material being solvated. By default, None.
        solvent : Union[Material, None], optional
            The solvent material. By default, None.
        solvent_fraction : Union[Parameter, float, None], optional
            Fraction of solvent in layer. E.g. solvation or surface coverage. By default, None.
        name :
            Name of the material. By default, None.
        interface :
            Calculator interface. By default, None.
        """
        if unique_name is None:
            unique_name = global_object.generate_unique_name(self.__class__.__name__)

        if material is None:
            material = Material(sld=6.36, isld=0, name='D2O', interface=interface)
        if solvent is None:
            solvent = Material(sld=-0.561, isld=0, name='H2O', interface=interface)

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

        # In super class, the fraction is the fraction of material b in material a
        super().__init__(
            material_a=material,
            material_b=solvent,
            fraction=solvent_fraction,
            name=name,
            unique_name=unique_name,
            interface=interface,
        )
        if name is None:
            self._update_name()

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

    @material.setter
    def material(self, new_material: Material) -> None:
        """Set the material."""
        self.material_a = new_material

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

    @solvent.setter
    def solvent(self, new_solvent: Material) -> None:
        """Set the solvent."""
        self.material_b = new_solvent

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

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

        This might be the fraction of:
        - solvation where solvent is within the layer, or
        - patches of solvent in the layer where no material is present.
        """
        return self._fraction

    @solvent_fraction.setter
    def solvent_fraction(self, solvent_fraction: float) -> None:
        """Set the fraction of layer covered by the material."""
        if not isinstance(solvent_fraction, (int, float)):
            raise ValueError('solvent_fraction must be a float between 0 and 1')
        if solvent_fraction < 0 or solvent_fraction > 1:
            raise ValueError('solvent_fraction must be between 0 and 1')
        self._fraction.value = solvent_fraction

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

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

    @classmethod
    def from_dict(cls, obj_dict: dict) -> 'MaterialSolvated':
        """Re-route the saved ``solvent_fraction`` Parameter onto ``_fraction``.

        :class:`ModelBase.from_dict` writes the saved Parameter to
        ``_solvent_fraction`` because that's the constructor-arg name, but
        the live `solvent_fraction` property returns ``self._fraction``
        (the field MaterialMixture maintains). Without this override the
        saved fit metadata (fixed/bounds/etc.) is stranded on the unused
        ``_solvent_fraction`` attribute and the active parameter keeps the
        defaults from `__init__`.

        Also re-runs `_materials_constraints` so the parent MaterialMixture's
        mixed `_sld` / `_isld` depend on the live `_fraction`, not the
        temporary Parameter created from the float kwarg.
        """
        instance = super().from_dict(obj_dict)
        saved = instance.__dict__.pop('_solvent_fraction', None)
        if saved is not None:
            old = instance._fraction
            instance._fraction = saved
            try:
                instance._global_object.map.prune(old.unique_name)
            except (AttributeError, KeyError):
                pass
            instance._materials_constraints()
        return instance

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

material property writable

Get material.

solvent property writable

Get solvent.

solvent_fraction_parameter property

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

solvent_fraction property writable

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

This might be the fraction of: - solvation where solvent is within the layer, or - patches of solvent in the layer where no material is present.

__init__(material=None, solvent=None, solvent_fraction=None, name=None, unique_name=None, interface=None)

Constructor.

Parameters:

Name Type Description Default
unique_name Optional[str]

By default, None.

None
material Union[Material, None]

The material being solvated. By default, None.

None
solvent Union[Material, None]

The solvent material. By default, None.

None
solvent_fraction Union[Parameter, float, None]

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

None
name

Name of the material. By default, None.

None
interface

Calculator interface. By default, None.

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

    Parameters
    ----------
    unique_name : Optional[str], optional
        By default, None.
    material : Union[Material, None], optional
        The material being solvated. By default, None.
    solvent : Union[Material, None], optional
        The solvent material. By default, None.
    solvent_fraction : Union[Parameter, float, None], optional
        Fraction of solvent in layer. E.g. solvation or surface coverage. By default, None.
    name :
        Name of the material. By default, None.
    interface :
        Calculator interface. By default, None.
    """
    if unique_name is None:
        unique_name = global_object.generate_unique_name(self.__class__.__name__)

    if material is None:
        material = Material(sld=6.36, isld=0, name='D2O', interface=interface)
    if solvent is None:
        solvent = Material(sld=-0.561, isld=0, name='H2O', interface=interface)

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

    # In super class, the fraction is the fraction of material b in material a
    super().__init__(
        material_a=material,
        material_b=solvent,
        fraction=solvent_fraction,
        name=name,
        unique_name=unique_name,
        interface=interface,
    )
    if name is None:
        self._update_name()

from_dict(obj_dict) classmethod

Re-route the saved solvent_fraction Parameter onto _fraction.

:class:ModelBase.from_dict writes the saved Parameter to _solvent_fraction because that's the constructor-arg name, but the live solvent_fraction property returns self._fraction (the field MaterialMixture maintains). Without this override the saved fit metadata (fixed/bounds/etc.) is stranded on the unused _solvent_fraction attribute and the active parameter keeps the defaults from __init__.

Also re-runs _materials_constraints so the parent MaterialMixture's mixed _sld / _isld depend on the live _fraction, not the temporary Parameter created from the float kwarg.

Source code in src/easyreflectometry/sample/elements/materials/material_solvated.py
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
@classmethod
def from_dict(cls, obj_dict: dict) -> 'MaterialSolvated':
    """Re-route the saved ``solvent_fraction`` Parameter onto ``_fraction``.

    :class:`ModelBase.from_dict` writes the saved Parameter to
    ``_solvent_fraction`` because that's the constructor-arg name, but
    the live `solvent_fraction` property returns ``self._fraction``
    (the field MaterialMixture maintains). Without this override the
    saved fit metadata (fixed/bounds/etc.) is stranded on the unused
    ``_solvent_fraction`` attribute and the active parameter keeps the
    defaults from `__init__`.

    Also re-runs `_materials_constraints` so the parent MaterialMixture's
    mixed `_sld` / `_isld` depend on the live `_fraction`, not the
    temporary Parameter created from the float kwarg.
    """
    instance = super().from_dict(obj_dict)
    saved = instance.__dict__.pop('_solvent_fraction', None)
    if saved is not None:
        old = instance._fraction
        instance._fraction = saved
        try:
            instance._global_object.map.prune(old.unique_name)
        except (AttributeError, KeyError):
            pass
        instance._materials_constraints()
    return instance

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

This function creates a parameter for the variable name.

A parameter has a value and metadata.

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

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

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

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

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

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

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

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