Constraints
EasyReflectometry offers three kinds of constraints between model parameters: equality constraints, derived read-only parameters and inequality constraints. The Constraints tutorial walks through all three on a worked example.
Equality constraints (dependencies)
A parameter can be tied to an arbitrary expression of other parameters. It then leaves the set of free fit parameters and follows the expression.
from easyreflectometry import constrain
from easyreflectometry import constrain_equal
from easyreflectometry import unconstrain
constrain_equal(layer_b.roughness, to=layer_a.roughness)
constrain(layer_b.thickness, '2 * t', t=layer_a.thickness)
unconstrain(layer_b.thickness)
Constraints survive Project save/load: the expression and the
structural paths of the parameters it refers to are stored with the
project, and the graph is rebuilt when it is loaded. This covers the
helpers above; a dependency created by calling make_dependent_on
directly is not recorded.
Derived (read-only) parameters
A derived parameter is a dependent parameter that belongs to no layer: a live calculation that can be shown or referenced from an equality constraint.
from easyreflectometry import constrain_to_sum
from easyreflectometry import derived_parameter
total = derived_parameter('total', 'a + b', a=layer_a.thickness, b=layer_b.thickness)
# keep the film thickness fixed at 120 Å while the split is fitted
constrain_to_sum(layer_b.thickness, [layer_a.thickness, layer_b.thickness], total=120.0)
Warning
A standalone derived parameter is session-only: it has no
structural path, so it cannot be named in an inequality constraint,
and a project whose equality constraints depend on one cannot be
saved (Project.as_dict raises). Numeric totals (as above) are
fine — they are embedded by value.
For a derived value that persists and can be used in inequalities, use
one owned by the model: every Model exposes
total_thickness, the summed thickness of the layers between the
superphase and the subphase, re-derived whenever the layer structure
changes.
Guarding a sum remainder
The parameter tied by constrain_to_sum absorbs whatever the others
leave over, so on its own the constraint lets a fit push the partners
past the total and drive the remainder negative — a layer of negative
thickness. clamp_sum_partners narrows each partner's max to the
headroom it actually leaves, sharing the slack in proportion to the
current values, and restore_sum_partners hands the original maxima
back when the constraint is released.
from easyreflectometry import clamp_sum_partners
from easyreflectometry import is_constrained_to_sum
from easyreflectometry import restore_sum_partners
clamp_sum_partners([layer_a.thickness], remainder=layer_b.thickness.value)
is_constrained_to_sum(layer_b.thickness) # True, also after a project reload
unconstrain(layer_b.thickness)
restore_sum_partners([layer_a.thickness])
The narrowed maxima are persisted by structural path, so removing the constraint after a save/load still gives the original bounds back.
constraints
User-facing helpers for constraining model parameters.
These functions are thin wrappers around the EasyScience
parameter-dependency mechanism (Parameter.make_dependent_on /
Parameter.make_independent). The dependency graph is owned entirely
by EasyScience; constrained parameters are excluded from the free fit
parameters.
Constraints created through this module are preserved when a project is
saved and reloaded: Project.as_dict records the expression and the
structural paths of the parameters it refers to, and Project.from_dict
re-applies them once every parameter exists again. Dependencies created by
calling make_dependent_on directly are outside that contract and are
not persisted.
Cross-parameter inequalities (t_head < t_tail) are not dependencies
but fit penalties; see :mod:easyreflectometry.inequality_constraints.
__all__ = ['clamp_sum_partners', 'constrain', 'constrain_equal', 'constrain_to_sum', 'derived_parameter', 'is_constrained_to_sum', 'restore_sum_partners', 'unconstrain']
module-attribute
USER_CONSTRAINT_FLAG = '_easyreflectometry_user_constraint'
module-attribute
_SUM_TOTAL_ALIAS = 'total'
module-attribute
_SUM_PARTNER_ALIAS = re.compile('p\\d+')
module-attribute
SUM_PARTNER_MAX_BACKUP = '_sum_partner_previous_max'
module-attribute
constrain_equal(parameter, to)
Tie parameter to always equal to.
parameter becomes dependent: it is removed from the free fit
parameters, immediately takes the value of to, and follows it from
then on. Its value, unit, variance, min and max are replaced by
to's, and its fixed flag is cleared. While constrained, the
parameter's value and bounds cannot be set directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
Parameter to make dependent (the follower). |
required |
to
|
DescriptorNumber
|
Parameter (or descriptor) to follow. |
required |
Source code in src/easyreflectometry/constraints.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
constrain(parameter, expression, **parameters)
Tie parameter to an arbitrary expression of other parameters.
Placeholders in expression are supplied as keyword arguments:
.. code-block:: python
constrain(layer_b.thickness, '2 * t', t=layer_a.thickness)
parameter becomes dependent; its value, unit, variance, min and max
are replaced by the evaluated expression and its fixed flag is
cleared. Placeholder names must be valid Python identifiers and not
Python keywords. An unmapped name in the expression that matches a
mathematical builtin (e, pi, sin, ...) evaluates silently
instead of raising NameError, so prefer descriptive placeholder
names.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
Parameter to make dependent (the follower). |
required |
expression
|
str
|
Mathematical expression to evaluate, e.g. |
required |
parameters
|
DescriptorNumber
|
Placeholder-name to parameter mapping for |
{}
|
Source code in src/easyreflectometry/constraints.py
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 | |
unconstrain(parameter)
Remove any constraint from parameter.
Idempotent: calling it on an already-independent parameter is a
no-op. The parameter keeps its current (last evaluated) value and
becomes fittable again. Its bounds, unit, variance and fixed state
are not restored to their pre-constraint values. Review and reset
the bounds before fitting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
Parameter to make independent again. |
required |
Source code in src/easyreflectometry/constraints.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
derived_parameter(name, expression, unit=None, **parameters)
Create a standalone read-only parameter computed from other parameters.
The returned parameter is dependent: it never enters a fit, it
follows expression whenever any of parameters changes, and its
value cannot be set directly — a live calculation to display or reuse
inside another :func:constrain expression.
.. code-block:: python
total = derived_parameter('total', 't1 + t2', t1=layer_1.thickness, t2=layer_2.thickness)
constrain(layer_3.thickness, 'T - t', T=total, t=layer_4.thickness)
.. warning::
A standalone derived parameter belongs to no model, so it has no
structural path (:meth:`~easyreflectometry.Project.parameter_path`
returns ``None``): it cannot be referenced from an inequality
constraint, and a project holding a :func:`constrain` that depends
on one cannot be saved (``Project.as_dict`` raises). It is a
session-only convenience. For a derived value that must survive
save/load or appear in inequalities, use a parameter owned by the
model, such as :attr:`~easyreflectometry.model.Model.total_thickness`.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Display name of the new parameter. |
required |
expression
|
str
|
Mathematical expression over the placeholder names in |
required |
unit
|
Optional[str]
|
Unit the result is converted to. By default the unit produced by the expression is kept. |
None
|
parameters
|
DescriptorNumber
|
Placeholder-name to parameter mapping for |
{}
|
Returns:
| Type | Description |
|---|---|
Parameter
|
A new dependent parameter. |
Source code in src/easyreflectometry/constraints.py
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 | |
constrain_to_sum(parameter, of_parameters, *, total=None)
Constrain parameter so that the sum of of_parameters stays equal to total.
parameter becomes dependent and takes the value
total - sum(other parameters), where "other" means every entry of
of_parameters except parameter itself (it may be listed or not).
This is the "keep the total film thickness fixed while fitting how it
is split" idiom:
.. code-block:: python
constrain_to_sum(layer_b.thickness, [layer_a.thickness, layer_b.thickness], total=120.0)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
Parameter to make dependent (it absorbs the remainder). |
required |
of_parameters
|
Iterable[DescriptorNumber]
|
The parameters whose sum is constrained. |
required |
total
|
Union[DescriptorNumber, numbers.Number, None]
|
The target sum: a parameter/descriptor (e.g. a
:func: |
None
|
Source code in src/easyreflectometry/constraints.py
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 | |
is_constrained_to_sum(parameter, of_parameters=None)
Whether parameter currently carries a :func:constrain_to_sum dependency.
Purely introspective — nothing is changed. Detection reads the public
dependency map and recognizes the alias shape :func:constrain_to_sum
builds ('total' plus 'p0', 'p1', ...), so it also holds for a
constraint restored from a project file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parameter
|
Parameter
|
The parameter that would absorb the remainder. |
required |
of_parameters
|
Optional[Iterable[DescriptorNumber]]
|
When given, every entry (except |
None
|
Source code in src/easyreflectometry/constraints.py
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 | |
clamp_sum_partners(partners, remainder)
Narrow the partners' maxima so a :func:constrain_to_sum remainder cannot go below zero.
Each partner's max is capped at value * (1 + remainder / occupied)
— its share of the remaining budget, distributed in proportion to the
current values. A partner whose max is already tighter is left alone,
and so is one whose cap would collapse onto its min (a degenerate
min == max bound is rejected by EasyScience). The original maxima are
stashed on the parameters for :func:restore_sum_partners.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
partners
|
Iterable[Parameter]
|
The free parameters of the sum (everything except the tied remainder). |
required |
remainder
|
float
|
Current value of the tied remainder parameter. |
required |
Source code in src/easyreflectometry/constraints.py
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 | |
restore_sum_partners(partners)
Give back the maxima :func:clamp_sum_partners narrowed.
Idempotent: a partner without a stashed backup (never clamped, or already restored) is left alone. A backup is only applied when it widens the current bound — restoring never narrows a maximum that was widened in the meantime.
Source code in src/easyreflectometry/constraints.py
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | |