From 92279aea0cd30f7299d7bd0c6bf62cef24c3cceb Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 21 Aug 2026 17:23:39 +0200 Subject: [PATCH 1/7] improved handling of constraints --- .../tutorials/basic/assemblies_library.md | 4 +- docs/docs/tutorials/basic/model.md | 91 +++++++++ src/easyreflectometry/__init__.py | 6 + src/easyreflectometry/constraints.py | 87 +++++++++ tests/unit/test_constraints.py | 184 ++++++++++++++++++ 5 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/easyreflectometry/constraints.py create mode 100644 tests/unit/test_constraints.py diff --git a/docs/docs/tutorials/basic/assemblies_library.md b/docs/docs/tutorials/basic/assemblies_library.md index da419f56..cdb2bdf4 100644 --- a/docs/docs/tutorials/basic/assemblies_library.md +++ b/docs/docs/tutorials/basic/assemblies_library.md @@ -8,7 +8,9 @@ analysis by making chemical and physical constraints available with limited code. In this page, we will document the assemblies that are available with simple examples of the constructors that exist. Full API documentation is also available for the -`easyreflectometry.sample.assemblies` module. +`easyreflectometry.sample.assemblies` module. For custom constraints +between arbitrary parameters, see *Constraining Parameters* in the +[Model tutorial](model.md). ## Multilayer diff --git a/docs/docs/tutorials/basic/model.md b/docs/docs/tutorials/basic/model.md index 6b9c76c3..d1f1e52d 100644 --- a/docs/docs/tutorials/basic/model.md +++ b/docs/docs/tutorials/basic/model.md @@ -85,3 +85,94 @@ This will create a `Model` instance where the resolution function defining the FWHM is determined from a linear interpolation. In the present case the provided data Q-points are (`[0.01, 0.2, 0.31]`) and the corresponding FWHM function values are (`[0.001, 0.043, 0.026]`). + +## Constraining Parameters + +It is often physically motivated to reduce the number of free parameters +in a model by tying parameters together. For example, two layers that +were deposited in the same process step can be expected to have the same +thickness, or the roughness of every interface in a stack can be assumed +to be conformal. Assemblies such as `SurfactantLayer` and `Bilayer` +provide ready-made constraints for their specific chemistry +(`constrain_area_per_molecule`, `conformal_roughness`, +`constrain_multiple_contrast`), but any parameter in a model can be +constrained directly. + +### Tying two parameters together + +The most common constraint is a simple equality. Here the thickness of +`layer_2` is tied to the thickness of `layer_1`. + +```python +from easyreflectometry import constrain_equal + +constrain_equal(layer_2.thickness, to=layer_1.thickness) +``` + +After this call `layer_2.thickness` is no longer an independent +parameter: it immediately takes the value of `layer_1.thickness`, +follows it whenever it changes (including during fitting), and is +removed from the free fit parameters. Only `layer_1.thickness` is varied +by the minimizer. + +### Functional constraints + +Constraints are not limited to equality. An arbitrary mathematical +expression of one or more parameters can be used, where each placeholder +in the expression is supplied as a keyword argument. + +```python +from easyreflectometry import constrain + +# layer_2 is always twice as thick as layer_1 +constrain(layer_2.thickness, '2 * t', t=layer_1.thickness) + +# an SLD that is a fraction-weighted average of two materials +constrain( + mixed.sld, + 'frac * a + (1 - frac) * b', + frac=fraction, + a=solvent.sld, + b=film.sld, +) +``` + +Note that constraining a parameter **overwrites its current value, unit +and bounds** with the evaluated expression, and clears its `fixed` flag. +While constrained, the parameter's value and bounds cannot be set +directly. + +### Removing a constraint + +```python +from easyreflectometry import unconstrain + +unconstrain(layer_2.thickness) +``` + +The parameter keeps its last evaluated value and becomes an independent, +fittable parameter again. Calling `unconstrain` on a parameter that is +not constrained does nothing. The parameter's original bounds and +`fixed` state are **not** restored — they remain whatever the constraint +left behind, so review and reset the bounds before fitting. + +### Things to be aware of + +- Constraints are directional: the dependent parameter follows the + independent one, never the other way around. When chaining constraints + across several objects (as in the multiple-contrast tutorials), make + sure the chain has a single independent parameter at its root. +- Placeholder names in expressions must be valid Python identifiers and + not Python keywords. An unmapped name that happens to match a + mathematical builtin (`e`, `pi`, `sin`, ...) evaluates silently + instead of raising an error — prefer descriptive placeholder names. +- If the model is already attached to a calculator, regenerate the + bindings after changing constraints so the calculator picks up the new + dependency graph: + + ```python + model.generate_bindings() + ``` + +- Custom constraints are not yet preserved when a project is saved and + reloaded — re-apply them after loading a project. diff --git a/src/easyreflectometry/__init__.py b/src/easyreflectometry/__init__.py index a18fee06..72ebcf74 100644 --- a/src/easyreflectometry/__init__.py +++ b/src/easyreflectometry/__init__.py @@ -6,6 +6,9 @@ from importlib import metadata from .analysis.bayesian import PosteriorResults +from .constraints import constrain +from .constraints import constrain_equal +from .constraints import unconstrain from .project import Project try: @@ -17,4 +20,7 @@ 'Project', 'PosteriorResults', '__version__', + 'constrain', + 'constrain_equal', + 'unconstrain', ] diff --git a/src/easyreflectometry/constraints.py b/src/easyreflectometry/constraints.py new file mode 100644 index 00000000..76a7e9d4 --- /dev/null +++ b/src/easyreflectometry/constraints.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""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. + +Note: custom constraints are not yet preserved when a project is saved +and reloaded — the nested-parameter serialization in EasyScience drops +the dependency information. Re-apply constraints after loading a +project. +""" + +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter + +__all__ = ['constrain', 'constrain_equal', 'unconstrain'] + + +def constrain_equal(parameter: Parameter, to: DescriptorNumber) -> None: + """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 + ---------- + parameter : Parameter + Parameter to make dependent (the follower). + to : DescriptorNumber + Parameter (or descriptor) to follow. + """ + parameter.make_dependent_on(dependency_expression='a', dependency_map={'a': to}) + + +def constrain(parameter: Parameter, expression: str, **parameters: DescriptorNumber) -> None: + """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 + ---------- + parameter : Parameter + Parameter to make dependent (the follower). + expression : str + Mathematical expression to evaluate, e.g. ``'2 * t'``. + parameters : DescriptorNumber + Placeholder-name to parameter mapping for `expression`. + """ + parameter.make_dependent_on(dependency_expression=expression, dependency_map=parameters) + + +def unconstrain(parameter: Parameter) -> None: + """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 + ---------- + parameter : Parameter + Parameter to make independent again. + """ + if not parameter.independent: + parameter.make_independent() diff --git a/tests/unit/test_constraints.py b/tests/unit/test_constraints.py new file mode 100644 index 00000000..3bdbe938 --- /dev/null +++ b/tests/unit/test_constraints.py @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +""" +Tests for the user-facing constraint helpers +""" + +import pytest +from easyscience import global_object +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter + +from easyreflectometry import constrain +from easyreflectometry import constrain_equal +from easyreflectometry import unconstrain +from easyreflectometry.project import Project + + +@pytest.fixture(autouse=True) +def clear_global_map(): + global_object.map._clear() + yield + global_object.map._clear() + + +class TestConstrainEqual: + def test_ties_value_and_follows(self): + leader = Parameter('leader', 5.0, unit='angstrom', min=0.0, max=10.0) + follower = Parameter('follower', 1.0, unit='angstrom', min=0.0, max=2.0) + + constrain_equal(follower, to=leader) + + assert follower.independent is False + assert follower.value == 5.0 + leader.value = 7.0 + assert follower.value == 7.0 + + def test_overwrites_bounds_and_clears_fixed(self): + leader = Parameter('leader', 5.0, unit='angstrom', min=0.0, max=10.0) + follower = Parameter('follower', 1.0, unit='angstrom', min=0.5, max=2.0, fixed=True) + + constrain_equal(follower, to=leader) + + assert follower.min == leader.min + assert follower.max == leader.max + assert follower.fixed is False + + def test_dependent_setters_are_locked(self): + leader = Parameter('leader', 5.0) + follower = Parameter('follower', 1.0) + constrain_equal(follower, to=leader) + + with pytest.raises(AttributeError): + follower.value = 3.0 + with pytest.raises(AttributeError): + follower.fixed = True + + def test_descriptor_dependency_gives_degenerate_bounds(self): + # A DescriptorNumber dependency evaluates to a non-Parameter, so + # EasyScience sets min == max == value on the dependent. Pinned + # here because it is why the docs say "reset bounds after + # unconstrain". + leader = DescriptorNumber('leader', 3.0) + follower = Parameter('follower', 1.0, min=0.0, max=2.0) + + constrain_equal(follower, to=leader) + assert follower.min == 3.0 + assert follower.max == 3.0 + + unconstrain(follower) + assert follower.min == 3.0 + assert follower.max == 3.0 + + +class TestConstrain: + def test_scale_expression_follows(self): + leader = Parameter('leader', 5.0, unit='angstrom', min=0.0, max=10.0) + follower = Parameter('follower', 1.0, unit='angstrom', min=0.0, max=2.0) + + constrain(follower, '2 * t', t=leader) + + assert follower.value == 10.0 + leader.value = 6.0 + assert follower.value == 12.0 + + def test_multi_parameter_expression(self): + fraction = Parameter('fraction', 0.25, min=0.0, max=1.0) + sld_a = Parameter('sld_a', 2.0) + sld_b = Parameter('sld_b', 6.0) + mixed = Parameter('mixed', 0.0) + + constrain(mixed, 'frac * a + (1 - frac) * b', frac=fraction, a=sld_a, b=sld_b) + + assert mixed.value == pytest.approx(0.25 * 2.0 + 0.75 * 6.0) + fraction.value = 0.5 + assert mixed.value == pytest.approx(0.5 * 2.0 + 0.5 * 6.0) + + def test_reconstrain_replaces_dependency(self): + first = Parameter('first', 1.0) + second = Parameter('second', 2.0) + follower = Parameter('follower', 0.0) + + constrain_equal(follower, to=first) + constrain_equal(follower, to=second) + assert follower.value == 2.0 + + # No stale updates from the previous target + first.value = 100.0 + assert follower.value == 2.0 + second.value = 3.0 + assert follower.value == 3.0 + + def test_unknown_name_raises_and_reverts(self): + leader = Parameter('leader', 5.0) + follower = Parameter('follower', 1.0) + + with pytest.raises(NameError): + constrain(follower, 'a + b', a=leader) + + assert follower.independent is True + assert follower.value == 1.0 + + +class TestUnconstrain: + def test_removes_constraint_and_keeps_last_value(self): + leader = Parameter('leader', 5.0, min=0.0, max=10.0) + follower = Parameter('follower', 1.0, min=0.0, max=2.0) + constrain_equal(follower, to=leader) + leader.value = 7.0 + + unconstrain(follower) + + assert follower.independent is True + assert follower.value == 7.0 + # Bounds and fixed state are not restored + assert follower.min == 0.0 + assert follower.max == 10.0 + assert follower.fixed is False + # Fittable again + follower.value = 1.5 + assert follower.value == 1.5 + assert leader.value == 7.0 + + def test_idempotent_on_independent_parameter(self): + parameter = Parameter('parameter', 1.0) + unconstrain(parameter) + unconstrain(parameter) + assert parameter.independent is True + + +class TestProjectRoundTrip: + @pytest.mark.xfail( + reason=( + 'EasyScience serializes nested Parameters via the generic arg-spec walk ' + '(SerializerBase._convert_to_dict), which drops _dependency_string / ' + '_independent / serializer ids, and decodes them via cls(**data) instead of ' + 'Parameter.from_dict — so user constraints between model parameters do not ' + 'survive save/load. Fix belongs in easyscience core; this test flips when it lands.' + ), + strict=True, + ) + def test_constraint_survives_as_dict_from_dict(self): + src_project = Project() + src_project._info['name'] = 'Test' + src_project.default_model() + src_project._with_experiments = False + sample = src_project.models[0].sample + leader = sample[1].layers[0].thickness + follower = sample[2].layers[0].thickness + constrain(follower, '2 * t', t=leader) + assert follower.value == 2 * leader.value + + project_dict = src_project.as_dict() + global_object.map._clear() + + project = Project() + project.from_dict(project_dict) + sample = project.models[0].sample + leader = sample[1].layers[0].thickness + follower = sample[2].layers[0].thickness + + assert follower.independent is False + leader.value = 42.0 + assert follower.value == 84.0 From aeff90a8ec3b859ef8907bf8616fd358668bfcc3 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 21 Aug 2026 17:54:05 +0200 Subject: [PATCH 2/7] ruff --- docs/docs/tutorials/basic/assemblies_library.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/tutorials/basic/assemblies_library.md b/docs/docs/tutorials/basic/assemblies_library.md index cdb2bdf4..fdb7e9c5 100644 --- a/docs/docs/tutorials/basic/assemblies_library.md +++ b/docs/docs/tutorials/basic/assemblies_library.md @@ -9,7 +9,7 @@ limited code. In this page, we will document the assemblies that are available with simple examples of the constructors that exist. Full API documentation is also available for the `easyreflectometry.sample.assemblies` module. For custom constraints -between arbitrary parameters, see *Constraining Parameters* in the +between arbitrary parameters, see _Constraining Parameters_ in the [Model tutorial](model.md). ## Multilayer From e037a8587d507f3bb7a3906619b2186918cb92a6 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 21 Aug 2026 18:00:10 +0200 Subject: [PATCH 3/7] wording --- docs/docs/tutorials/basic/model.md | 6 +++--- src/easyreflectometry/constraints.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/tutorials/basic/model.md b/docs/docs/tutorials/basic/model.md index d1f1e52d..2076a89d 100644 --- a/docs/docs/tutorials/basic/model.md +++ b/docs/docs/tutorials/basic/model.md @@ -153,7 +153,7 @@ unconstrain(layer_2.thickness) The parameter keeps its last evaluated value and becomes an independent, fittable parameter again. Calling `unconstrain` on a parameter that is not constrained does nothing. The parameter's original bounds and -`fixed` state are **not** restored — they remain whatever the constraint +`fixed` state are **not** restored. They remain whatever the constraint left behind, so review and reset the bounds before fitting. ### Things to be aware of @@ -165,7 +165,7 @@ left behind, so review and reset the bounds before fitting. - Placeholder names in expressions must be valid Python identifiers and not Python keywords. An unmapped name that happens to match a mathematical builtin (`e`, `pi`, `sin`, ...) evaluates silently - instead of raising an error — prefer descriptive placeholder names. + instead of raising an error. - If the model is already attached to a calculator, regenerate the bindings after changing constraints so the calculator picks up the new dependency graph: @@ -175,4 +175,4 @@ left behind, so review and reset the bounds before fitting. ``` - Custom constraints are not yet preserved when a project is saved and - reloaded — re-apply them after loading a project. + reloaded. Re-apply them after loading a project. diff --git a/src/easyreflectometry/constraints.py b/src/easyreflectometry/constraints.py index 76a7e9d4..bb575b81 100644 --- a/src/easyreflectometry/constraints.py +++ b/src/easyreflectometry/constraints.py @@ -10,7 +10,7 @@ parameters. Note: custom constraints are not yet preserved when a project is saved -and reloaded — the nested-parameter serialization in EasyScience drops +and reloaded. The nested-parameter serialization in EasyScience drops the dependency information. Re-apply constraints after loading a project. """ @@ -75,7 +75,7 @@ def unconstrain(parameter: Parameter) -> None: 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 + are not restored to their pre-constraint values. Review and reset the bounds before fitting. Parameters From c60cb4340291ce09ee1f972dfb13714003854b9d Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 21 Aug 2026 19:10:23 +0200 Subject: [PATCH 4/7] additional cell in a notebook to showcase the new way of doing constraints --- .../advancedfitting/multi_contrast.ipynb | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb index 1c482386..0480196d 100644 --- a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb +++ b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb @@ -383,11 +383,41 @@ "cell_type": "markdown", "id": "01abb1a2-1c77-4c59-b8ae-58fc29c00857", "metadata": {}, - "source": [ - "Even through only as single value (that for the d13-DSPC head thickness) was changed, all three values changed. \n", - "\n", - "Having constructed each of the surfactant layer object and implemented the constraints, we can now build Samples and models. " - ] + "source": "Even through only as single value (that for the d13-DSPC head thickness) was changed, all three values changed. " + }, + { + "cell_type": "markdown", + "id": "d375c89c", + "source": "### Custom constraints\n\nThe assembly methods used above cover the common chemical relationships, but arbitrary constraints between any two parameters are also possible with the `constrain`, `constrain_equal` and `unconstrain` helpers (see *Constraining Parameters* in the [model tutorial](../basic/model.md)). As a demonstration, we tie the head thickness of the base contrast to always be half the tail thickness. Since the other contrasts are chained to `d13d2o`, the constraint propagates to all of them. ", + "metadata": {} + }, + { + "cell_type": "code", + "id": "041f4e4c", + "source": "from easyreflectometry import constrain, unconstrain\n\nconstrain(d13d2o.head_layer.thickness, '0.5 * t', t=d13d2o.tail_layer.thickness)\nd13d2o.tail_layer.thickness.value = 22\nd13d2o.head_layer.thickness.value, d70d2o.head_layer.thickness.value, d83acmw.head_layer.thickness.value", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "d62ee7bd", + "source": "Changing the tail thickness now also updates every head thickness. Note that constraining overwrites the dependent parameter's value and bounds, and `unconstrain` does not restore them. We will not use this constraint in the fit below, so we remove it again and restore the original values before building the models. ", + "metadata": {} + }, + { + "cell_type": "code", + "id": "1465c350", + "source": "unconstrain(d13d2o.head_layer.thickness)\nd13d2o.tail_layer.thickness.value = 20\nd13d2o.head_layer.thickness.value = 10", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "b7e83a2d", + "source": "Having constructed each of the surfactant layer object and implemented the constraints, we can now build Samples and models. ", + "metadata": {} }, { "cell_type": "code", From fff5716309f28eefbe1ec5ec427d71485f2239ae Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 21 Aug 2026 19:27:52 +0200 Subject: [PATCH 5/7] fixed notebook --- .../advancedfitting/multi_contrast.ipynb | 17 +++++++++++--- .../tutorials/fitting/material_solvated.ipynb | 9 +++++++- docs/docs/tutorials/fitting/monolayer.ipynb | 10 ++++++++- docs/docs/tutorials/fitting/repeating.ipynb | 9 +++++++- .../tutorials/fitting/simple_fitting.ipynb | 9 +++++++- .../simulation/resolution_functions.ipynb | 22 ++++++++++++++++++- 6 files changed, 68 insertions(+), 8 deletions(-) diff --git a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb index 0480196d..ab9402ee 100644 --- a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb +++ b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb @@ -72,7 +72,14 @@ "id": "694b4e5e-2d1a-402e-aa3f-a26cc82f7774", "metadata": {}, "outputs": [], - "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/multiple.ort',\n known_hash='241bcb819cdae47fbbb310a99c2456c7332312719496b936a153dc7dee83e62c',\n)\ndata = load(file_path)" + "source": [ + "file_path = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/multiple.ort',\n", + " known_hash='241bcb819cdae47fbbb310a99c2456c7332312719496b936a153dc7dee83e62c',\n", + ")\n", + "data = load(file_path)" + ] }, { "cell_type": "markdown", @@ -394,7 +401,7 @@ { "cell_type": "code", "id": "041f4e4c", - "source": "from easyreflectometry import constrain, unconstrain\n\nconstrain(d13d2o.head_layer.thickness, '0.5 * t', t=d13d2o.tail_layer.thickness)\nd13d2o.tail_layer.thickness.value = 22\nd13d2o.head_layer.thickness.value, d70d2o.head_layer.thickness.value, d83acmw.head_layer.thickness.value", + "source": "from easyreflectometry import constrain # noqa: E402\nfrom easyreflectometry import unconstrain # noqa: E402\n\nconstrain(d13d2o.head_layer.thickness, '0.5 * t', t=d13d2o.tail_layer.thickness)\nd13d2o.tail_layer.thickness.value = 22\nd13d2o.head_layer.thickness.value, d70d2o.head_layer.thickness.value, d83acmw.head_layer.thickness.value", "metadata": {}, "execution_count": null, "outputs": [] @@ -408,7 +415,11 @@ { "cell_type": "code", "id": "1465c350", - "source": "unconstrain(d13d2o.head_layer.thickness)\nd13d2o.tail_layer.thickness.value = 20\nd13d2o.head_layer.thickness.value = 10", + "source": [ + "unconstrain(d13d2o.head_layer.thickness)\n", + "d13d2o.tail_layer.thickness.value = 20\n", + "d13d2o.head_layer.thickness.value = 10" + ], "metadata": {}, "execution_count": null, "outputs": [] diff --git a/docs/docs/tutorials/fitting/material_solvated.ipynb b/docs/docs/tutorials/fitting/material_solvated.ipynb index e64f7f7a..cc7f8695 100644 --- a/docs/docs/tutorials/fitting/material_solvated.ipynb +++ b/docs/docs/tutorials/fitting/material_solvated.ipynb @@ -96,7 +96,14 @@ "id": "a95a39dd-d0eb-4029-9dc8-41e6e7918f66", "metadata": {}, "outputs": [], - "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n)\ndata = load(file_path)" + "source": [ + "file_path = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n", + " known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n", + ")\n", + "data = load(file_path)" + ] }, { "cell_type": "markdown", diff --git a/docs/docs/tutorials/fitting/monolayer.ipynb b/docs/docs/tutorials/fitting/monolayer.ipynb index a04bba8f..e3f90336 100644 --- a/docs/docs/tutorials/fitting/monolayer.ipynb +++ b/docs/docs/tutorials/fitting/monolayer.ipynb @@ -92,7 +92,15 @@ "id": "e392660e-6f02-4f0b-be86-4c8ea78883e0", "metadata": {}, "outputs": [], - "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/d70d2o.ort',\n known_hash='3e4750536621be8eec493fa21a287e408d384f29cacb113b71d02690d99f0998',\n)\ndata = load(file_path)\nplot(data)" + "source": [ + "file_path = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/d70d2o.ort',\n", + " known_hash='3e4750536621be8eec493fa21a287e408d384f29cacb113b71d02690d99f0998',\n", + ")\n", + "data = load(file_path)\n", + "plot(data)" + ] }, { "cell_type": "markdown", diff --git a/docs/docs/tutorials/fitting/repeating.ipynb b/docs/docs/tutorials/fitting/repeating.ipynb index 156a1229..ea35dffc 100644 --- a/docs/docs/tutorials/fitting/repeating.ipynb +++ b/docs/docs/tutorials/fitting/repeating.ipynb @@ -98,7 +98,14 @@ "id": "7121c7e9", "metadata": {}, "outputs": [], - "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/repeating_layers.ort',\n known_hash='a5ffca9fd24f1d362266251723aec7ce9f34f123e39a38dfc4d829c758e6bf90',\n)\ndata = load(file_path)" + "source": [ + "file_path = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/repeating_layers.ort',\n", + " known_hash='a5ffca9fd24f1d362266251723aec7ce9f34f123e39a38dfc4d829c758e6bf90',\n", + ")\n", + "data = load(file_path)" + ] }, { "cell_type": "markdown", diff --git a/docs/docs/tutorials/fitting/simple_fitting.ipynb b/docs/docs/tutorials/fitting/simple_fitting.ipynb index 27d1e485..ce6b09eb 100644 --- a/docs/docs/tutorials/fitting/simple_fitting.ipynb +++ b/docs/docs/tutorials/fitting/simple_fitting.ipynb @@ -94,7 +94,14 @@ "id": "7d851064-605c-4f80-a510-197bcdbff2ea", "metadata": {}, "outputs": [], - "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n)\ndata = load(file_path)" + "source": [ + "file_path = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n", + " known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n", + ")\n", + "data = load(file_path)" + ] }, { "cell_type": "markdown", diff --git a/docs/docs/tutorials/simulation/resolution_functions.ipynb b/docs/docs/tutorials/simulation/resolution_functions.ipynb index d46a76cb..46fe1fc9 100644 --- a/docs/docs/tutorials/simulation/resolution_functions.ipynb +++ b/docs/docs/tutorials/simulation/resolution_functions.ipynb @@ -101,7 +101,27 @@ "id": "609174e5-1371-412d-a29f-cb05bfe36df0", "metadata": {}, "outputs": [], - "source": "file_path_0 = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-0.0.ort',\n known_hash='f8a3e7007b83f0de4e2c761134e7d1c55027f0099528bd56f746b50349369f50',\n)\nfile_path_1 = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-1.0.ort',\n known_hash='9d81a512cbe45f923806ad307e476b27535614b2e08a2bf0f4559ab608a34f7a',\n)\nfile_path_10 = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-10.0.ort',\n known_hash='991395c0b6a91bf60c12d234c645143dcac1cab929944fc4e452020d44b787ad',\n)\ndict_reference = {}\ndict_reference['0'] = load(file_path_0)\ndict_reference['1'] = load(file_path_1)\ndict_reference['10'] = load(file_path_10)" + "source": [ + "file_path_0 = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-0.0.ort',\n", + " known_hash='f8a3e7007b83f0de4e2c761134e7d1c55027f0099528bd56f746b50349369f50',\n", + ")\n", + "file_path_1 = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-1.0.ort',\n", + " known_hash='9d81a512cbe45f923806ad307e476b27535614b2e08a2bf0f4559ab608a34f7a',\n", + ")\n", + "file_path_10 = pooch.retrieve(\n", + " # Fetch test data from the easyscience/reflectometry data repository\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-10.0.ort',\n", + " known_hash='991395c0b6a91bf60c12d234c645143dcac1cab929944fc4e452020d44b787ad',\n", + ")\n", + "dict_reference = {}\n", + "dict_reference['0'] = load(file_path_0)\n", + "dict_reference['1'] = load(file_path_1)\n", + "dict_reference['10'] = load(file_path_10)" + ] }, { "cell_type": "code", From 237eb0d0bf67f714b7d2d6152d7068ec2510de6c Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Tue, 25 Aug 2026 18:23:55 +0200 Subject: [PATCH 6/7] initial checkin --- .../advancedfitting/constraints.ipynb | 825 ++++++++++++++++++ .../advancedfitting/multi_contrast.ipynb | 9 +- docs/docs/tutorials/index.md | 4 + docs/src/api/api.rst | 9 + docs/src/api/constraints.rst | 76 ++ pixi.lock | 119 ++- pyproject.toml | 3 +- src/easyreflectometry/__init__.py | 6 + src/easyreflectometry/constraints.py | 123 ++- src/easyreflectometry/fitting.py | 63 +- .../inequality_constraints.py | 435 +++++++++ src/easyreflectometry/model/model.py | 60 ++ .../model/model_collection.py | 18 +- src/easyreflectometry/orso_utils.py | 30 +- src/easyreflectometry/project.py | 174 +++- .../sample/assemblies/base_assembly.py | 54 ++ src/easyreflectometry/summary/summary.py | 8 +- src/easyreflectometry/utils.py | 12 +- tests/model/test_model_collection.py | 21 + tests/test_orso_utils.py | 75 ++ tests/test_project.py | 9 +- tests/unit/test_constraints.py | 16 +- tests/unit/test_derived_parameters.py | 180 ++++ tests/unit/test_inequality_constraints.py | 273 ++++++ 24 files changed, 2500 insertions(+), 102 deletions(-) create mode 100644 docs/docs/tutorials/advancedfitting/constraints.ipynb create mode 100644 docs/src/api/constraints.rst create mode 100644 src/easyreflectometry/inequality_constraints.py create mode 100644 tests/unit/test_derived_parameters.py create mode 100644 tests/unit/test_inequality_constraints.py diff --git a/docs/docs/tutorials/advancedfitting/constraints.ipynb b/docs/docs/tutorials/advancedfitting/constraints.ipynb new file mode 100644 index 00000000..16db5c2c --- /dev/null +++ b/docs/docs/tutorials/advancedfitting/constraints.ipynb @@ -0,0 +1,825 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c8077e80", + "metadata": {}, + "source": [ + "# Constraints: equalities, derived parameters and inequalities\n", + "\n", + "Real samples rarely consist of fully independent parameters: layers grown in the same process share a roughness, a film has a known total thickness, or physics dictates an ordering such as $t_\\mathrm{head} < t_\\mathrm{tail}$.\n", + "`easyreflectometry` offers three kinds of constraints to express this, and all of them survive saving and reloading a project:\n", + "\n", + "1. **Equality constraints** (dependencies) — a parameter follows an expression of other parameters and leaves the fit: `constrain`, `constrain_equal`, `unconstrain`.\n", + "2. **Derived read-only parameters** — live calculations such as `Model.total_thickness` or your own `derived_parameter`; they can be referenced from other constraints, and `constrain_to_sum` uses them to keep a total fixed while the split is fitted.\n", + "3. **Inequality constraints** — `t_A < t_B` or `t_A + t_B < 90` declared on the project and *enforced during fitting* as penalties on the BUMPS fit problem. Only the BUMPS minimizers (and the DREAM sampler) support them; LMFit and DFO-LS refuse with a clear error rather than silently ignoring physics.\n", + "\n", + "In this tutorial we build a simple two-layer film, visualize it, apply each kind of constraint, and run an inequality-constrained fit on simulated data whose *true* answer violates the constraint — so we can watch the fit land exactly on the boundary of the allowed region." + ] + }, + { + "cell_type": "markdown", + "id": "e0136841", + "metadata": {}, + "source": [ + "First configure matplotlib to place figures in the notebook and import the needed modules." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a2121e31", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae11c47d", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from easyscience import global_object\n", + "from easyscience.fitting import AvailableMinimizers\n", + "\n", + "from easyreflectometry import InequalitySpec\n", + "from easyreflectometry import Project\n", + "from easyreflectometry import constrain\n", + "from easyreflectometry import constrain_to_sum\n", + "from easyreflectometry import derived_parameter\n", + "from easyreflectometry import unconstrain\n", + "from easyreflectometry.data import DataSet1D\n", + "from easyreflectometry.sample import Layer\n", + "from easyreflectometry.sample import Material\n", + "from easyreflectometry.sample import Multilayer" + ] + }, + { + "cell_type": "markdown", + "id": "7821b513", + "metadata": {}, + "source": [ + "## Building the sample\n", + "\n", + "We start from a default project and replace its film with two layers on a silicon substrate:\n", + "\n", + "| | material | SLD (10⁻⁶ Å⁻²) | thickness (Å) |\n", + "|---|---|---|---|\n", + "| superphase | vacuum | 0.0 | ∞ |\n", + "| film A | MatA | 3.0 | 40 |\n", + "| film B | MatB | 5.0 | 60 |\n", + "| subphase | Si | 2.07 | ∞ |" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f98cfbdd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Vacuum Layer', 'A', 'B', 'Si Layer']" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "project = Project()\n", + "project.default_model()\n", + "model = project.models[0]\n", + "\n", + "film_a = Multilayer(Layer(Material(3.0, 0.0, 'MatA'), thickness=40.0, roughness=3.0, name='A'), name='Film A')\n", + "film_b = Multilayer(Layer(Material(5.0, 0.0, 'MatB'), thickness=60.0, roughness=3.0, name='B'), name='Film B')\n", + "substrate = model.sample[-1]\n", + "model.remove_assembly(len(model.sample) - 1) # drop the default D2O / Si pair ...\n", + "model.remove_assembly(len(model.sample) - 1)\n", + "model.add_assemblies(film_a, film_b, substrate) # ... and insert our film\n", + "\n", + "t_a = film_a.layers[0].thickness\n", + "t_b = film_b.layers[0].thickness\n", + "[layer.name for assembly in model.sample for layer in assembly.layers]" + ] + }, + { + "cell_type": "markdown", + "id": "04b200c1", + "metadata": {}, + "source": [ + "To make the discussion easier to follow we define a small helper that draws the sample twice: as a **layer stack** (the beam arrives from the top; hatched media are semi-infinite) and as the corresponding **SLD profile** computed by the calculator. We will call it after every change so the effect of each constraint is visible." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8ba361b0", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6sAAAGZCAYAAABvz6cKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAaFRJREFUeJzt3Qd0VNXWwPE96ZWQECChgwgISBfEioJgQ2xPxYaKYnsqyFPEJyo8FdQn+lBU1E+wYO8iiBQBUVRA6dJ7C6GEhITUmW/tk8yYhJSZEDLl/n9rjZk+515i7uy799nH5nA4HAIAAAAAgA8J8vYAAAAAAAAojWAVAAAAAOBzCFYBAAAAAD6HYBUAAAAA4HMIVgEAAAAAPodgFQAAAADgcwhWAQAAAAA+h2AVAAAAAOBzCFYBAAAAAD6HYBUAAAA+b/HixXLGGWdIdHS02Gw2WbZsmTz55JPmenHNmjWTW265xWvjBFB9CFaBADdlyhRzIN+6dau3hwIAqKKVK1fK1VdfLU2bNpWIiAhp2LChXHDBBfLyyy8fE6hdeumlFb6XBnJ6XHBeYmJipEWLFub9P//8c7Hb7eJr8vLy5B//+IccPHhQXnzxRXnvvffMvgAQ2AhW4be+/fZbCQoKkr1791b63IKCAmnQoIE5KM+YMaNKn9e1a1e55557Kg0Ki1/q1asn5513nsef6c625ebmyv/+9z/p3Lmz1KpVS2rXri3t2rWTIUOGyNq1az36PACA7/rll1+kW7dusnz5crnjjjvklVdekdtvv90cJ/Q4UBXh4eEm4NOLBn/XX3+9bNiwwQSsvXv3lvT0dPElmzZtkm3btsm//vUvc5y78cYbJT4+Xh577DE5evSot4cH4AQJOVFvDJxo3333nQkgk5KSKn3u3LlzZc+ePeaM89SpU+Wiiy7y6LP0tX/++aeMGTOm0ufqc5o3by4Oh0NSUlJMEHvxxRebALSys92ebNtVV11lguCBAweaLy961lmD1GnTppkyqTZt2pjn3XTTTXLdddeZLyYAAP/z9NNPS1xcnCmD1ROTxe3bt69K7xkSEmICvuKeeuopGTdunIwcOdIcVz7++GM5UfLz800GNywszK3nO7ez9PbrdugFQGAiswq/NX36dLnkkkvceu77778vXbp0kWHDhslXX30lmZmZHn2WBoVadnX++edX+lwNhPULgAaJegb4p59+ktDQUPnwww+rbdv0C4sGpRoY61lxzfg+8MAD8tprr5ly3/79+7ueGxwcbMZeek4PAMA/aFZRK2dKB2pKK3iq0yOPPCJ9+/aVTz/9VNavX19pObGWEG/evFn69etn5pJqFZMem/SErZMel/QY9N///ldeeuklOemkk8wJ1DVr1rhOKJ999tnm9bqNAwYMkL/++qvE55x77rnmupYC63v16tXL3C5rzmpZ0tLSZOjQodK4cWPz2S1btpRnn33WJ0ueAfyNYNXHfPbZZ+aP7vz58495bNKkSeaxVatWmdsrVqwwf8B1nokGI5qFu+222+TAgQPHvHbXrl0yePBgcxDRP9Ka+bv77rtNKWlFf+zLmu+ot/X5pZVuaOB87cKFC+X++++XunXrmoPQnXfeaT5XDxw333yzKePRy8MPP1zi4FbZ3J0dO3a4FaxqedCXX35psovXXHONuf3111+LJzTTqeW8kZGR4indZn2du2d+3dk2/eKizjzzzGMe0+C0Tp06rtvMWQUA/6ZzM5cuXeo6/p9oerJVj8ezZs1ya5rNhRdeKPXr15fnnnvOVAU98cQT5lLa5MmTzRxbLeN94YUXJCEhQWbPnm0CXc2c6neLBx980JQ96/HNedzS7w2PPvqoua7fJ/Qk7b///W+3tycrK8sEu3riWr93TJgwwby/ZpD18wD4LuomfIwGKHqW8pNPPnGdRXTSchw9s9q+fXtzWw8iejbz1ltvNYHq6tWr5Y033jA/f/31V1fwuXv3bunevbsJDvUAoeWhGrxqYKx/wN0twamq++67z4xv9OjRZlw6Rg3g9GDUpEkTeeaZZ0wm8fnnnzfbpgeSyujz9WyyzuGpzDfffCNHjhwxwaqOQ8/Gaimwzs9xh5bX6sFUx+mOw4cPy/79+82BXg++emDWzy9dbnU82+ZsKqHboQdcSqAAIHBplY5W7XTq1MkczzULqfNK9SSqVu5UN+f3DOeJ0YpkZ2ebYFUDQKWVPlrdo1lLDSwTExNdz925c6ds3LjRnLx20iyqBq2LFi0yP9Xll19u+jFowPvOO+9Iz549JScnxxyHddt1Xq0nxo8fb7ZFp/OcfPLJrgBYT+Drd4/hw4ebjCsA30Nm1cdoBk7/yGsgqWcrnbTRjmZbr732Wtd9ekBYsGCBjBo1yswt0dKat99+W37//XeTzXTSM4fO12sTBf0DrSU6Wn6jc2BOND3bqgGYjvfdd981Bx1nYKrBlmZ4tTS3UaNGZvzuZjr1wO1O6Y+eSdU5nM4DkQatP/zwg6Smprr1WVrGq40m3C057tOnjzkQa8Cp26iZTd0u7dpYXdt2+umnm5MZb775ptlvGni/+uqrsn37drc+AwDgP/T4ocHcZZddZposaQZTs5HaEVhPyFY3PWmuMjIy3Hr+P//5T9d1PXbpba2g0hO9pXstFA9UtR+ELj+jVVnOQFV16NDBbLN+d6gOWtKsQa5WcenJZOdFj9f6XUu/SwHwTQSrPkgDUs3IzZs3z3WfBq86r6J4sFq8JFXPbOofXg1i1B9//GF+6ms0ENQAuKxMXU3MY9Ty4+Kf06NHD5N11PuLl67q+DRTXBnNEOtB253gUUuiZ86caZoQFT9Y6ng0e+0OPVi2bdvWlDm7Y+LEiSbrrRcNlPXMt3Zt/OKLL6pt23T8ul3aDEMPvjof9t577zUZV/0d0fcBAASO0047zRxHDh06ZE5K64loDSY1y+ic+1ldtBpIxcbGVvpc7Uis05GKa9WqlflZevqJTkEqTrv7qtatWx/zvqeccor5XuNpj4myaJfj77//3gTKxS8arB5PkyoAJx61gz5Iy2k046llv1rmo/S6lv84DwBK1xrT0tqPPvromD+0WoqqNHuoWUFnSY83aKlvcc5sbumSG71fD8KV0SBNaQOIyuh+0zJeLSfS0qPiAbNmdTXAcyfTWbxhUWW0RKv4iQENlPXz9UyzdgOuqOzak23Tucc6Z0cvenZaM+e6hIEG4VoWpoEyACCw6DFEA1e96HcCnQqkmcOy5ohWlXNurDYhqk5V6ftQHfTEvWZqtTdGWYp/twLgWwhWfZAGITpfQ5sCaWmnLn/y888/HzNnUpsF6bzPhx56yASyWrajf5A12PW0u115GdbipciVKe+5mjV19353GixpplPnabpTwqwBaXmNiJRmckufES5uy5YtZjkY7bJbVXrWWbOrGkjq2V2dd1wd21ZccnKyKW/WrLG+vwasWn7MXFYACFzOE6N6wrI6aQMj/V7gzvQV/b6hx9LiAZ+zi3BlFUnO/gvr1q075jE99up8V+0QfLy0+7Bmi52ZVAD+gzJgH6WlnFr+MmfOHHPGVIO44iXAmoHUx7TFvGZXr7jiCnNQKR14aZlLrVq1Ku0gqKWkqnT5qLNEp/RzSz9P56ZU98GyLLoftJTHnRJgDTQ1mNeMpu7D4hfNuOrZ6Q8++KDSrKoGjmedddZxrydXvLTqeLetPJpR1bk+mk3W3x8AgP/78ccfyzyZ65zTWVYZbVXpOqva10G/czibEVXmlVdecV3XceptPR45q8MqOtGqJ9u1iVLx7xX6nUXHoGuUVwc9ua9TbJzVS8Xp5zqP0QB8D2kXH6Vn/7TZgAZVutaYlpYWn+vhzEqWPnhpk6XSWT3N0mpJ6JIlS46Zt6qv17OnetZRaZMBbeCgdJ6IHkBK0+eWbkagHX49ycJWla4vqiXP7gR0zqyqlv2U1eXvrbfeMs957LHHyn0P/SKgJbnHk6HUwFEPuhoc6xyc6tg2zdBqBr50ibVzzqueUCjexAIA4L+0q75279cT09rRX08Q68lY/Y6g2UstBS5Op71oT4PSdEqK8xijAZpzuoj2vdCT09qsSZfF02ogPa67Q5fO0xOtgwYNMlNsdF1yPdGrS824cxzShovaVFCbL2ovC11eTrvo64nispbJqwqtQNNt06k42sxJl9fR7zi6VJz2BNG5tcW7FgPwHQSrPkrPSF555ZVmPqr+QdWFtIvTbOk555xjOgJqMKQdATUg0mxiaVo+rI9p91hdukYDJs2CaoZRuwbrMjIakGngowcK/aOuwbB2sNUDTekOs9os6K677jIlp5rN1c6EerayJv7Q6wFQD8za8KgyGojqGdvy2tFrUK5fALQZVZcuXY55XA+Yejb79ddf92iMeqDW8iWlwadmbzW41Cy4/rtVx7bpPtcOwHqA1w6HemJDlyPSkwu6VJGetCiv/BoA4F/0O4Aes/UEqgaRGqzqMVu77OsJVz2OF6dltbpSQGl6jHcGq7oUjK6nqqKiokwHew3iHn/8cRMU68lud+ixRoNV7eyv3x+0KZPOn9X3cffkvL7e+Rr9/qPfV3Tpm9INmapKt0/7Ouj3Id2PujKBHo+1dFmr02piZQQAVeSAz5o1a5amTR02m82xY8eOYx7fuXOn44orrnDUrl3bERcX5/jHP/7h2L17t3nNE088UeK527Ztc9x8882OunXrOsLDwx0tWrRw3HvvvY6cnBzXc5YuXero0aOHIywszNGkSRPH+PHjHZMnTzbvt2XLFtfzCgoKHCNGjHAkJiY6oqKiHP369XNs3LjR0bRpU8egQYNcz3O+dvHixSXGomPT+1NTU0vcr6+Njo6ucJ9069bNcc8991S673Rb9DNGjRpV7nO2bt1qnjNs2LAyH582bZrZ9ykpKQ53OLe3+CUiIsLRqVMnx2uvveaw2+3Vsm1KxzRu3DjHueee60hOTnaEhIQ44uPjHeeff77js88+K3Ncxf8NAQA4Xu4ctwHgeNj0P1UNdIGapI2mdH7LtGnTqm0eS0X0jLWWTusSAYG2bQAAHC8tqdUy2or6MQDA8aAMGH5Dl+PREiGdS1MTtITYkyVr/GnbAAAAAF9HZhUAAAAeI7MK4EQjWAUAAAAA+BzWWQUAAAAA+BzmrAIA3GK3283SSLo0ha7PDAAAUBVa3JuRkSENGjSocKmsGg1WdQ1JXf/x8ssvr8mPBQBUAw1Uy1u3GAAAwFM7duyQRo0alfs4mVUAgFs0o+o8sNSqVcvbwwEAAH4qPT3dnAB3frcoD8EqAMAtztJfDVQJVgEAwPGqbFpRjTdYWr16tXTp0sV80enXr58pK1P79u2TG264QZKTk03t8tChQyUnJ8c8pi3RBwwYIPXq1ZO4uDg555xzZPny5a73fPLJJ+XSSy+VO++80zzevHlzmTdvnnz11VfSsmVLiY+Pl3//+981vakAAAAAgCqq8WD1rbfekg8++ED27t0rSUlJcuONN5oJtpdddpm5vWnTJlm5cqUJRp966ilXU4/rr79etmzZIikpKdK5c2e55pprzOucfvjhBxP8Hjx4UG666Sbzvl9//bV5n59//lleeOEF+eOPP2p6cwEAAAAAvr7OqjZYuueee+Thhx82tzXw1AB1wYIFpulSamqqqxvUrFmz5K677jLBa2lpaWkmW7pz505p2LChyazOnDlTFi1aZB5fs2aNtGvXTtauXSutW7c293Xv3l2GDBkit99+e01tLgAE3PwSrV45fPgwZcAAAOCEf6eo8TmrTZs2dV2vX7++hIeHyy+//GIC0ISEBNdjGkMXFBSY60ePHpXhw4fL9OnTTebUGdDu37/fBKvO93KKiooq8z4tJwYAAAAA+L4aD1a3bdvmuq7zVHVe6plnnmnmo+7Zs6fM12gJ79KlS2XhwoWmtbEzs1qDSWEAAAAAQCDPWZ00aZKsW7fOZEtHjBhhmiX17NnTtC5+7LHHzOKwGoRqUDtjxgxXmjgiIsIEqJodffTRR2t62AAAAACAQA5Wb7vtNhk4cKAp0d21a5dMnTpVgoODZdq0aeb2KaecYuqXL7nkEtm4caN5zYMPPmieo69p3769CW4BAAAAAIGrRhssAQD8Fw2WAABATX6nqPHMKgAAAAAAlSFYBQAL0CW+bDZbiUubNm28PSwAAADf6QYMAPAOXX969uzZrtshIRwCAACA7+KbCgBYhAanSUlJbj9flxbTS/H5JQDgifRcu6w5nCd/pefLuvR82ZddIAdy7HIwxy6Hcu2SXeCQPIdIrvOn3SH5dhGHFLZUKd5ZpXSTleK3K3peRWzl3W+rwmsq+pxyHrSV86qqvVc1v6Za38vm5f1chc/x8L3Cg23SMjZETq0dKtc2jZLuiWEVfCrcRbAKABaxYcMGadCggVkKTLuqjx07Vpo0aVLu8/Xx0aNH1+gYAfi/zRn58s7mTPl+d7YsOZgndn9s5VljY/bHnVMV1tjOLUcKZNaeHBn/1xG5vlmkvHl6vESFMOvyeARMN+CFCxea9VenT58uMTEx3h4OAPgUXbda16lu3bq17NmzxwShulzYqlWrJDY21u3Mqq6JTTdgAGVZciBX/r3ssPyw5++/G6pBZJCcEhcqrWuFSMOoYKkTFiR1woMkPixIIkNsEmqzSWiQSFhQ4c+QIFuJpirFM1y2MrJbOge/9OOlr5elvC/AFX0xLu9rc4WvKfe95IS/V4WvqcbtrP7Pr8JrPH6v8t+tKtt5JN8h69PzZc7ebPlo21FzkuaC5HD57rxECQ2q7LfRetLd7AYcMMHq1q1bpW3btjJkyBB56aWXvD0cAPBpaWlp0rRpUxk/frwMHjzYrdewdA2A8kp971+SJu9szjK39Wt53+Rwua5ZlPRJCpdG0RTywVoWpOTIxT/ul8x8h/y3S5wMb1v2SWErS7fa0jXNmjWTMWPGyIQJE+T333/39nAAwKfVrl1bWrVqJRs3bvT2UAD4sRWHcqXbjH0mUNUg9abmUbLp8iT5vnddueWkaAJVWNI59cNlQrfa5vqTK9LNXG1UTcAEq2ro0KHSuXNnuf322yUvL8/bwwEAn6UlwZs2bZLk5GRvDwWAn/o1NUfOnJkqGzLypXFUsCzsV1fePTNBmscQoAK3nBQlXRJCTXnwlE2FVQeweLCqnS7ffPNNWbNmjfz3v//19nAAwGf861//kvnz55spE7/88otcccUVEhwcLAMHDvT20AD4ocX7c6Xf3P3mi/i59cLkj4vryRl1w709LMBnBNlsck+rwj46b27MFHtgzLyscQEVrKouXbrIsGHDTPMQ7XwJABDZuXOnCUy1wdI111wjderUkV9//VXq1q3r7aEB8DO7sgrMfLz0PIecUy9Mvjs/URIjgr09LMDnXNs0UmJCbLIxI1+WHqDqsyoCpsFScVlZWdK+fXtp3ry5zJ49u0SXOABA1dBgCUC+3SHnz06Vn/blSqf4UFnQt67EagtfAGW6Yv5++WpHtjzdqZY82p5jp2UbLBUXFRUlkyZNkrlz58qUKVO8PRwAAICAMHpFuglUY0Nt8snZCQSqQCX6JkeYnz/szvb2UPxSwP6FueCCC+Smm26S4cOHS0pKireHAwAA4NdWp+XJ2NUZ5vqbPeLl5Fqh3h4S4DfB6s+puXIkz+7t4fidgA1W1QsvvCBBQUGmSzAAAACqRmeN6VqqBQ6RyxtHyLXNorw9JMAvnBQbIo2igiXfIfLnIeateiqgg1VtHPLiiy/KRx99JNOnT/f2cAAAAPzS59uPyty9OaJ9lMZ3LVw/EoB7uiYUViH8cTDX20PxOwEdrKobb7zRlATffffdZl1BAAAAeNZUacSfh831h9vGso4q4KEuCWHm5x8Hyax6KuCDVe0E/Prrr0tqaqqMGjXK28MBAADwK59sOyqbjxRIYniQPNwu1tvDAfxOl6LM6tIDZFY9FfDBqmrRooWMGTNGJkyYIIsXL/b2cAAAAPyC3eGQZ1alm+tD28RIdIglvjoCJySz+ld6vmTl02TJE5b5i6NNljp16iS333675OWRggcAAKjMtzuzZfXhfKkVapN7W8d4eziAX0qODJKEsCCxO0Q2ZOR7ezh+xTLBakhIiLz55puyevVq0yUYAAAAFXt+TeFSNfe2ipHaYZb52ghU+7TEVrUK53qvTydY9YSl/up06dJFhg0bJqNHj5aNGzd6ezgAAAA+a1VanlkbMsQmch9ZVeC4OINVMquesVSwqp588klJSkqSO++806wZBgAAgGO9uSHT/LysUaQkRwV7eziAXzs5lsxqVVguWI2OjpZJkybJ3LlzZcqUKd4eDgAAgM85mu+Qd7cUBqtDTo729nAAv0dmtWosF6yqvn37mvVXhw8fLikpKd4eDgAAgE/5bHuWpOU6pGl0sFyQHO7t4QB+j8xq1VgyWFXjx4+XoKAgM4cVAAAAf5u8Kcv8HHxStATZbN4eDhAwwer+HLuk5bJ8jbssG6zWrVtXXnzxRfnwww9lxowZ3h4OAACAT9iTVSDzUnLM9ZtbRHl7OEBAiAkNkjrhhaHX9kyyq+6ybLCqtBT4ggsukLvuukuOHDni7eEAAAB43afbs0RbUJ6eGCZNYwqzQQCOX5OiRmU7Mgu8PRS/EWT1NY9ef/11SU1NlVGjRnl7OAAAAF738baj5ud1zSK9PRQgoDSOLgxWt2cRrLrL0sGqatGihYwZM0YmTJggixcv9vZwAAAAvEbLE39JzRWdpfqPJpQAA9WpSVGwSmbVfZYPVtXQoUOlY8eOcvvtt0teXp63hwMAAOAVnxRlVc+pFyYNWFsVqFaNowrL6rcTrLqNYFVEQkJC5M0335RVq1bJCy+84O3hAAAAeMUX2wuD1WuaklUFTlhmNYsGS+4iWC3StWtXs4zN6NGjZePGjd4eDgAAQI3al10gv+7PNdcvaxTh7eEAAadxUbUCmVX3EawWo4FqUlKS3HnnneJwaB88AAAAa5i+K9t0Ae4cHyqNoukCDJyozOrOrAIpsBNruINgtZjo6GiZNGmSzJ07V6ZMmeLt4QAAANSYb3YWlgCTVQVOjOTIYNO8LN8hsj/H7u3h+AWC1VL69u1r1l8dPny47Nu3z9vDAQAAOOGyCxzyw54cc71/I5asAU6EkCCbJIYXhl97sykFdgfBahnGjx8vQUFBpkswAABAoJuXkiOZ+Q5pEBkkXRJCvT0cIKCzq2rvUTKr7iBYLUPdunXlxRdflA8//FBmzJjh7eEAAACcUN/tKiwBvrRhpNhsWqgI4ERIiizKrB4ls+oOy8+e3717txw6dOiY+zt37iw9e/aUwYMHy9dffy1RUbRw97b4+Hhp0KCBt4cBAEDAmbu3sAS4XwPmqwInUpIzs0oZsFtCrB6o9j6vj6SnHzG383JzJTcvR8JCwyU0LEzy8/Mldf9eOa/X+RJXK77S99MOwtk5R8Vut0tkeKQEBXu+mLa9oECO5hw1ZcgR4VU7u1l6Ozzlq9tRq1aMzPlxNgErAADVKOVogaw5nG8av5xb3/PvDQDclxRRmFndQxmwWywdrGpGVQPVdnXPlX3pO2XToWVyUlInOal+B9dz1kf+Iat2/CynNbtEEmLql/te+QV5snTLbLHn5Ei3k/pJXFSix+M5nLVflmyaJTGRidK1eR8JCfZ8zsimlBVlboe7fHU7Mo4elNWp882/GcEqAADVO19VdYgPlTrhnp+gBlCFzCplwG6xdLDqpIHq1n2r5NSmZ0vbhqeXeOy0Fn1lz6FNsmLbfBnQ7R4JCjr2j3heQa78tPYLyc7NlPPbXycJMUkej+Hgkb2ybOs8SYhNkrPbXCmhwZ6f2Vyz69dyt8MdgbIdAADAfT8WBavn1Q/39lCAgJcUQbDqCcs3WNJS0017l0nbxj3LDIw0OD2rzRVyKDNFVu5YWG6Al551QM455aoqB3gL/vpcakXVOa4Ab82OReVuR2UCZTsAAIBnfiyar0qwCtRgg6VsyoDdYflgVedEaqlpRYFRYmxDadf4TPlz61wTzAVagBco2wEAADyzO6tA1mfkS5BN5ByCVaAGl64hs+oOywer2rzHnbmdXZr3lsiwWFm47ivTgChQArxA2Q4AAFD1EuDO8aFSO8zyXwuBE65+URnw4TyHZBc4vD0cn2f5v0rudsvVAOzM1gNkT9pmWbv7dxPgHc7cL2e1vtxvAzwCVQAArO3HvdnmJyXAQM2oHWaTkKJFMvazfE2lLB+seqJRwsnSol4H+XXDd5J2ZJ80qdtGflr3hV8GeASqAADA1VwpiWAVqAm6nGNieGEIlprDvNXKEKx6GODl2/NMGXB0RJxEhcXKocx94nDY/SrAI1AFAADbM/Nl85ECCbaJnF2PYBWoKXWLSoFTabJUKYJVDwK8Oas+kLTMVDN/defB9ZKVk2EC1aO5mX4T4BGoAgCA4l2Au9UJk9hQvhICNYXMqvv4y+RBgHcgY4+kH91vlrGpH9dM1u5ebB7Pyk33iwCPQBUAADixvirgHXUjCkOw/TnMWa1MSKXPsLjiAd6FHW+RA0f2yOJN35ty4IKCfPOcrJx0kdiGPh3gEagCAAAnndJEsAp4R11nZpUy4EqRWfUgwKsTmyytkrvI1T2GSaukLuKQwl+wfek7fDrAC5RAVQ+sAADg+G05UiDbMwtEq3/PrOf5MR1A1TFn1X0Eq1UI8MJDI6Vnq/5yWZe7JTq8tsSE1/bZAC9QAtX8gjzJzjnq8esAAMCxnFnV7nXCJDqEr4OAd+asUgZcGcqAjyPAqxvXSK474yGfDfACJVDV7Vi6ZbbY7Zx9AgCgOpsrUQIMeHPOKt9tK8OptAAO8AJpO44cTZPI8EiPXw8AAMqar5ptrrO+KlDzmLPqPoLVAA7wAmk7up10gQQFF9b3AwCAqtuQkS+7j9olLEikZyLBKuC1OatkVitFGXAAB3iBtB02m83j9wAAAOWXAPdMDJPIEI6vgLfmrB7MsYvd4ZAgvueWi8xqUfOeQAzwrLwdAACgbK4la5IivD0UwJIStKxBS/JF5HAuq11UxPLBqs7b0OY9BHiBsx0AAKD87z3zioLVXjRXArwiLNgmMUVVDQdzKQWuiOWDVV0ORZv3WD3AC5Tt6N+/v1x44YVlPvbTTz+ZcuIVK1ZIILnlllvk8ssv9/Yw4GfGjRtn/n8YOnSot4cCoAb9dThfUrLtolPmTk9kfVXAWxKKlQKjfJYPVnU5FG3eY+UAL1C2Qw0ePFhmzZolO3fuPOaxyZMnS7du3aRDhw5Vem+4Lzc319tDQAUWL14skyZN4v8FwMIlwGfUDZfwYObJAd4uBSazWjHLB6u6HEpcVKJlA7xA2Q6nSy+9VOrWrStTpkwpcf+RI0fk008/NcHsgQMHZODAgdKwYUOJioqSU089VT788MNjTmI899xz0rJlSwkPD5cmTZrI008/bR6bN2+eyUilpaW5nr9s2TJz39atW83tJ598Ujp16lTiPV966SVp1qzZMRnRZ555RurXry+1a9eWMWPGSH5+vjz00EOSkJAgjRo1MkH28Rg/frzZxujoaGncuLHcc889Zn+ozMxMqVWrlnz22WclXvPVV1+Z52dkZJjbO3bskGuuucaMUcc1YMAA17YW3xbdRw0aNJDWrVsf15hx4ui//Q033CBvvvmmxMfHe3s4ALw1X5USYMCryKy6x/LBalWWQwmUAC9QtqO4kJAQufnmm02wqvNynDRQLSgoMEFqdna2dO3aVb777jtZtWqVDBkyRG666Sb5/fffXc8fOXKkKZMcNWqUrFmzRj744AMTUFa3uXPnyu7du2XBggUmqHziiSdMwK1BxG+//SZ33XWX3HnnnWVmit0VFBQkEyZMkNWrV8s777xjPvPhhx82j2lAet111x0TEOvtq6++WmJjYyUvL0/69etnrmsp9c8//ywxMTGm3Lp4BnXOnDmybt06k9meNm3acewVnEj33nuvXHLJJdKnT59Kn5uTkyPp6eklLgD8l73YfFWCVcC7yKy6h6VrLBrgBcp2lOW2226T559/XubPny+9evVyBV9XXXWVxMXFmcu//vUv1/Pvu+8+mTlzpnzyySfSvXt3k0383//+J6+88ooMGjTIPOekk06Ss846S6qbZik1kNSAUrORms3NysqSRx99tETQvHDhQhNUVkXxOYma2X3qqadMEPzqq6+a+26//XY544wzZM+ePZKcnCz79u2T6dOny+zZs83jH3/8sck0v/XWW64lhHR/apZVs8x9+/Z1Bb76nLAw5kD5qo8++kj++OMPUwbsjrFjx8ro0aNP+LgA1IxVaXlyIMcuUcE2Oa0Of6sBnwhWyaxWyPKZVSsGeIGyHeVp06aNCb7efvttc3vjxo0mI6glwEozrP/5z39MaawGi5ol1GB1+/bt5vG//vrLZJR69+4tJ1q7du1MoOqk2Vsdl1NwcLDUqVPHBJBVpUGnbouWPWt2VLPIWgqtQbHSAF3HoVlX9f7770vTpk3lnHPOMbeXL19u9qG+VveVXnS/aYZ606ZNrs/RcROo+i4t5X7ggQdk6tSpEhHh3nIVerLk8OHDrou+BwD/LwE+q16Y6UYKwAfKgMmsVohg1WIBXqBsR2U0MP38889NllSzgJoZPffcc81jmnXVzOmIESPkxx9/NPNNtczVWdIaGRlZ4Xs7g8viZcZaKlv6OcUfL+s5KjQ0tMRtzVyWdZ9mNqtC55VqWbE20tH9sXTpUpk4caJ5rHgJr2ZXnfN8dX/deuutriyqznHUsmndT8Uv69evl+uvv971HppZhe/Sf3s96dGlSxdTLq8XrT7QzL5e15M4pel8bZ3TXPwCwH/9uJcSYMBXUAbsHoJVCwV4gbId7tBmQBow6lzTd99915QGO4MvnXOpDYJuvPFG6dixo7Ro0cIEXk4nn3yyCVh1DmZZtIGT0rJZJw3eSj9n7969JQLW0s+pqQBFA90XXnhBTj/9dGnVqpWZI1ua7ott27aZwEXn6DrLn5UGNxs2bJB69eqZhlPFL1pSDf+g2fWVK1eWOOGg3bG12ZJe1yw+gMBVYHfI/H1FwWoSwSrgbTRYcg9zVi0S4AXKdrhLS1WvvfZaU8aoTWG0W23xYFS73/7yyy+mkZE2NkpJSZG2bduax7VEUrOu2oRIy1rPPPNMSU1NNQ2KNGOrQZp21dWOv9r9VgNdDQaL07my+hqdg6qNir7//nuZMWPGCctMaYlm6WBYy4d1rJrRffnll80atBqov/7668e8XvfDlVdeaboQ6xxU7ULspMGMZqM1wNduxfqYBrZffPGF2UfFnwvfpWXc7du3L3GfZsP196T0/QACz4q0PEnLdUhsqE26JjBlA/A2MqvuIbNqgQAvULbDXkaZYkU0sDx06JAp8dXlVJwee+wxky3U+zWoTEpKMsuuFKddgIcPHy6PP/64nHLKKSbwdc4b1TJdXepm7dq1prz22WefNU2LitPXaAMjLbnV7K12Gi7e1Km6aaOjzp07l7hoYxz9bA3GdYwakOh8RW2aU97+0tJgzUIXp8v7aLdiXb5HA1rdNn2uzlmlLBQA/KsE+Oy64RISxHxVwNvIrLrH5ig9sc5CNFPWt89FcnqTKyQ+un5ABniBsh1bU1fLku0z5OdffjLNgFD93nvvPRk2bJgpE6ZREsqiVQpa+q2ZfE5UAP6l/4/7ZdqubHm+S5z8q22st4cDWN7KQ3nS4bsUqRcRJClX/51UsYp0N79TUAYcwAFeIG3Hkk2zJKjoDBSql3YF1vm3ukSOrulKoAoAgSXf7pAFzvmqNFcCfC6zqrlDZ28VlMS3/wAO8AJpO2Iia0tEeMVdelE1Oq9Wl/vRcmid4wsACCx/HsyT9DyH1A6zSaf4kh3nAXh3zmq+Q+SI/gdlIlgN4AAvkLaja/M+nHE6QbRRlDZh0u7H2pgKABCY66ueUy9cgpmvCviEyBCbRBQ14mfeavkIVgM4wAuk7QgJ5kwwAABV8WNKtvlJCTDgW+gIXDnmrOqyH1n7ZdnWeQEX4Fl5OwAAgEie3SE/7cs111lfFfC9eau7j9rJrFbA8plVXQ5Fm/cQ4AXOdgAAgEJLDuRKZr5D6oQHyam1qVICfAmZ1cpZPlg9mnPUNO+xeoAXKNsBAAD+NqdofdVe9cMliN4PgE9hrdXKWT5YDQoKMs17rBzgBcp2AACAkmbvKZyv2ocSYMDnkFmtnOWDVV0OpSrNewIlwAuU7QAAACVl5tvll/2F81X7JEd4ezgAygtWyayWy/LBalWWQwmUAC9QtgMAABxLGyvl2UWaRgfLSTFFa2QA8L0yYDKr5bJ8sGrVAC9QtgMAAJRtVrESYNYqB3wPZcCVI1i1YIAXKNsBAADKN3tPYXMlSoAB30SDpcoRrFoswAuU7QAAAOVLOVogK9LyzPXzaa4E+CQyq5UjWLVQgBco2wEAACo2t2jJmo7xoVIvgvmqgC9nVg+QWS0XwapFArxA2Q4AAFC52XsL56teQFYV8FnxRZnVQ2RWy0WwaoEAL1C2Iy+3sP0+AAAon8PhkFnMVwX8JljNLhA5mu/w9nB8EsFqgAd4gbIdm1JWSG5e4YEXAACUb0NGvuzIKhD9HnxWPaqYAF9VK9QmwUWNusmulo1gNYADvEDajk17l0lYKKVMAABUZvquwhLgM+uGS3QIX/UAX6VLStWmFLhCIWJxBQUFsufQZhOQqR0H1sv2/WulSWIbiQ6rJdtS13j0fvn2fFmzc5Fk5WRIu8Y9JePoQXPxREb2IVm9Y5FEhcdK08S2svvgRvFUoG1H/dpN5ah49vkAAFjRd0XB6qUNKQEG/KEUWBssEayWzdLBak5OjqRlHZbFR380tx3i0P+IhItsylhjLlWZJ6JsoSJL986vwqgcYt4iRCTbnisLt0+ryjsE3HbsPLpFgh2F/2YAAKBs6bl2mb+v8Fh5aSOCVcDXJYQV1gGz1mrZLB2shoeHS6068ZI48CKJqF/H28NBBbJTDsj+D2eYfzMAAFC2WXuzJc8ucnJsiLSqFert4QCoBB2BK2bpYFUFBwVJbKNkiW7SwNtDQQUyQ8PkUBDzbgAAqMi0nZQAA/6EYLVifPsHAAAIAHaHQ6bvLgpWKQEG/EJ8eGE4dpBgtUwEqwAAAAHgt/25si/bLrGhNjmrLtNmAH+QQGa1QgSrAAAAAeDTbUfNz/4NIyTMuXgjAP8oA84pbG6KkghWAQAAAqAE+NPthcHqNU2jvD0cAB4Gq5QBl41gFQAAIABKgHdmFZgS4H4NmK8K+IuEojmrlAGXjWAVAAAggEqAIygBBvwG3YArRrAKAADgxygBBvxXfFjhyaWDOQSrZSFYBQAA8GMLUnIoAQYCoBuww0GTpdIIVgEAAPzY/23KMj+vaxpFCTDgp+us5jtEMvU/KIFgFQAAwE+l5drls+2FwerglpQAA/4mKtgmoUURGR2Bj0WwCgAA4Kc+3Jol2QUi7eJCpHudMG8PB4CHbDZbiVJglESwCgAA4Kf+b2Om+Tm4ZbT50gvAjzsC02TpGASrAODjcnJyvD0EAD5oUWqOLD2YZ0oIb2xOCTDg78HqwVzmrJZGsAoAPmbcuHGSnJws06ZNM7fPO+88bw8JgA96fk2G+XlT8yipGxHs7eEAqKKEoiZLlAEfK6SM+wAAXvTOO+/IkiVL5N5775W4uDhvDweAD1p3OE++2pFtrv+rbay3hwOgOsqACVaPQbAKAD6mYcOG5jJ16lS54oor5MCBA94eEgAf88JfR0QLBi9rFCGnxIV6ezgAqqMMmDmrx6AMGAB8TO3atSU/P1+io6Pl5ZdflpSUFG8PCYAP2ZyRL+9sLmys9DBZVcDvJYQVNkcjs3osMqsA4GM+++wz1/XWrVtLWlqaV8cDwLc8/Odh0e+0FySHy5n1wr09HADHKZ45q+UiswoAPmr+/PneHgIAH7MgJUc+335Ugmwi47vW9vZwAFRrN2CC1dIIVgHAR02aNMnbQwDgQ/LsDhm6pLDS4o6W0dK+NnNVgUCQwDqr5SJYBQAf5XCw3hqAvz2xPF3+PJQn8WE2GdOxlreHA6CakFktH8EqAPgom62w4QIAzNubLeNWF66r+ubp8VKPdVWBgJFYNGf1AJnVYxCsAn4sKytLrrzySmnQoIE88cQT3h4OAOAE2JiRL9ctPGiWqhl8UpRc1STK20MCUI3qFAWrh/McptwffyNYBfzYO++8I2FhYfLtt9/KJ598In/99Ze3h4RqRBkwgF1ZBXLB7FRJybZLx/hQeakbTZWAQCwDdtZSsdZqSQSrgB+Li4uTevXqycknn2zW5tTbCBxvvvmmt4cAwItWHsqTs3/YJ1szC6RlbIjMPD9RYkL56gYEmuAgmyQUZVf3E6yWwF88BKxFixZJcHCwXHLJJdX+3jk5OdKpUyczp3DZsmUlHluxYoWcffbZEhERIY0bN5bnnnuu0vfbvn27GWdUVJQJPh966CHJz8+v9HXXXXed/PjjjxIfHy/dunUz5cAIHDExMcfcV1BQIF999ZUMGDDAK2MCcOLZHQ6ZsilTes7cJ1uOFEjzmGCZ1TtR6kcyTxUI9HmrBKslEawiYP3f//2f3HfffbJgwQLZvXt3tb73ww8/XGZgmJ6eLn379pWmTZvK0qVL5fnnn5cnn3xS3njjjXLfS4MPDVRzc3Pll19+MaW9U6ZMkccff7zScRw4cEDWr19vxqOvReDSkyDDhg0zv3cDBw70uPnSa6+9Jh06dJBatWqZS8+ePWXGjBknbLwAqlb6P3dvtpw1M1VuXXRIMvMd0jspXBZfVE+axYR4e3gAaiRYLfD2UHwKwSoC0pEjR+Tjjz+Wu+++2wSCGvxVF/2C/8MPP8h///vfYx6bOnWqCTrffvttadeuncl83n///TJ+/Phy30/fa82aNfL++++bbO1FF10k//nPf2TixInmvSqin9elSxd55JFHZPXq1Saggf/KzMw0Jy+cDh48KC+//LJ07drVXCZMmCD//ve/JTU11WRXPdGoUSMZN26cOYmyZMkSOf/88012Vn9vAHhPboFDfk3NkSeXH5a236ZI79n7ZdH+XIkOscnYTrXk+/MTpU44GVXAMsFqNpnV4jhNh4CkzYbatGkjrVu3lhtvvFGGDh0qI0eOdGWjtm7dKs2bNzcltL169XL7fVNSUuSOO+4wgYKW7JZVenzOOeeYpkdO/fr1k2effVYOHTpkynXLes2pp54q9evXL/EaDbQ1kOjcuXO545k8ebIMGTLEzFW99NJLze0XX3zR7e2Bb81P1Y7OiYmJcuutt8rPP/8s06ZNM78bN998sznxoQFnnz59yiwPrkz//v1L3H766adNtvXXX381J1aA8jJ9R/IdcrTAIfl2kXyHQwocIvl2h+Q7xFzXklW338+jz/bguaVe4yh1n+t60bWS95X1vJLvVfi4o+LHy73PIVkFDsnIK9yX6Xl22Z1VIDuzCkyJ71/peZJX7LtpTIhNbmoRJf9uX0saRhGkAlZBGXDZCFYRsCXAGqSqCy+8UA4fPizz5893BaahoaEmkC0r4CyPfuG45ZZb5K677jLzQzXgLW3v3r0mCC7OGYTqY2UFq3p/8UC19GvK88cff5juv9dee625rdurgavOkdXtg3/RbPqXX34pLVq0kOTkZFPCvnz5cvN7Wt00e/vpp5+aTK6WA1c0N1svxcvcEXjScu3y874c+eNgnmw8ki+bMvJNB9q0PLuk5zmEVRROvISwIFPue1HDCLmqcaTUCqPwDbCaxKK1kwlWSyJYRcBZt26d/P777+aLvwoJCTEBnQawzmC1YcOGsnbtWo/eV8sxMzIyTIbWF2gWVTOwmolTF198sQlCvvvuO7n88su9PTx4qFWrViaA1N9N7eysmdZ9+/bJTTfdZP6dPZ2jWpaVK1ea4DQ7O9tkZ/X/kbZt25b7/LFjx8ro0aOP+3PhezLz7fLR1qPy3uZMWbAv162MZ4hNJNgmEhJkK7puM7f14glPfpU9eWvn/yPO19iKfZa5XurzS95nK/N5Fb/GeZ+t7NcUuy8q2CYxoTaJDQkyPxtEBkujqMLLqbVDpUl0cLX8Pw7Af5FZLRvBKgKOBqXaSbd4AyTNioaHh8srr7xS5eVd5s6da0p29X2K0yzrDTfcYBojJSUlmVLh4py39bGy6P0aXHvyGs12ffDBB6a0WINxJw1WNYglWPU/+u+pc5v133bXrl3mZIrOtdYS4KCgIFcG/Xi+0GqWVrtXa6XBZ599JoMGDTIVB+UFrHpi5sEHHyyRWdUO1/Bfutj8WxszZcyKdNlbbF5Uq9gQ6Vk3TFrVCjFLpDSOCjbr/sWFBklcmE0ig20EUwBwAtFgqWwEqwgoGqS+++678sILL5iuvMVpAPfhhx+aMt6q0OY2Tz31lOu2dhjWjJc2curRo4e5T7NW2gAnLy/PVYo7a9YsEySUVQLsfI3OH9Qsmi5b43yNdmwtL4j45ptvTPOlP//80yzP46QBjnaKLf5e8A/676UNkJw6duxo5h9rR2n999bAVYMFbYp09dVXm4s21/KEzqVu2bKlua4NmxYvXiz/+9//ZNKkSWU+X0/MlD45A/+lpb3X/HRAfkktbNymy6EMaRkt1zePkibRfB0AAG+iwVLZmBSBgKINaTTbOHjwYGnfvn2Jy1VXXWWyrkozV9qAqXRGsyJNmjQp8X5atqlOOukk0/hGXX/99SYg0M/X5kgayGowUDw7paWX+tlOGlRrUKrlnjpHcebMmfLYY4/JvffeW26goNlT7XKsAU3pbdT5rtpZGIFBM+dXXnmlCVh37NhhGnx9/fXXctpppx33e9vt9hJzUhG4FqXmSOfvUkygGhdqk5dPqy1r+yfJI+1rEagCgA8FqwdyCVaLI1hFQNFgVLulllXqq4GcLtmhy7to5lPntmZlZVXr5+vn6lI0W7ZsMZmr4cOHm/VStfGRk5Zg6mc7aWZUg2z9qVlWbZSkpZ9jxowp8zM0o6ufodtTmmbeNLDRYBaBR09EPPTQQ+ZEiJake0JLenXNYW0MpnNX9fa8efNMCTsC27KDuXLR3P2SmmOXTvGhsvTi+vLP1jES5ulkUwDACVOHOatlsjl0Mp9F6Re+PpdeLE2G3yLRTf6e3wjfk7l9t2x/YYrMnjadZTaAKtBs/5w5c2TPnj3mpEqHDh1kxIgRcsEFF7j9HjpnVV+rJ1y0TB2+Tzv7njFzn+zLtstZdcNkZu9EiQrhPDUA+JpDOXZJ+HS3uZ49sKGEB/gJxXQ3v1NQ+wMAFuAsgYe1mikNXHjABKqd40Nl2nkEqgDgq7SZncanun71gRy7NGCdZYOjFgAAAeiZVRmy+ECe1A6zyTe96kgca3cCgM8KstmKlQLTEdiJIxcAAAHmjwO58p+V6eb6q6fFSyOaKAGAz2Ot1WMRrAKAj1mzZo3pav3AAw/I7bffLps3b/b2kOBnHvrzsCkl+0eTSBnYPMrbwwEAuIFg9VicagUAH6PrBEdGRpp1fBMTE01XaV3yCHDH7D3ZMndvjmjV7/Ndju2MDgDwTYnhhfNUWWv1bwSrAOCDdBmi/v37m+t16tTx9nDgJ7TB/6PLDpvrd50cI01jOMwDgL8gs3osjmIA4GNiY2NNS/dXXnnFZFYPHjzo7SHBT3y3K9s0VYoOscmj7WO9PRwAgAcSIwhWS2POKgD4mLFjx0qjRo1kxowZsmjRIpk4caK3hwQ/8cq6I+bn3SdHS/1Ilj0AAP/MrNIN2InMKgD4GJ2vOmbMGG8PA35mY0a+zNyTI7qM/N2tYrw9HABAFYNVXWcVhcisAoCPeu2117w9BPiRSesLs6oXNoiQFrGciwYAfw1W99FgyYVgFQB81E8//eTtIcBPHM13yNubssz1e1pFe3s4AIAqqB9ROH0jJZsyYCeCVQDw4c6ugDu+3nlUDubapUl0sFzUIMLbwwEAVEFyUa+BlGy7FNj5DqAIVgHAh5evAdzxybbCrOqNzaMkOIjfGwDwR/UigkT/hGucmsq8VYNgFQAAP3Ykzy4zdmeb6/9oEunt4QAAqkhPNtYtmre65yilwIpgFQB8lC5fA1Rm2q5s0elNLWNDpGN8qLeHAwCohlJggtVCBKsA4KOee+45bw8BfuDTbUddWVVKxwEgUIJVyoAVve0BwEfl5+fL6tWrZe/eveZ2UlKStG3bVkJDyZ7h7xLg6buLgtWmlAADgL9LjqQMuDiCVQDwMXa7XR5//HGZOHGiHD58uMRjcXFx8s9//lNGjx4tQUEUx1jdD3tyTAlwi5hg6UQJMAD4vaSizOpeglWDYBUAfMwjjzwiU6ZMkXHjxkm/fv2kfv365v6UlBT54YcfZNSoUZKbmyvPPvust4cKL/thT2FjpUsaUgIMAIGAMuCSCFYBwMe8++678t5775lAtbhmzZrJkCFDpGnTpnLzzTcTrFqcrsM7s6gLcL/kcG8PBwBQjWXAu8msGtSQAYCPycjIkAYNGpT7eHJysmRmZtbomOB7Nmbky9bMAgkNEjm3PsEqAASCxlGFucQdWQSrimAVAHxMr1695F//+pfs37//mMf0vhEjRpjnwNp0vqo6q264xGjECgDwe02iC8uAd2cVSJ7dIVZHGTAA+JjXX39dLr74YpNBPfXUU0vMWV25cqXpCDxt2jRvDxM+Ml+1LyXAABAw6kUESXiQSI5dZFdWgTSLsXa4Zu2tBwAf1LhxY1m+fLnMnDlTfv31V9fSNd27d5dnnnlG+vbtSydgi8stcMjcvYWZ1b7JEd4eDgCgmgTZbNI4OsRM9diWSbBq7a0HAB+lwehFF11kLkBpvx/IlSP5DqkbHiSdEliyBgACrRRYg9XtmfkiYu3qGU7NA4Cf0eZKCxYs8PYw4EUL9xVmVc+pH27OwgMAAkfTonmr2zNpsmT5zGpBfr4cWrlesvbs8/ZQUIGc/Wnm3wqAyMaNG+W8886TggIOYla1MDXX/Dyrbpi3hwIAqGZNoghWnSwdrObk5Eh2xgHZ9+ln3h4K3GC3B5t/MwCwMrvDIT+nFnUCrmft8jAACERNi+apbjNlwNZm6WA1PDxc6tSpJf8cmSgNG9Ogwpft2pEtr4zdb/7NgECXkJBQ4eNkVK1tzeF8Sct1SHSITTrFM18VAAJNi6JgdUMGwaqlg1UVHBQszVrESsvW0d4eCioQGpopwUGHvD0MoEZoBcHdd99tlq0py7Zt22T06NE1Pi741nzV0xPDJCSI+aoAEGha1yoM0bZmFkhOgUPCg637t97ywSoA+JpOnTqZ5WsGDRpU5uO6rA3BqnUt3Fc4X/VsSoABICDVjwiSWqE2Sc9zmK7A7Wpbt4qGbsAA4GMuueQSSUtLq7BM+Oabb67RMcF3LHTOV6W5EgAEJJvN5squrku3dikwmVUA8DGPPvpohY9r1nXy5Mk1Nh74jp2ZhYvEa0VYj0SCVQAIVK1rhcriA3myLj1PRCLFqsisAgDgJ34/oF9aRE6tHSoxoRzCASBQkVktxJEOAAA/seRA4XzV0+qQVQWAQHZKXGGwujKt8CSlVRGsAgDgJxa7glXrNtsAACvomlB4UlKD1ewCh1gVwSoAAH7A4XDIkoOFwWo3MqsAENCaRgdLnfAgybOLrDxk3ewqwSoA+JjNmzd7ewjwQbp8QVquQyKCRdpbeBkDALBKR+DTiqponFU1VkSwCgA+pkOHDtK+fXvTFfi3337z9nDgI5YUNVfqFB8moUHWXSAeAKyiW1EpMMEqAMBn7N+/X8aOHSv79u2TAQMGSHJystxxxx3y7bffSnZ2treHB6/PV6UEGACs4Iyi9bTn7M0xU0GsiGAVAHxMRESE9O/fX9566y3Zs2ePfP7551KnTh0ZMWKEJCYmyuWXXy5vv/22pKamenuo8EKw2o3mSgBgCefWDzdTP3ZkFciaw9ZcwoZgFQB8fM7KGWecIePGjZM1a9bIn3/+KWeffbZMmTJFGjVqJBMnTvT2EFED8u0O+eNgYRkwmVUAsIaokCDpVT/cXJ+x25qVVQSrAOBHTj75ZBk+fLgsWLBAdu/eLX379vX2kFAD1qfnS1aBQ6JDbK6F4gEAge+ShpHm54dbsyxZCkywCgB+SkuDNXhF4FtWtGxBx/hQCbLRXAkArGJgs0hTCqzVNb/tt16jJYJVAAB83PJDhV9QOsUzXxUArKROeLBc1zTKXB+zMsNy2VWCVQAA/CizCgCwlhHtYiUsqHDe6tubssRKCFYBAPBxyw/9vcYqAMBa2sSFyqhTa5nrQ347JKNXpMvBHLtYAV0aAMBHaanP0qVLZevWraYrcPPmzaVz587mOqxj79ECScm2S5BNpH1tDtsAYEWPto+VHZkF8sbGTHlyRbqMWZkujaOCpWl0sESF2CTUZpOwYJuE2ESq61tCs5gQGds5rprerWo46gGAD/rxxx9l8ODBsm3bNtf8FGfAqmusnnPOOd4eImq4BLhVbIhZxgAAYD1BNpu83qO2nJ8ULs+sypAVaXmyLbPAXE4U7ZNAsAoAKGHjxo1y6aWXSo8ePeTFF1+UNm3amIBV11mdMGGCXHzxxbJixQpp0aKF2+85duxY+eKLL2Tt2rUSGRlp1m599tlnpXXr1id0W3D8aK4EAHCetL62WZS5aNXNxox82ZlVIDkFDsmzi+Q59Gf1NWBKDA8WbyNYBQAf89JLL8npp58uc+bMKXG/Bq1XXHGF9OnTxwSxL7/8stvvOX/+fLn33nvltNNOk/z8fHn00UfNGq0aAEdHR5+ArUB1WXaQ5koAgJKSIoPNJdARrAKAj5k3b57JhJZ3VnXo0KEycuRIj97z+++/L3F7ypQpUq9ePTMnlpJi37Y8rai5UgLNlQAA1kKwCgA+Zvv27XLqqaeW+3j79u3NXNbjcfjwYfMzISGh3Ofk5OSYi1N6evpxfSY8dzTfIevS8831jrXJrAIArIVODQDgY44cOSJRUYULgJdFH8vKqvo6a3a73WRnzzzzTBP4lkezu3Fxca5L48aNq/yZqJq16Xmi04/qhAdJUiSHbACAtZBZBQAfpHNJ9+7dW+Zj+/fvP6731rmrq1atkoULF1b4PC01fvDBB0tkVglYa9aaw4VZ1bZxISxZBACwHIJVAPBBvXv3di1ZU5wGLHp/VQOXf/7znzJt2jRZsGCBNGrUqMLnhoeHmwu8Z3XRfNV2cZQAAwCsh2AVAHzMli1bqv09NcC977775MsvvzQNnHS9Vvi+1YeLglXmqwIALIhgFQB8TNOmTSt8PC0tTaZPn17p80qX/n7wwQfy9ddfS2xsrKvEWOei6rqr8P0yYAAArIZuDQDgZ7QT8E033eTRa1577TXTAbhXr16SnJzsunz88ccnbJw4/k7AmzIKg1XKgAEAVsSpWgCwgLLmv8L3OwHrv5p2Aq4XwbllAID1cPQDAMAH0QkYAGB1BKsAAPggOgEDAKyOMmAA8DETJkyo8PFdu3bV2Fjg/U7AbQlWAQAWRbAKAD7mxRdfrPQ5TZo0qZGxwPtlwO1qc6gGAFgTR0AAsMA6q/AvdAIGAIA5qwAA+Jx1RZ2AE8LoBAwAsC6OgADgYxYtWiTTpk0rcd+7774rzZs3l3r16smQIUMkJyfHa+PDibe6WAkwnYABAFZFsAoAPmbMmDGyevVq1+2VK1fK4MGDpU+fPvLII4/It99+K2PHjvXqGHFi0QkYAACCVQDwOcuWLZPevXu7bn/00UfSo0cPefPNN+XBBx803YI/+eQTr44RJ9YaOgEDAECwCvi7r7/+2pSHnnbaabJ+/XpvDwfV4NChQ1K/fn3X7fnz58tFF13kuq3/1jt27PDS6FDTZcAAAFgVwSrg5+677z6Tcbvmmmtk1KhR3h4OqoEGqs6OwLm5ufLHH3/I6aef7no8IyNDQkPJuFmhEzCZVQCAlRGsAn6uTp060rJlS2natKkkJCR4ezioBhdffLGZm/rTTz/JyJEjJSoqSs4++2zX4ytWrJCTTjrJq2NEzXQCrk8nYACAhXEURMC55ZZbTPdM50WDuQsvvNB8wa8Ou3btkhtvvNG8b2RkpJx66qmyZMkS1+MOh0Mef/xxSU5ONo9rU5wNGzZU+r4TJ06UZs2aSUREhJmf+Pvvv7s1nkcffdQELgMHDpTRo0cf17bBN/znP/+RkJAQOffcc03WXC9hYWGux99++23p27evV8eIE4dOwAAAFCJYRUDS4HTPnj3mMmfOHPPF/9JLL62WuYRnnnmmKcGcMWOGrFmzRl544QWJj493Pee5554zDXBef/11+e233yQ6Olr69esn2dnZ5b7vxx9/bBrnPPHEE6bks2PHjuY1+/btq3RMv/zyiwlUGzZsaD4P/i8xMVEWLFhgft/0csUVV5R4/NNPPzW/KwjsTsCUAAMArI5gFQEpPDxckpKSzKVTp06mpFIb0qSmph7X+z777LPSuHFjmTx5snTv3t00NtIMl7MkU7OqL730kjz22GMyYMAA6dChg1kfc/fu3fLVV1+V+77jx4+XO+64Q2699VZp27atCXS19FMzaBXJy8uTqVOnyk033STXX3+9GRcCR1xcnAQHBx9zv5Z7F8+0IjA7AbNsDQDA6ghWEfCOHDki77//vpnXqaW7Tr169TIlw5745ptvpFu3bvKPf/xD6tWrJ507dzYlmk7aFGfv3r2m9Ld4wKFlvYsWLSrzPbWBztKlS0u8JigoyNwu7zVO06ZNM8GMPldLk/X2/v37PdomAL6FTsAAABQiWEVA0qAtJibGXGJjY02QqaW2GgQ6NWnSxMwr9cTmzZvltddek5NPPllmzpwpd999t9x///3yzjvvmMc1UFXFlx1x3nY+VpoGlwUFBR69xkkzqdddd50JWNu3b2+yspppBeC/nYA3H6ETMAAAitO2CEjnnXeeCSqVzvl79dVXzTqV2rRIu+YqLc/1lN1uN5nVZ555xtzWzOqqVatM2e6gQYOkJqWkpJh5s7/++qvrPs2uagD7wAMP1OhYAFRfJ2C7g07AAAAojoQISNrUSMt+9XLaaafJW2+9JZmZmSVKdqtCM7GavSzulFNOke3bt5vrOkfWGUgWp7edj5XVTEczo568Rr333nuSn59vSoy1gZReRowYIcuXL5c///yzytsIwPslwG3j6AQMAADBKixBv/RpCfDRo0eP6320E/C6detK3Ld+/XpXtlYbLmmAqR2IndLT002X3p49e5b5ntoop2vXriVeoxlcvV3ea5RmUIcPHy7L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fv36yb59+8Tq5s+fbwKrX3/9VWbNmiV5eXnmdyczM9P1nGHDhsm3334rn376qXn+7t275corrxSrW7x4sfn/sEOHDiXuZ3+VdOjQITnzzDMlNDRUZsyYIWvWrJEXXnhB4uPjXc/Rk2kTJkwwf8t+++03iY6ONv+PauBvNc8++6y89tpr8sorr8hff/1lbuv+efnll13Psfr+0r9P+ndcT0KWxZ39c8MNN5hjqP7dmzZtmgmAhwwZIpai66wCAHCiPffcc47mzZu7br/66quO+Ph4R05Ojuu+ESNGOFq3bu2wou7duzvuvfde1+2CggJHgwYNHGPHjvXquHzRvn37NH3jmD9/vrmdlpbmCA0NdXz66aeu5/z111/mOYsWLXJYVUZGhuPkk092zJo1y3Huuec6HnjgAXM/++tY+rfnrLPOKvdxu93uSEpKcjz//POu+3Q/hoeHOz788EOH1VxyySWO2267rcR9V155peOGG24w19lfJen/W19++aXrtjv7Z82aNeZ1ixcvdj1nxowZDpvN5ti1a5fDKsisAgBqxOHDhyUhIcF1e9GiRXLOOedIWFiY6z49q6zlnZrlsBItj9bsspaBOQUFBZnbup9w7O+Scv4+6b7TbGvx/demTRtp0qSJpfefZqMvueSSEvtFsb+O9c0330i3bt3kH//4hyk179y5s7z55puux7ds2SJ79+4tsc/i4uJMub4V99kZZ5whc+bMkfXr15vby5cvl4ULF8pFF11kbrO/KubO/tGftWvXNr+XTvp8PTZoJtYqQrw9AABA4Nu4caMpD/vvf//ruk8P1Drfqbj69eu7Hitefhfo9u/fb+aAObffSW9rSTX+puXlOvdSSzbbt2/v+n3Rkx76xa70/tPHrOijjz4y5eRaBlwa++tYmzdvNmWtWoqv0xV0v91///1mPw0aNMi1X8r6f9SK++yRRx4xUzn0JEdwcLD5+/X000+bslXF/qqYO/tHf9arV6/E4yEhIeYknZX2IZlVAIBHX1C0UURFl9LBlTYtufDCC03G4o477vDa2BE42cJVq1aZYAxl27FjhzzwwAMydepU06wL7p0E6dKlizzzzDMmq6rzAvXvlc4nxLE++eQT8/v1wQcfmJMi77zzjjkZqT+B6kRmFQDgtuHDh8stt9xS4XNatGjhuq5NW8477zxTMla6cVJSUtIx3Ued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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def show_sample(model, title='Sample'):\n", + " \"\"\"Draw the layer stack (left) and the SLD profile (right).\"\"\"\n", + " layers = [layer for assembly in model.sample for layer in assembly.layers]\n", + " fig, (ax_stack, ax_sld) = plt.subplots(1, 2, figsize=(9.5, 4.2), width_ratios=[1.0, 1.6])\n", + "\n", + " slds = [layer.material.sld.value for layer in layers]\n", + " norm = plt.Normalize(min(slds) - 0.5, max(slds) + 0.5)\n", + " cmap = plt.get_cmap('viridis')\n", + " semi_infinite = 30.0 # display height of the semi-infinite media\n", + "\n", + " depth = 0.0\n", + " for index, layer in enumerate(layers):\n", + " is_medium = index in (0, len(layers) - 1)\n", + " thickness = layer.thickness.value\n", + " height = semi_infinite if is_medium or thickness <= 0 else thickness\n", + " ax_stack.add_patch(\n", + " plt.Rectangle(\n", + " (0.0, -depth - height),\n", + " 1.0,\n", + " height,\n", + " facecolor=cmap(norm(layer.material.sld.value)),\n", + " edgecolor='black',\n", + " hatch='//' if is_medium else None,\n", + " alpha=0.85,\n", + " )\n", + " )\n", + " label = layer.name if is_medium else f'{layer.name}: {thickness:.1f} Å'\n", + " ax_stack.text(1.08, -depth - height / 2, label, va='center', fontsize=10)\n", + " depth += height\n", + " ax_stack.annotate('beam', xy=(0.18, -10), xytext=(-0.35, 25), fontsize=9, arrowprops={'arrowstyle': '->'})\n", + " ax_stack.set_xlim(-0.5, 2.6)\n", + " ax_stack.set_ylim(-depth - 5, 30)\n", + " ax_stack.axis('off')\n", + " ax_stack.set_title(title)\n", + "\n", + " z, sld = model.interface().sld_profile(model.unique_name)\n", + " ax_sld.plot(z, sld, color='#00a3e3')\n", + " ax_sld.set_xlabel('z / Å')\n", + " ax_sld.set_ylabel('SLD / 10⁻⁶ Å⁻²')\n", + " ax_sld.set_title('SLD profile')\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "\n", + "show_sample(model, 'vacuum / A / B / Si')" + ] + }, + { + "cell_type": "markdown", + "id": "eae7fddc", + "metadata": {}, + "source": [ + "## 1. Equality constraints\n", + "\n", + "An equality constraint ties a parameter to an expression of other parameters.\n", + "The constrained parameter becomes *dependent*: it is removed from the free fit parameters, follows the expression whenever any input changes, and cannot be set directly.\n", + "Here the roughness of film B is tied to 1.5× the roughness of film A:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "daace094", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "r_A = 3 Å -> r_B = 4.5 Å (r_B independent: False)\n", + "r_A = 4 Å -> r_B = 6 Å (follows automatically)\n" + ] + } + ], + "source": [ + "r_a = film_a.layers[0].roughness\n", + "r_b = film_b.layers[0].roughness\n", + "\n", + "constrain(r_b, '1.5 * r', r=r_a)\n", + "print(f'r_A = {r_a.value:g} Å -> r_B = {r_b.value:g} Å (r_B independent: {r_b.independent})')\n", + "\n", + "r_a.value = 4.0\n", + "print(f'r_A = {r_a.value:g} Å -> r_B = {r_b.value:g} Å (follows automatically)')" + ] + }, + { + "cell_type": "markdown", + "id": "fcea4397", + "metadata": {}, + "source": [ + "`unconstrain` releases the parameter again; it keeps its last value and becomes fittable:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0d1a2f4e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "after unconstrain: r_B = 6 Å, independent = True\n" + ] + } + ], + "source": [ + "unconstrain(r_b)\n", + "print(f'after unconstrain: r_B = {r_b.value:g} Å, independent = {r_b.independent}')" + ] + }, + { + "cell_type": "markdown", + "id": "f357034b", + "metadata": {}, + "source": [ + "## 2. Derived read-only parameters\n", + "\n", + "Every model owns `total_thickness`: a read-only parameter equal to the summed thickness of the layers between the superphase and the substrate.\n", + "It updates whenever a layer thickness — or the layer structure itself — changes, never enters a fit, and can be used inside other constraint expressions." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a3489f4a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total film thickness = 100 Å\n", + "after t_A -> 45 Å: 105 Å\n", + "setting it fails as expected: This is a dependent parameter, its value cannot be set directly.\n" + ] + } + ], + "source": [ + "total = model.total_thickness\n", + "print(f'total film thickness = {total.value:g} Å')\n", + "\n", + "t_a.value = 45.0\n", + "print(f'after t_A -> 45 Å: {total.value:g} Å')\n", + "\n", + "try:\n", + " total.value = 1.0\n", + "except AttributeError as error:\n", + " print(f'setting it fails as expected: {error}')\n", + "\n", + "t_a.value = 40.0" + ] + }, + { + "cell_type": "markdown", + "id": "45367883", + "metadata": {}, + "source": [ + "`derived_parameter` builds your own live calculations from any parameters — the counterpart of a bumps `Calculation` slot:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6d05728e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "derived SLD contrast (B - A) = 2 1/Å^2\n" + ] + } + ], + "source": [ + "contrast = derived_parameter(\n", + " 'sld_contrast',\n", + " 'b - a',\n", + " a=film_a.layers[0].material.sld,\n", + " b=film_b.layers[0].material.sld,\n", + ")\n", + "print(f'derived SLD contrast (B - A) = {contrast.value:g} {contrast.unit}')" + ] + }, + { + "cell_type": "markdown", + "id": "a4b77f0a", + "metadata": {}, + "source": [ + "### Keeping a total fixed while fitting the split\n", + "\n", + "`constrain_to_sum` is the classic use of a derived quantity: keep $t_A + t_B$ fixed while the individual thicknesses vary.\n", + "The last parameter becomes dependent and absorbs whatever the others change by — watch the stack keep its total height as $t_A$ moves:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bff23c00", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "t_A = 30, t_B = 70, sum = 100\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+oAAAGZCAYAAAAet8uUAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAewtJREFUeJzt3QecE9X2wPGT7X3ZZekdqUpTFAQVCygiKj7sImLl2euzYMcG6h/0qahY0WfvBUWlCFgABQUEBKQjveyyvWb+n3OXrNklW9ndJDO/r59INpkkdybJZM7cc891WZZlCQAAAAAACAgh/m4AAAAAAAD4B4E6AAAAAAABhEAdAAAAAIAAQqAOAAAAAEAAIVAHAAAAACCAEKgDAAAAABBACNQBAAAAAAggBOoAAAAAAAQQAvUAkZeX5+8mAAD2Y58MAAD8iUDdzyzLkiuvvFJiY2Olbdu28ttvv/m7SQDgWLm5uXLGGWdIdHS09OjRQ9avX+/vJgEAAAciUPezL774QqZOnSpvvfWWnHDCCTJ69Gh/NwkAHGvy5Mny119/yYcffiitWrWS22+/3d9NAgAADhTm7wY43Z9//ikjR46UCy64QE4//XRp1KiRv5sEAI7eJ99www1y9tlnS+fOneXcc8/1d5MAAIAD0aPuZ/3795fvv/9edu/eLR999JEMGjRInGDKlCnicrlk4cKFEgg2bNhg2vN///d/jnptX7Qt119/vdid2+2Wbt26yaOPPurvpiCAHHPMMTJt2jTZt2+ffPLJJz73yXv27DHDlb7++mu/tBGA8/z666/mmFH3Pfo7vXjxYnnwwQfNdW86jPLSSy/1WzsB+ClQ//nnn81OIS0t7aBe9I477jA7lvPPP18CzcsvvyzHH3+8NGnSRCIjI6Vdu3Zy2WWXmWDKl1dffVW6du0qUVFR0rFjR3n22Wer9XoDBgyQDh06mJ50fZ1nnnnmoNfhsccek88++6xGj9X3xfuiPwiHHnqoPPLII5KdnX3QbXMyPajX7w8Cw7vvviubN2+25UmJg9kH1IfMzEx54IEH5NRTT5Xk5GSzr9GTdxX1cuuycXFxZnnNQtq1a5fPky9PPPGE2W/rPlnHmOv7XB0XX3yxCcQbNGgg48aNk8cff/yAZRo2bGhqi9x3333Vem4ANfPHH3/IOeecI23atDHf7RYtWsjJJ598wDGXBqmanVgRDWK9j3N0v9K+fXvz/B9//LHZjwSagoICk92zd+9eeeqpp+R///uf2RYA7K3agfrYsWMPKlDX4ml64KQ70y+//FIyMjIkkPz+++/mIE9PJrzwwgvmoE17V4466ijZunXrAWMZ9WDtsMMOMz8W/fr1kxtvvNHngV1lZ0l1e6gff/zR7wfp+uOnPwJ6mTBhghx++OHmgHTUqFEH3TanB+r6/UFgePLJJ82Qk8TERLGbQA/UNYPooYceMgF4z549K1z277//Nic016xZY9brP//5j3z11VdmP5Wfn19q2XvuuUfuvPPOkgP41q1by0UXXSTvvfdelduWk5Mjy5YtM/tkLSy3aNEin8tdffXVpvjnrFmzqvzcAKpPjz2PPPJIWbJkiVx11VXy3HPPmWOvkJAQ+e9//1uj59SOGM9xjga+up/Q2hQarA8cOFDS09MlkKxdu1Y2btxo9n9ay0iPTZOSkuTee+81+ywANmVVw5NPPmnpQ9avX2/V1KxZs8xz6L/h4eHWlClTrIO1dOlSqy4tXLjQtHncuHElt2VnZ1sNGza0hg4dWmrZESNGWLGxsdbevXur9NwLFiwwz/3GG29Y7du3t0477bSDbq++/qhRo2r0WG3Lddddd8Dt55xzjhUSEmLl5ORYteH11183r/Xrr79a/lRUVGTWST/T2h79jNcV3a6+vnL18dq18Rmwk99++82s54wZMyw7Oph9QH3Izc21tm3bZq7rPkDfC90n+HLNNddY0dHR1saNG0tumz59unnM5MmTS277+++/zW+K92fX7XZbxx13nNWyZUursLCwSm17//33zXN/8803Zjtee+215S7brVs3a+TIkVV6XgA1o8dFjRo1slJTUw+4b8eOHaX+btOmzQHHZWXpvlG/277ocZ5+/8877zyrLhUUFFh5eXlVXn7OnDmmXR9++GGly+o2COT9P4Cqq3KPuqbseqrfao+zJ2WovJTw8rz99tsmlfrEE080Y//074OlU+noc2rv786dO6W2eXq7vTMJdFy5pkdee+21pZa97rrrJCsry/T4VIVmF2ga17/+9S8zFGD69OnmeX31QK1cubLS9HN9T/T133jjjZL3qDbGKjVt2tQ8V1hYxfUH9YyvbhMtwqTTG2mKqKZrlfc50fX597//bZZLSEiQSy65RFJTU0sto+PYBw8eLCkpKeY5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novuV9q2bWum7NTf0Yr2A76medPfZM/+qUmTJmZf7v0cFf3GK92H6O+I7q/12POMM86wVqxYUWqZg5merTZ+LwH4h0v/588TBQBQEe111XHh2kOiWRO1RXuWNSNm06ZNJdMAQnz2QmmGkKab4x/ae69DujT9nR51wHm0d1p7pGta2BYAKlOv86gDQEV8zXWtww9UZUM+qksLEurUaTqcAL5pETed51dT4CGlChxq4To9eUSQDgAA/D5G3Rcda+3r4Npb06ZND/ZlcBC06FNlhcZ0fKleAH/SMYdauEzrD+jn8ccffzTTYuk4cq1sXpu0AJtnGh6UpttFe4p1rLkWPPIel43i6f3oRQMAAAEdqGtxHy34VRGy6/1LqyV7T3fli06bV1uFS4Ca0qriWqDviSeeMBXOPQXmfBUGQt3RdE6d2lKrKeuJEi2YBAAAgPpz0GPUV6xYUVK1tzzM3ehfWtFUeyYrotXD62sOaAAAAABA+SgmBwAAAACAnVLfAQCwM51jXTPH4uPjKR4HAABqTPvIMzIypHnz5qZeUkUI1OtJ27ZtTfXqs846y99NAQBUgwbprVq18nczAACAjWqItWzZssJlCNQBAKiA9qR7flQTEhL83RwAABCktFiynvz3HFtUhEAdAIAKeNLdNUgnUAcAAAerKkPpKk6MR61avny5HHHEEeZAb/DgwSXV8nfu3CkjRoww8xXreIWbb75Z8vLyzH06V++wYcOkcePGkpiYKAMGDJAlS5aUPKdOqXb66afLv//9b3O/TsM2e/Zs+eyzz6RDhw6SlJQk99xzj9/WGQAAAABQPQTq9eiVV16Rd955R7Zv3y5NmzaViy++2BQUOPPMM83fa9eulT/++MME4p55o7WI0UUXXSTr16+XHTt2yOGHHy7nnXdeqbnpv/vuOxP47927V0aOHGme9/PPPzfP89NPP8mECRPkt99+8+OaAwAAAACqiunZ6rGY3LXXXit33HGH+VuDbg3O586dawrM7dq1q6Ty3/Tp0+Xqq682gXtZaWlpppf877//lhYtWpge9W+//VbmzZtXMq/9YYcdJitXrpTOnTub2/r06SOjR4+WK6+8sl7XGQDsMp5MM5b27dtH6jsAAKiXYwrGqNejNm3alFxv0qSJREZGys8//2yC7+Tk5JL79NxJUVGRuZ6TkyO33XabfP3116bH3BPM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GzaYvx988EHp1atXqed8+umnpW3btgf0hD/22GPSpEkTadCggTz00ENSWFgot99+uyQnJ0vLli3NCYaDMXHiRLOOsbGx0qpVK7n22mvN9lBZWVmSkJAgH330UanHfPbZZ2b5jIziMZObN2+W8847z7RR2zVs2LCSdfVeF91GzZs3l86dOx9Um1G3fv31V5k8eTInrQA/K3Jb8uiy4v3srV3jJDaMw1HASZIjijNo6FGvGHtGh9J5ubWauJODW2WX9QgLC5NLLrnEBOqWZZXcrkF6UVGRCdBzc3Old+/e8tVXX8myZctk9OjRMnLkSPnll19Klh8zZozpcbzvvvtkxYoV8s4775hgurbNmjVLtm7dKnPnzjUB9QMPPGBONiQlJcmCBQvk6quvln//+98+MwSqSqe0e+aZZ2T58uXyxhtvmNe84447zH0ajF9wwQUHnAzQv8855xyJj4+XgoICGTx4sLmuwwd++ukniYuLM0MMvHvOZ86cKatWrTIZDVOnTj2IrYK6pCdpRowYIS+//LL5nFUkLy9P0tPTS10A1J5PNufIyvRCaRDhkus6xfm7OQDqWRI96lVCoO5QGsRoNXEnB7d2WQ+Pyy+/XNauXStz5swpFXieffbZkpiYaHrS//Of/5ge7/bt28sNN9xggs4PPvjALKu9yP/9739Nj/qoUaPkkEMOkWOPPVauvPJKqW3aO61BtPZAa7v13+zsbLn77rulY8eO5oRBRESE/PjjjzV+DU1tPvHEE01v/kknnSSPPPJIyboqXa9vv/1Wtm3bZv7euXOnfP3116Y96v333zcZBq+88orpme/atavZnps2bTLZBR4a9Osyhx12mLkgMF133XUydOhQGTRoUKXLjhs3znxnPBfNyABQO/Rk8iN/FPem39Q5ThL2j1UF4Mwx6t4dTCiNvaNDRUVG16iauF2CW7ush7cuXbpI//795bXXXjN/r1mzxvQEa9q70p71hx9+2ASdGihr77AGqhp4qj///NP0JA4cOFDqmga0erLIQ3vttV0eoaGh0rBhQxM819SMGTPMuugJCu0V1+wBTf/XEwKqT58+ph3a267eeustadOmjQwYMMD8vWTJErMN9bG6rfSi200zE/SEiIe2W08qIHC999578ttvv5kAvCr0RNG+fftKLjoEAkDt+PLvXFmaViBxYS65sUu8v5sDwI9j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9KgDNksJb9q0qbmu/951111y3HHHya5du6RRo0Y1ft6yjx0/frwccsghcvzxx5u/deqrV1991cxVfdJJJ5nbXn/9denatavMnz9fjj76aJ+96U8//bTce++9MmzYMHPbm2++aXrJP/vsM7ngggvKbY+uz1dffSV//PGHmRNbX2vixIk1Xj8A9pk/3eWikBwAIPjRow7YVGZmprz11ltmHHfDhg1Lbte5pDVNvqby8/PN815++eUlB8SLFi2SgoICGTRoUMlyXbp0MSnK8+bN8/k82iuqQbb3YxITE6Vv377lPsZDX79nz57SuXNnufjii+Xtt9+WwsLCGq8TAoe+91OnTi11m57AadeunTRu3FhGjx5thlYA3ubv+idQBwDADgjUARvRACcuLs5c4uPj5YsvvpD3339fQkL++apr8NysWbMav4b2dut4cu9gXwNuHTeuBd68ae+43ueL5/ayY9greoyH9qBrgO5J99dictrDjuD30EMPyfLly0v+1qyJK664wpzQ0QwRHV4xbtw4v7YRgdujTsV3AIBdEKgDNnLiiSfK4sWLzeWXX34x472HDBlSqhCc9k4eTKCjKe76nM2bNxd/0N77FStWyIUXXmj+1nH4559/vgneEfz0sztw4MCSv7U4oWZZaMHBW2+91dQ0+OCDD/zaRgSWnblFsj6zSDS/5ygqvgMAbIIx6oCNaDE2TXX3eOWVV0w6uQY5jzzyyEE/vwb8M2bMkE8++aTU7ToeXlPitafdu1d9x44dJWPmy/Lcrst49/Dr37169Sq3DRqQFxUVlTpRoOPdQ0NDD3osPvwvNTW1VJaFFibUE0MeOpPB5s2b/dQ6BKIF++dP75IYJokR9D8AAOyBXzTAxnQMuaa95+Tk1MrzaZCs44SHDh1a6vbevXtLeHi4qTTvsWrVKtm0aZP069fP53PpmGMN1r0fk56ebqq/l/cYHZusBesmTJhQkjmglyVLlpjn07HrCG4apHuquuvJn99++61UMcKMjAzzWQPKBuqkvQMA7IQedcBGNJD1jO/WnsnnnnvOFJU744wzSpa55JJLpEWLFtVOf9dx4Bqojxo1yqSbe9Neex1HrKnJOq95QkKCmTpNA27vIEsLzOnr/utf/zInEW6++WbT06/zpmugfd9995me8rPOOstnGz7//HMzHZu+lr6mt3POOce0T6eJQ/A6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reSJJ54QJ5o0aZK0bdvWbIe+ffvKL7/8Ik42btw4OeqooyQ+Pl4aN24sZ511lqxatarUMrm5uXLddddJw4YNJS4uTs4++2zZsWOHONX48ePF5XLJzTffXHIb2+gfW7ZskYsvvthsC90nde/eXRYuXFhyv04cdf/990uzZs3M/YMGDZK//vpLnKSoqEjuu+++Uvvthx9+2GwbJ2+nuXPnyhlnnCHNmzc337HPPvus1P1V2SZ79+6VESNGSEJCgjRo0MD8PmZmZoqTEKgDABAAVq5cKW63WyZPnizLly+Xp556Sl588UW5++67S5ZJT0+XU045Rdq0aSOLFi2SJ598Uh588EF56aWXxEnef/99ufXWW82JjN9++0169uwpgwcPlp07d4pTzZkzxwSY8+fPl+nTp0tBQYH5rGRlZZUsc8stt8iXX34pH374oVl+69atMnz4cHGiX3/91XzXevToUep2tlGx1NRUOeaYYyQ8PFymTZsmK1askAkTJkhSUlLJMnqS8JlnnjH7qQULFkhsbKz5HurJDqd4/PHH5YUXXpDnnntO/vzzT/O3bpdnn33W0dtJ9zu6X9YTqr5UZZuMGDHC/Bbq/mzq1Kkm+B89erQ4is6jDgAAAs8TTzxhtWvXruTv559/3kpKSrLy8vJKbrvzzjutzp07W07Sp08f67rrriv5u6ioyGrevLk1btw4v7YrkOzcuVO79Kw5c+aYv9PS0qzw8HDrww8/LFnmzz//NMvMmzfPcpKMjAyrY8eO1vTp063jjz/euummm8ztbCOr1H7l2GOPLfd+t9ttNW3a1HryySdLbtPtFxkZab377ruWUwwdOtS6/PLLS902fPhwa8SIEeY628mkFliffvppyd9V2SYrVqwwj/v1119Llpk2bZrlcrmsLVu2WE5BjzoAAAFq3759kpycXPL3vHnzZMCAARIREVFym/ZCaIqz9oA5gQ4F0GwCTZX0CAkJMX/r9sE/nx3l+fzoNtNedu/t1qVLF2ndurXjtptmHgwdOrTUtlBso3988cUXcuSRR8q5555rhlIcfvjh8vLLL5fcv379etm+fXupbZWYmGiGoThpW/Xv319mzpwpq1evNn8vWbJEfvzxRxkyZIj5m+10oKpsE/23QYMG5jPoocvrvl574J0izN8NAAAAB1qzZo1Jn/y///u/ktv04EbHQnpr0qRJ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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "t_A = 70, t_B = 30, sum = 100\n" + ] + }, + { + "data": { + "image/png": "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pZea8965D3qqW7dulQM/7+PoT33B3bFjx2GNTYNULZXUubAa+GlnXW1aVFbQdyjlqPqCffnll5e6XoMyfZNo3759qet79OhRfPvBpKWlmXHqfER949DjofMaf/rppwO2HTVqlHmubdu2VfiYGvDp4yQkJJg3FS1b1Teo33//XQ7V2rVrTbmrfimg82qTk5PNef2i4a677jLzbjX41A8HF1100UHfyPSNUrfXLyxefvllU16rgd1ff/1lbr/hhhuK5wd7/87KKs+uSZX9fevvR78w0cB+f7ptZf4uvF84aBCtf8sa5GqZsl4HADg4nZup02p0mk1N0L4EGmx6v3StiH420Z4G2khP+yDoF70PP/ywOe1vwoQJZo7t8OHDzWcd7YHw448/mkBX32v0/XTEiBGm7FkbNnq/bNb30fvvv9+cv/322817qL7XVlZWVpYJdj/44AO56qqr5MUXXzSPr59F9PkA+DHLwQ63DPizzz4z91+zZo25nJaWZkVERFjPPfdctYxPyy61ZLJly5bmebQ884EHHrDWr19/yI85ZMgQKzw8/IBSIi0ZatWq1QHbZ2Zmmue+7777DvrYWgKt286fP99c1vKehIQE65prrjlgWy3Pqcyxv+OOO6zY2NgKy5AffvjhMktsyyqn1dIgve6PP/4ovu7777831+1f6qolrnr9L7/8UuEYu3TpctCSq4rKgPV63YfqKAMuqaIy4Mr+vvV3qZffe++9A7a95557zG05OTkHHYv+DZc8Rs8884wpRd+4cWOV9gkAnOiHH34w0yf01KtXL2vkyJHmvcs77aM6y4DV33//bV7f77rrroM+jm532223lZpSos8fFhZm7d6921yn72e6nb6f79q1q9RjdO3a1apXr561d+/e4usWL15sBQUFWVdddVXxdfpeXFYZcFmfAfYvA37sscfM/q5evbrUdvpep8d08+bNFe4nAN9xbGa1OmgmTjNOWkqitEz17LPPrpZSYKXZuYceeshk+DQ7qd8K6jeRWj6jZZizZ8+u0uNp5vObb76Rs84664BSIm2uoGUx+9OyVu/tB6OZMj0W3iycNt7RDPaXX34peXl5B5ToapxWUbdapePMzMys1Le7laUl1iWXTenZs6f5efrpp5cqdfVer+VNBxvj8uXLZc2aNRIoKvv79v48nL+NP//8UzZs2FBcgaC8DZy0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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "constrain_to_sum(t_b, [t_a, t_b], total=100.0)\n", + "\n", + "t_a.value = 30.0\n", + "print(f't_A = {t_a.value:g}, t_B = {t_b.value:g}, sum = {t_a.value + t_b.value:g}')\n", + "show_sample(model, 't_A -> 30 Å: t_B absorbs the change (sum stays 100 Å)')\n", + "\n", + "t_a.value = 70.0\n", + "print(f't_A = {t_a.value:g}, t_B = {t_b.value:g}, sum = {t_a.value + t_b.value:g}')\n", + "show_sample(model, 't_A -> 70 Å: sum still 100 Å')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "32af147e", + "metadata": {}, + "outputs": [], + "source": [ + "# release it again and restore the starting structure\n", + "unconstrain(t_b)\n", + "t_a.value = 40.0\n", + "t_b.value = 60.0" + ] + }, + { + "cell_type": "markdown", + "id": "0afca80c", + "metadata": {}, + "source": [ + "## 3. Inequality constraints\n", + "\n", + "Inequalities are different from the dependencies above: **no parameter leaves the fit**.\n", + "Instead the constraint is declared on the *project* and translated, at the start of every fit, into a penalty on the BUMPS fit problem.\n", + "While the constraint is violated BUMPS skips the model evaluation and adds a penalty that grows with the violation, steering the optimizer back into the allowed region.\n", + "\n", + "Constraints reference parameters by **structural path** (stable across save/load, unlike unique names):" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fe203b23", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "models/0/sample/1/layers/0/thickness\n", + "models/0/sample/2/layers/0/thickness\n" + ] + } + ], + "source": [ + "path_a = project.parameter_path(t_a)\n", + "path_b = project.parameter_path(t_b)\n", + "print(path_a)\n", + "print(path_b)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b615bd3c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "A below B a < b lhs=40 rhs=60 satisfied\n", + "budget a + b < 90 lhs=100 rhs=90 VIOLATED\n" + ] + } + ], + "source": [ + "project.add_inequality_constraint(\n", + " InequalitySpec('a', '<', 'b', lhs_paths={'a': path_a}, rhs_paths={'b': path_b}, name='A below B')\n", + ")\n", + "project.add_inequality_constraint(\n", + " InequalitySpec('a + b', '<', '90', lhs_paths={'a': path_a, 'b': path_b}, rhs_paths={}, name='budget')\n", + ")\n", + "\n", + "for spec, evaluation in zip(project.inequality_constraints, project.evaluate_inequality_constraints()):\n", + " status = 'satisfied' if evaluation.satisfied else 'VIOLATED'\n", + " print(f'{spec.name:10s} {spec!s:12s} lhs={evaluation.lhs:g} rhs={evaluation.rhs:g} {status}')" + ] + }, + { + "cell_type": "markdown", + "id": "1b2e5cc4", + "metadata": {}, + "source": [ + "Expressions are unit-checked when a constraint is registered — comparing a thickness with an SLD is refused:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7697b28d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Incompatible units in 'a < s': left side is in 'Å', right side in '1/Å^2'.\n" + ] + } + ], + "source": [ + "try:\n", + " project.add_inequality_constraint(\n", + " InequalitySpec(\n", + " 'a',\n", + " '<',\n", + " 's',\n", + " lhs_paths={'a': path_a},\n", + " rhs_paths={'s': project.parameter_path(film_a.layers[0].material.sld)},\n", + " )\n", + " )\n", + "except ValueError as error:\n", + " print(error)" + ] + }, + { + "cell_type": "markdown", + "id": "6151592e", + "metadata": {}, + "source": [ + "A fit started from a point that already violates a constraint would begin on the penalty plateau, where only the penalty slope guides the optimizer — so check the start point first.\n", + "Our current values ($40 + 60 = 100$) violate the 90 Å budget; we move to a feasible start:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "22ab8f72", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "violated now: ['budget']\n", + "after moving : none\n" + ] + } + ], + "source": [ + "print('violated now:', [spec.name for spec in project.violated_inequality_constraints()])\n", + "\n", + "t_a.value, t_b.value = 30.0, 50.0 # 30 < 50 and 30 + 50 = 80 < 90\n", + "print('after moving :', [spec.name for spec in project.violated_inequality_constraints()] or 'none')" + ] + }, + { + "cell_type": "markdown", + "id": "3e6e8543", + "metadata": {}, + "source": [ + "### Simulated data whose true answer violates the budget\n", + "\n", + "We simulate a measurement from $t_A = 45$ Å, $t_B = 55$ Å — a film whose total (100 Å) breaks the 90 Å budget on purpose — and add 5 % noise.\n", + "This is the interesting case: the *unconstrained* optimum lies outside the allowed region, so the constrained fit must settle on the boundary." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "5aa5e28a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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b4FJVF3MyFaCGVkiIiIi94y96sWECERERETkfRjIlkPKCjp06IDevXu7eleIiIiIqAwsLagESwuIiIiI6g5LC4iIiIjI6zGQJSIiIiKPxEC2HKyRJSIiInJvrJGtBGtkiYiIiOoOa2SJiIiIyOsxkCUiIiIij8RAloiIiIg8EgNZIiIiIvJIDGTLwa4FRERERO6NXQsqISPmIiIicPLkSXYtICIiIqqDrgVNmjRBZmam6l5QET9n74yny8nJUf+XA0pEREREdReDVRbIMiNbCavVijNnziA0NBQ+Pj7l/mpgxtY1ePxdh8fedXjsXYfH3nV47OvPsdc0TQWxDRs2hE5XcRUsM7KVkAPYuHHjSpeTF5b/sFyHx991eOxdh8fedXjsXYfHvn4c+/BKMrE2HOxFRERERB6JgSwREREReSQGsrXk7++PKVOmqP9T3ePxdx0ee9fhsXcdHnvX4bF3HX83PvYc7EVEREREHokZWSIiIiLySAxkiYiIiMgjMZAlIiIiIo/EQLYM//73v9G8eXMEBASgb9++2LhxY4XLf//992jXrp1avnPnzvj5559LPC5lyJMnT0ZCQgICAwMxdOhQHDx40MnPwjM5+tjffffdaiIL+8tVV13l5Gfh/cd+z549uPHGG9XyckxnzpxZ63XWZ44+9i+99NIF73v5d0K1P/4ff/wxBgwYgMjISHWRz/PSy/Mz33XHnp/5zjn2P/74I3r16oWIiAgEBwejW7du+OKLL9zjfS+Dvehv33zzjWYwGLTZs2dre/bs0caOHatFRERoycnJZS6/du1azdfXV5s2bZq2d+9e7YUXXtD0er22a9eu4mXeeOMNLTw8XJs/f762Y8cO7dprr9VatGihFRQU1OEzq5/HfsyYMdpVV12lnT17tviSnp5eh8/KO4/9xo0btaeeekr773//q8XHx2vvvPNOrddZXznj2E+ZMkXr2LFjiff9uXPn6uDZeP/xv+2227R///vf2rZt27R9+/Zpd999t/p8P3XqVPEy/Mx33bHnZ75zjv3KlSu1H3/8UX3XHjp0SJs5c6b6/l2yZInL3/cMZEvp06ePNn78+OLbFotFa9iwoTZ16tQyl7/55pu1ESNGlLivb9++2oMPPqiuW61W9WUzffr04sczMzM1f39/9UVEzjv2tg+16667zol7XT+Pvb1mzZqVGUzVZp31iTOOvQSyXbt2dfi+eqPavk/NZrMWGhqqff755+o2P/Ndd+wFP/OrxhGfz927d1cJJFe/71laYMdoNGLLli0qHW4/Ra3cXr9+fZl/I/fbLy+GDRtWvPzRo0eRlJRUYhmZdk3S+OWtsz5yxrG3WbVqFWJjY9G2bVuMGzcOaWlpTnoW9efYu2Kd3siZx0lO6ck85S1btsTtt9+OEydOOGCPvYsjjn9+fj5MJhOioqLUbX7mu+7Y2/Az37nHXpKgK1asQGJiIi677DKXv+8ZyNpJTU2FxWJBXFxcifvltrxAZZH7K1re9v/qrLM+csaxF1IbNXfuXPWP7s0338Tvv/+O4cOHq21RzY+9K9bpjZx1nOTLY86cOViyZAk++OAD9SUjtYU5OTkO2Gvv4Yjj/8wzz6gfDLYvcH7mu+7YC37mO+/YZ2VlISQkBAaDASNGjMB7772HK664wuXvez+nrp3IxW655Zbi6zIYrEuXLmjVqpX6xT5kyBCX7huRs8gXt4285yWwbdasGb777jvcd999Lt03b/LGG2/gm2++UZ8nMmCGXH/s+ZnvPKGhodi+fTtyc3PVD4UJEyaoMz6DBg2CKzEjaycmJga+vr5ITk4ucb/cjo+PL/Nv5P6Klrf9vzrrrI+ccezLIv/oZFuHDh1y0J7Xz2PvinV6o7o6TjLSuE2bNnzfO/D4v/XWWyqYWrZsmQqWbPiZ77pjXxZ+5jvu2Ev5wUUXXaQ6Fjz55JO46aabMHXqVJe/7xnI2pF0ec+ePdUvDRur1apu9+vXr8y/kfvtlxe//vpr8fItWrRQL6L9MtnZ2fjzzz/LXWd95IxjX5ZTp06peilpD0I1P/auWKc3qqvjJBmUw4cP833voOM/bdo0vPrqq6p0Q1oS2eNnvuuOfVn4me+8zx35m6KiIte/7506lMxDW1LIKLs5c+aoNhMPPPCAakmRlJSkHr/zzju1Z599tkQLKD8/P+2tt95S7UBktHBZ7bdkHQsWLNB27typRlSyFYvzj31OTo5qU7R+/Xrt6NGj2vLly7UePXporVu31goLC132PL3h2BcVFakWOHJJSEhQx1muHzx4sMrrJOcd+yeffFJbtWqVet/Lv5OhQ4dqMTExWkpKikueozcdf/k8l7ZF//vf/0q0eJLPG/tl+Jlf98een/nOO/avv/66tmzZMu3w4cNqefnele/fjz/+2OXvewayZXjvvfe0pk2bqn8w0qJiw4YNxY8NHDhQtfew991332lt2rRRy0vvxsWLF5d4XNpSvPjii1pcXJx64wwZMkRLTEyss+dTX499fn6+duWVV2oNGjRQAa60KpJeeQykan/s5UtCfgeXvshyVV0nOe/Yjx49WgW5sr5GjRqp29L7kWp//OVzpKzjLz+kbfiZ75pjz8985x37559/Xrvooou0gIAALTIyUuvXr58Khu256n3vI/9xbs6XiIiIiMjxWCNLRERERB6JgSwREREReSQGskRERETkkRjIEhEREZFHYiBLRERERB6JgSwREREReSQGskRERETkkRjIEhEREZFHYiBLRFQPrF+/Hh06dFAXuU5E5A04sxcRUT3Qt29fPPHEE7BarXj33Xfx559/unqXiIhqza/2qyAiIncXHh6OVq1aQXIXUVFRrt4dIiKHYEaWiMjDff755/j444+xZs2acpfZtGmTysrarvfs2bPK63/ooYewdetWVZYwZ84ch+wzEZEjsEaWiMjDLViwANdee22Fy6xbtw6XXnopLrnkEnW9OmbNmoWHH364lntJROR4LC0gInIDeXl5GDduHH788UeEhobiqaeewk8//YRu3bph5syZ5f5dYWEhli1bhtdff73C9X/22Wdq/XIS7qOPPsKjjz5a/JjUy77zzjsX/I3U0sbFxdXymREROQ8DWSIiNzBx4kT8/vvvKrsaGxuL5557Tp3Ol0C2IitWrECjRo3Qrl27cpfZtm0b9u3bh5tvvlkFso899hh27tyJLl26qMel5OCbb75x+HMiInI2lhYQEblYbm4uPv30U7z11lsYMmQIOnfurOpezWazQ8oKJBt79dVXIzIyUg30Gj58uLqvqh5//HG88cYbWLJkCQYNGgSLxVLlvyUiciYGskRELnb48GEYjcbiwVhCAs62bdtW+HeSXZXyg4oCWVnv119/jTvuuKP4Prn+1VdfwWQyVWn/pLRh//79SEpKwqpVq+Dr61ulvyMicjaWFhAReaiNGzeqrK0M4CrPwoULkZaWhtGjR5e4X7KqixYtwvXXX18He0pE5BzMyBIRuZj0d9Xr9SUmKcjIyMCBAwcqLSsYMWJEhRlSKSG45ZZbsH379hKX22+/na20iMjjMSNLRORiISEhuO+++9SAr+joaDXY6/nnn4dOV3GuQbKtr7zySrmPnz17FkuXLlWZ106dOpV4bMyYMapuNjk5mZ0JiMhjMSNLROQGpk+fjgEDBmDkyJEYOnQo+vfvX+GkBVJXe+jQIQwbNqzcZebOnataeckAstIGDx6MsLAwfPnllw57DkREdY0zexERuSnpEFBeH9kZM2Zg+fLl+Pnnn12yb0RE7oAZWSIiD9S4cWNMmjTJ1btBRORSrJElIvJAMrkBEVF9x9ICIiIiIvJILC0gIiIiIo/EQJaIiIiIPBIDWSIiIiLySAxkiYiIiMgjMZAlIiIiIo/EQJaIiIiIPBIDWSIiIiLySAxkiYiIiMgjMZAlIiIiInii/wfLBiMKrHCmnwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "q = np.linspace(0.01, 0.3, 150)\n", + "\n", + "t_a.value, t_b.value = 45.0, 55.0 # the \"truth\"\n", + "r_true = model.interface.fit_func(q, model.unique_name)\n", + "rng = np.random.default_rng(42)\n", + "r_measured = r_true * rng.normal(1.0, 0.05, size=q.size)\n", + "t_a.value, t_b.value = 30.0, 50.0 # back to the feasible start\n", + "\n", + "dataset = DataSet1D(name='simulated', x=q, y=r_measured, ye=(0.05 * r_true) ** 2)\n", + "\n", + "plt.figure(figsize=(7, 4))\n", + "plt.errorbar(q, r_measured, yerr=0.05 * r_true, fmt='.', ms=4, alpha=0.6, label='simulated data (truth: 45 + 55 Å)')\n", + "plt.plot(q, model.interface.fit_func(q, model.unique_name), color='#00a3e3', label='model at the start point (30 + 50 Å)')\n", + "plt.yscale('log')\n", + "plt.xlabel('q / Å⁻¹')\n", + "plt.ylabel('R(q)')\n", + "plt.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "006daf09", + "metadata": {}, + "source": [ + "Only the two thicknesses are fitted; everything else stays fixed:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "889379a6", + "metadata": {}, + "outputs": [], + "source": [ + "for assembly in model.sample:\n", + " for layer in assembly.layers:\n", + " for parameter in (layer.thickness, layer.roughness, layer.material.sld, layer.material.isld):\n", + " parameter.fixed = True\n", + "t_a.fixed = False\n", + "t_b.fixed = False\n", + "model.scale.fixed = True\n", + "model.background.fixed = True" + ] + }, + { + "cell_type": "markdown", + "id": "9d2f54ee", + "metadata": {}, + "source": [ + "### Engines that cannot enforce inequalities are screened out\n", + "\n", + "Inequality penalties live in the BUMPS fit problem, so they only work with the BUMPS minimizers (and DREAM sampling).\n", + "Selecting LMFit with active inequalities raises immediately — physics constraints are never silently dropped:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ae411300", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "CollectionBase is deprecated and will be removed in a future version. Please migrate to ModelBase or EasyList.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inequality constraints (constraints_factory) require the BUMPS engine; the selected minimizer uses 'lmfit'.\n" + ] + } + ], + "source": [ + "project.minimizer = AvailableMinimizers.LMFit\n", + "try:\n", + " project.fitter.fit_single_data_set_1d(dataset)\n", + "except ValueError as error:\n", + " print(error)" + ] + }, + { + "cell_type": "markdown", + "id": "871df28a", + "metadata": {}, + "source": [ + "### The constrained fit\n", + "\n", + "With a BUMPS minimizer the project's inequality constraints are picked up automatically:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "b4f076db", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "success = True\n", + "truth (outside the budget): t_A = 45.00, t_B = 55.00, sum = 100.00\n", + "constrained fit: t_A = 37.78, t_B = 52.22, sum = 90.00\n" + ] + } + ], + "source": [ + "project.minimizer = AvailableMinimizers.Bumps\n", + "result = project.fitter.fit_single_data_set_1d(dataset)\n", + "\n", + "print(f'success = {result.success}')\n", + "print('truth (outside the budget): t_A = 45.00, t_B = 55.00, sum = 100.00')\n", + "print(f'constrained fit: t_A = {t_a.value:.2f}, t_B = {t_b.value:.2f}, sum = {t_a.value + t_b.value:.2f}')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "27589108", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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376V3t8LL7yAp59+GnWl7/cHkbn/GPQWCw4disBdg1ox0CYi8gFSUXX//ferrS4NGjQI3bt3VweVK/Pee+/hmWeeUd958j0pzfGkEkyCGCJvNXDgQE8PwedptZye6y+CocXmuAHq9KHDh9CsXVtPD8mveH2QXRWTyYT169dj8uTJzsu0Wi2GDBmCVatWqfMSUG/ZskX90JDGZ7/88gueeOKJSu9T7ksy466Z7JSUFLeNOdZuRo7FAmNgEFILjDicU8wgm4jIA6oTrNbE2rVrERoaCk+T763x48er4PrKK69U332yusY999zj3OeWW25xBt5E3qLbxOmIbNvd08PwWel//YbtHz6HD/8vAe0aGzw9HKpjGrMGeKv0dFZWFpp5ekB+pkEH2ZmZmWr5msTExDKXy/nt20u76en1erz66qsYPHiw+pHx8MMPV9lZXEpoZJN53rLJ/btT89hQ7Digh8FYgsDAMDSJLl0WhYiIvHONaPkekO+S04mPj4c3OHjwoOoAfvHFF5dZ05pluOTtQlPaIKptN08Pw2flH9ip/kqA3aMFS8Z9nc2owbFgK44X2sD1IOqf1zc+c4dLL70UO3fuxO7du3HHHXdU6zbjxo3D1q1bVWbCndonhiEvLh75MbHo2zmFWWwi8lty4POll15C69at1cHNpk2b4rnnnnNev3nzZpx//vlqjWY5OCqf3wUFBWWysaNGjcIrr7yigknZRz67Hcuaibfffhtt2rRRjTHlAOxVV13lvO3SpUvx+uuvq6aasu3fvx9LlixRp6XqSZZ8lHEtX74ce/bswWWXXabuQ4LVPn36YNGiRaeUi7tmxeV+PvjgA7VspCx/JeP48ccfy9xGKq1kSpTcp9z3TTfdpA4gu/Ylufnmm9X18hzloHFVZs+ejS5duqjTLVu2dD6vp556SmXthZz++OOP8cMPPzifuzxvIiLyHdpAO9LvOoS+2auBIB5UqW8NOpMdFxen1upMS0src7mcT0pKqtV911UmOyVUB0tgkNpMgUFuvW8iIofe89OQWlL/E++SgrRYd1HZ6qKqpue8//77eO2113DOOefg2LFjziokCS6liWX//v3Vwc709HTcfvvtqgxaAkmHP/74QwWf8lcOpF577bUqmJReHevWrcO9996LTz/9FAMGDEB2djb+/PNPdTsJruXga+fOnTF16lRnJloC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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6sAAAGZCAYAAABvz6cKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAbrRJREFUeJzt3Qd4VGXWwPEz6b0RSuhNQOkgINhAWYqIBfzsimV1XbuoKK69oa6oq2JfwYKrYhdFRaUoohSld6T3AOnJJJmZ7zlvmDGBJMwkgZm58/89z5jpee9lzJ1zz3nPa3O5XC4BAAAAACCAhPl7AAAAAAAAHIxgFQAAAAAQcAhWAQAAAAABh2AVAAAAABBwCFYBAAAAAAGHYBUAAAAAEHAIVgEAAAAAAYdgFQAAAAAQcAhWAQAAAAABh2D1KFm7dq0MGjRIkpOTxWazyWeffSaBZNKkSWZcCxYs8PdQLGnmzJlm/+pPf9Df/eCDDx72ebt27ZLzzjtP6tWrZ17z3HPP+X3sAACo+fPnS79+/SQ+Pt4clxYtWmSObXq9vJYtW8oVV1zht3ECqDsEq0fJqFGjZOnSpfLYY4/JO++8I8cff7y89957Jhg4ml566SUTmOJQ7BuR2267Tb799lsZO3as+ZwOGTKk0uf547MLAKFMv0PoycQWLVpITEyMNGnSRP72t7/JCy+8cEigduaZZ1b7XhrIaYDnviQkJEjr1q3N+3/88cfidDol0JSUlMj//d//yb59++TZZ581xyjdFwCsLcLfAwgFhYWFMnfuXPnXv/4lN954Y4Uv/MuWLZNbb731qAZk6enpnHE8yvvmlFNOMZ+DqKgoCWQ//vijnH322XLHHXd47mvXrt0hY/fHZxcAQtUvv/wiAwYMkObNm8s111wjjRo1ki1btsivv/4q//nPf+Smm27y+T2jo6PljTfeMNf1b/ymTZvkyy+/NAFr//795fPPP5ekpCQJFOvXrzdjfP311+Xvf/+75/57771X7r77br+ODcCRQ7B6FOzZs8f8TElJOeK/S8+GFhcXm7OuOHLy8/NNGZK3wsLCguLfZPfu3Yd8ToNl7ABgVVqVpdOItAz24L/R+ne7JiIiIuTSSy+tcN+jjz4qTzzxhKmu0aD4gw8+kCOltLTUfGfx9iSuezsP3n7dDr0AsCbKgGtBz/Bdf/310r59e4mNjTXz/LREZePGjZ7n6FwKd5nKnXfeacpttERHz1p+9dVX5j3cZTh6v5vdbpcHHnhA2rZta85+NmvWTMaMGWPuL09fp9nayZMnS8eOHc1zv/nmm0rHq++/fPlymTVrlud36jjK0/cfPXq01K9f3wRj5557rifYLm/atGly8sknm+ckJibKsGHDzHt7Iysry5Sb6nh0vE2bNpXLL79cMjMzKxyUrr76amnYsKEJlLp27SpvvfVWhffR/azb8PTTT8trr70mbdq0Me/Xq1cvc0Avb+fOnXLllVea36XPycjIMBlE979VdfvGPZ9XH9N/7wYNGpj38fYzoCqb96nv36lTJ1mxYoU5Yx4XF2fKup566qlD9pm3nwe9rftW//303+Wss86SrVu3HvbfxL2NLpdLJkyY4NkHlY39cJ9dAEDdZxX1GF/ZSW89JtUlzVJqj40pU6bImjVrqn2uViJpCfGff/4pgwcPNt8JGjduLA8//LA5nlR2vNYpJO7jtR7/3FU97u8Uuo16fF65cmWF33Pqqaea63qMLX+MrmzOalXfPbQaSI+f+rv1ePrkk08GZMkzgL9wKqoWNCDS0pwLL7zQBC/6x/jll182f0D1D7AGHyNGjDB/eDWAuOiii+SMM84wf9j1D3J2drYJJHTuhdL7lf7h1CDj559/lmuvvVaOPfZYM1dFn6cHjoObM+kf+Q8//NAErVrGWlXgoAcILRXS36MlyUqDwfL08dTUVBMY6fboa/R9y59d1XkiOgdXD0z6h76goMBs90knnSR//PFHtYFLXl6eOSDpQeiqq66SHj16mCD1iy++MPtCx6/lSLoP161bZ353q1atzEFTD1Z6sLnlllsqvKeWpObm5so//vEPc8DSYE/3ux48IyMjzXNGjhxpglHdPh2fBsPTp0+XzZs3m9ve7BsNSjUIvP/++01m1dvPQHX2799v5oXqeM8//3z56KOP5K677pLOnTvL0KFDff48aGnUu+++KxdffLFpQqGfDT2R4E2Zsv67XnbZZWYOlJ48qIrun6o+uwCAuqcnvXU6kU6/0JOcR5oeC7777jtznNSpINVxOBzmOHbCCSeY46+eMNfvEJo51aC1vIkTJ0pRUZE5lmnAmJaWJt9//7053umcWQ089TuAzsM98cQT5ffffzfHaD2+68ncxx9/XG6++WZzUvrgY3R19HuKBrvbtm0z76Xl1Hrs1gzyjh076MEABDIXaqygoOCQ++bOnaunEl1vv/22574NGzaY+/79739XeO6wYcNcLVq0OOQ93nnnHVdYWJjrp59+qnD/K6+8Yt5nzpw5nvv0tj53+fLlXo25Y8eOrlNPPfWQ+ydOnGjea+DAgS6n0+m5/7bbbnOFh4e7srKyzO3c3FxXSkqK65prrqnw+p07d7qSk5MPuf9g999/v/k9n3zyySGPuX/vc889Z57z7rvveh4rLi529e3b15WQkODKycmpsF/r1avn2rdvn+e5n3/+ubn/yy+/NLf3799f6f73dd+cdNJJrtLS0hp9BmbMmGHu059u+rsOfp7dbnc1atTINXLkSJ8/D4sWLTK3r7/++grPu/jii839DzzwgOtw9Hk33HBDhfsqG3tVn10AQN377rvvzLFYL3osHDNmjOvbb781x8aD6d9m/RtdnVGjRrni4+OrfPyPP/4wf/f1O8Dh3kefd9NNN1U4luvvj4qKcu3Zs6fC8TopKcm1e/fuCu/RrVs3V4MGDVx79+713Ld48WJz3Lv88ssPORZNmTKlwuv12Hbw11ndBzo2t0ceecRs75o1ayo87+677zb7dPPmzdVuJwD/oQy4FrTss3yXur1795qyEs2k6tnAmtIsombPOnToYLKO7stpp51mHp8xY0aF5+vZwuOOO07qgp7tLF9Oo1lQPWuqJZ9Kz7JqdlOzxOXHFh4eLn369DlkbAfTLoNa0qvlxQdz/96vv/7aNI/Q3+GmGVI9m6qZWS3HLe+CCy4w2eDyY1aaWXX/O+mcGC1j1UxmTen8Hd3OuvwMaEay/JwhHWfv3r09Y/fl86D7Tel+Ko8mSAAQ3LTiRTOrWmWzePFik8HU6ibNNmplUl1zV8to1ZI3yjePdE9P0v4ZmjUtT6uctELJTbOauvyMVk5pltWtS5cuZpvdx7Xa0uOofjfQ7wrlj6MDBw4033Fmz55dJ78HQN2jDLgWtFRl3LhxpqxFS0vKz8/QMsnarMmqZbLl/6BX10xBy2TripbGlOcOAt1Bno5NuQOlgx2uc6DOu9GDVXU0MD7mmGNMY5/yNGBzP+7LmLXUSMuVb7/9dlM2pKVK2tZfS101KPZWZfu5tp8BLR0+eK6Njn/JkiU+fx50v+g+07lA5el8WgBAcNPS108++cQEgRqwfvrpp2Yqhnbv1YCvrk5aKz0xrLT3weHocUdLeMtzlw4f3L/h4OOo+3he2XFKj/m6lJqvDQ0ro8dRPa56+70KQOAgWK0FneOoQYpmrvr27Ws69WngofMXazNhX1+rcxafeeaZSh/X5gBVZfdq6+DMoZs7CHNvl85vrCzQ80dHvsONWem/0fDhw838Tj343XfffSbI1Dmd3bt39+r3VLafa/sZ8Gbsvn4eAADWpRU4GrjqRYNCbR6omUOdJ1pXdG6s0kqhulSX31d8ocdRzdRqY8LKHG5eLgD/IVitBW2Go42Gxo8f77lPGwdomaw3qupep5kxPWt6+umne9Xhzhe1fT931k67D2r5TE1e7z4IVtdIQs+A6sGlfHZ11apVnsdrQn+3Zlf1omdZu3XrZv7ttCFRTfdNbT8D3o7bm8+D7hfdZ5q9Ln+WevXq1VLX6vpzCQDw3fHHH+8pp61LekJa/85rgHc4etzRqSvlAz53F+HDdYp3H88rO07pMV+bLtY2q+o+jmq2uCbfWwD4F3NWa0GzYuUzYEo72On8B2+4OwIfTLvCakmpLnxdWdmpuxNtTejvrE0gpXNktNRXO/LpHM2DVbbMTXlaAuwuXzqYe19qx2RdaqZ8B2LtKqj7VufRuNvX+9IFUAPIgw9cWt5UfumXmuyb2n4GvOHt58HdPfj555+v8Jwj0eWwqs8uAKDuaW+Cg481yj2nsy6ne+g6q9oJWPtB6JQcb7z44oue6zpOva29JvQka3V0GTk9caxL05U//upJbR2Dfh+oq+OozvnVyqqD6e/V7xgAAhOZ1VrQeY969lFLP3WuiP4h1GYCutamN3r27GkCMl3XVMt5NBDTUlVtGa9L0Vx33XXmAKXt2zX40bOMer/+sXWfTfWV/k5dWkUX/tbyHs2QVjX/tDIaqOrrdYy67IyWu+ocEF0CRtfe1LGWP2gdTNea1WykrpOmS9foePbt22caRLzyyium+ZI2eXr11VdNw4WFCxeaM7P6mjlz5pjAy5s5NOXpGV49YOrBSv+dtFRZg+Vdu3aZ8ddm39T2M+ANbz8PesDXplQvvfSSCSR16ZoffvjBLAFU16r67AIA6p5OOdETr9qcUJvt6bxVXXpF/w7rMVJLgcvTv/t6LDuYTntxL2emAZq7skhP6Or8UT0Wa2WTrv2t65d7Q9dC1+VqtMpIGy3qOuz6feCee+6pco5oef/+97/NyVadSqPrq7uXrtHjqi5lUxf0u4dumx6z9buFHsP0RK8uA6ffL3RurWZxAQQgP3YiDnq6JMqVV17pSk9PN0uqDB482LVq1apDWqZXtXRNXl6eWVZEl4LRx8svBaLt6J988kmznEp0dLQrNTXV1bNnT9dDDz3kys7OrnapkeroEjPaUj4xMdG81r1Ui3t5lvnz5x922RL3/bq9ulxNTEyMq02bNq4rrrjCtWDBgsOOQdvT33jjja4mTZqY1vZNmzY1+yszM9PznF27dnn2rT6nc+fOZozlVbVf3fvFvVSLvq/uow4dOpjW9TrmPn36uD788MNa7RtfPgNVLV2j/74H09cdvCyMt5+HwsJC180332yW89FtHT58uGvLli11vnRNdZ9dAEDdmjZtmuuqq64yxzE91uhxsW3btmbJGD1elqd/j/XvcmWXq6++usKSM+5LXFycq2XLlmbZtI8++sjlcDi8Gpd7CZz169e7Bg0aZN6nYcOG5nhT/j2qO16r77//3nXiiSe6YmNjzfI2euxasWJFhefUZuka99J7Y8eONftN958et/v16+d6+umnK10CCEBgsOl//B0wAwAAILhollIzk+7uwQBQ15izCgAAAAAIOASrAAAAAICAQ7AKAAAAAAg4zFkFAAAAAAQcMqsAAAAAgIDDOqsAAK84nU7Zvn27WevYZrP5ezgAACBIaXFvbm6uNG7cWMLCwgIjWNWFq5977jk555xzjuavBQDUAQ1UmzVr5u9hAAAAi9iyZYs0bdq0ysfJrAIAvKIZVfeBJSkpyd/DAQAAQSonJ8ecAHd/t6gKwSoAwCvu0l8NVAlWAQBAbR1uWtFRb7C0fPly6dGjh/miM3jwYFNWpnbv3i2XXHKJZGRkmNrlW2+9Vex2u3ksLy9Pzj77bGnQoIEkJyfLKaecIosXL/a854MPPihnnnmm/OMf/zCPt2rVSmbOnCmfffaZtG3bVlJTU+Vf//rX0d5UAAAAAEANHfVg9Y033pD33ntPdu7cKY0aNZJLL73UTLA966yzzO3169fL0qVLTTD66KOPepp6XHzxxbJhwwbZtWuXdO/eXc4//3zzOrfvvvvOBL/79u2Tyy67zLzv559/bt5nzpw5Mn78ePn999+P9uYCAAAAAAJ9nVVtsHT99dfLmDFjzG0NPDVAnT17tmm6tGfPHk83qOnTp8t1111ngteDZWVlmWzp1q1bpUmTJiaz+u2338rcuXPN4ytWrJCOHTvKqlWrpH379ua+3r17y7XXXit///vfj9bmAoDl5pdo9Up2djZlwAAA4Ih/pzjqc1ZbtGjhud6wYUOJjo6WX375xQSgaWlpnsc0hnY4HOZ6YWGh3H777fL111+bzKk7oM3MzDTBqvu93OLi4iq9T8uJAQAAAACB76gHq5s2bfJc13mqOi/1xBNPNPNRd+zYUelrtIR34cKF8vPPP5vWxu7M6lFMCgMAAAAArDxn9dVXX5XVq1ebbOldd91lmiX17dvXtC6+9957zeKwGoRqUDtt2jRPmjgmJsYEqJodveeee472sAEAAAAAVg5Wr7rqKrnoootMie62bdtk8uTJEh4eLlOnTjW3jz32WFO/PGzYMFm3bp15zejRo81z9DWdOnUywS0AAAAAwLqOaoMlAEDwosESAAA4mt8pjnpmFQAAAACAwyFYBYAQoEt82Wy2CpcOHTr4e1gAAACB0w0YAOAfuv70999/77kdEcEhAAAABC6+qQBAiNDgtFGjRl4/X5cW00v5+SUAEEqcLpdk2p2yrcAhOwodklXskuwSp2QVuy8uyS11it0hUuRwid3pErvDdeB62X2lLpc4XfpeIs4D7+lw3zb3lXv8wO3DdZTxpuGMV8/xsnNNTRrc2Mpft1V+f/XPsx32Od7+nkN+Z5XPs/k05sgwkYzYcGmdECEDGkXLxS3jJDmKwtW6RLAKACFi7dq10rhxY7MUmHZVHzdunDRv3rzK5+vjDz300FEdIwAcbdprdHuhU5ZllciKbL2UyuqcEtla4DBBarFGmKidGrVz9UcPWN9/p352Fu4rkSmbC+WeRdnyWp9U+b8WcUdkdKHIMt2AFy5caDpJHXPMMf4eCgAEHF23Wtepbt++vezYscMEobpc2LJlyyQxMdHrzKquiU03YADBTL/6LskqkZ92F8vPu+0yZ0+xCUyr0zAmzGTQ0qLDJCUyTJKjbOZnSlSYJEbaJCbcJtFhB36Gi8SE6c+yiybawsQmYTb563LgdrjnPptpJKPXbQd+Ho4XTzH9CerifXx5nnJVdf2gsKPq5x3+Od4+r9rf6dXrD/qd5a5r5lxPaCzLLpF3NxTIyuxSc//b/VLlstbxB40WNekGbJlg9YorrpDZs2fL8uXLJTY21t/DAYCAlpWVJS1atJBnnnlGrr76aq9ew9I1AIKVw+mSGbvs8umWQvlia9EhwakGje2SIuS45Eg5LjlCOiRFSov4cGkaF26C1Ch9AlCNUqdLbluYJS+uzjcnKP44o6EclxLp72EFLG+/U1imDHjs2LHy3nvvyRNPPEHZGgAcRkpKirRr107WrVvn76EAwBGzIa9U3lyXL5P+LKgQoMZH2OTkBlFyYv1oObF+lPROj5L4COYaouYiwmzyn+NTZF1uqXyz3S5jF2XL5/3T/T2soGeZYFVL2+6880558skn5bLLLpO2bdv6e0gAELC0JHj9+vXm7yUAWM2ifcXyxPJcM49Qmxap1CibnNc8Ts5pFiOnNYoxJbtAXdJy7ud6pkjHHbtMBv/3vcXSo16Uv4cV1Cx1Culf//qX6XR50003HVKfDgCh7I477pBZs2bJxo0b5ZdffpFzzz1XwsPD5aKLLvL30ACgzqzKLpGzZmRK9693ywebygLVgY2i5f2T0mT7yMby2gmpckaTWAJVHDHtkyPlvOZlUxLf+rPA38MJepYKVuPi4uQ///mPfPPNN/Lpp5/6ezgAEDC2bt1qAlOtQjn//POlXr168uuvv0r9+vX9PTQAqDVdRmb0gizpPHWXfLmtyDQourBFrCwa1kCmD6wvF7SMI0DFUTOqdVk34P9tLJASd2ofNWKZBktuujnDhw+XJUuWyMqVKyU+nk5cAFAXaLAEIBB9va1Qrp67X3YWla0xc2aTGHm6R7LJcAH+arbU5JMdsrvIKd+fni6nZ8T4e0hB+53CUplVd4vu559/Xvbs2SOPPPKIv4cDAACAIyCvxCnX/bZfhs3YawLV9kkRMu20dPlyQDqBKvzebGnwgQBVu1Cj5iwXrKrWrVub7sDjx4832VUAAABYx5qcEjl+2m55dW2+uX1rhwSzVMiQxmSwEBhOaxRtfv64k2C1NiwZrKoxY8aYNQRvuOEGmi0BAABYxDfbi6T3tN2yOqdUmsSFmzLLZ49PkdgI5qQicAxoWBaszttbLLklZSXq8J1lg9WYmBh58cUXZcaMGfL+++/7ezgAAACopedX5cqwGZmSXeKSfvWjZOHQBswHREBqkRAhLePDxeESmb+32N/DCVqWDVbVkCFDZMSIEXL77bebSbwAAAAIPlol9/CSHLllQbZZjubqNnHy48D60jA23N9DA6rUI61sjdVF+0r8PZSgZelgVT333HOmy9QDDzzg76EAAACgBoHqmN+z5YElZYmHR7omyesnpEo0S9EgwHVPK2v09cd+gtWasnyw2qxZM7n//vvlhRdeMMvZAAAAIHgC1VsXZMvTK/PM7Wd7Jsu9nZPM6g9AoOueeiBY3UcZcE1ZPlhVt912m7Rr106uv/56cTqZ4AwAABAMHl2aK8+vLgtUX+2TIrcem+jvIQFe636gDHhVTqkUltLwtSZCIliNioqSCRMmyJw5c+Ttt9/293AAAABwGK+syZP7D5T+/uf4ZLn2mAR/DwnwSUZsmKRHh5kmSyuzKQWuiZAIVtWAAQPkoosuMkva7N+/39/DAQAAQBU+2Vwo18/LMtfv7ZQoN3cgo4rgo+Xq7ZIizPW1uaX+Hk5QCplgVY0fP16Kiork3nvv9fdQAAAAUInF+4vlsjn7RIsm/3FMvDzcNcnfQwJqrF1iWbC6JodgtSZCKljNyMiQhx9+WF5++WVZuHChv4cDAACAcjKLHHLOzL1S4HDJ3zKi5cVeKTRTQlA7hsxqrYRUsKpuvPFG6dy5M82WAAAAAkiJ0yXn/7RPNuY7pHVCuLx/Uj2JCCNQhTUyqwSrNRNywWpERIS89NJLMm/ePHnjjTf8PRwAAACIyL8WZcuMXXaJj7DJ5/3TJS065L6mwsKZVcqAayYk/wqceOKJcsUVV8jYsWMlMzPT38MBAAAIad9uL5J/ryhbouatfqnSKaVsfUog2LU9kFndV+yUfXaqOn0VksGqevLJJ00Z8N133+3voQAAAISsXYUOGfXLPnP9+nbxMrJ5nL+HBNSZ+IgwaRRTFnJtzCO76quQDVYbNGggjz/+uPz3v/+VuXPn+ns4AAAAIcfpcskVc/fJriKndEqJkKd7pPh7SECdax4fbn5uLnD4eyhBJ2SDVXXttdfK8ccfb5otlZZypgMAAOBoenF1nnyz3S4x4WIaKsVG0FAJ1tM8vqwUeHM+8YavQjpYDQ8PN82WFi9ebJazAQAAwNGxPrdU7v4jx1wf3yNFOjJPFVbPrOaTWfVVSAerqlevXibDeu+998rOnTv9PRwAAICQKP+9eu4+KXS4ZEDDaLmuXby/hwQcMQSrNRfywarSuatRUVFy5513+nsoAAAAlvfymnyZtbvYLFPz376pEmaj/BfW1TzuQBkwc1Z9RrAqImlpaaY78LvvviuzZs3y93AAAAAsSzui3vVHtrn+RPdkaZVQ9kUesH5mlTmrviJYPUDXXe3bt6/ccMMNUlJS4u/hAAAAWI7L5ZIb52dJfqlLTmkQZZaqAUIlWN1Z6JRih8vfwwkqBKsHhIWFmWZLK1eulP/85z/+Hg4AAIDlfL61SL7aViSRYSKv9qH8F6EhPTpMosNENEzdXkgpsC8IVsvp1q2b3HjjjfLggw/K1q1b/T0cAAAAy8gvdcrN87PM9TuPS5QOyXT/RWiw2WzSKLYsu7qDYNUnBKsHefjhhyUxMVFGjx7t76EAAABYxsNLcmRLgUNaxofLvzol+ns4wFGV4QlWnf4eSlAhWD1IcnKyPP300zJlyhT57rvv/D0cAACAoLciq0SeWZlnrr/QK0XiIvgKitCSEVv2mSez6hv+UlTi4osvlv79+5uSYLvd7u/hAAAABLXbf8+SUpfIWU1j5Mymsf4eDuC3zOpOglWfEKxWUVc+YcIE2bBhg8myAgAAoGambSuUb7bbTVOl8T1T/D0cwM9lwASrvgj5ha22b98u+/fvr/Sxyy67TB599FHp1auXNGnS5KiPDRWlpqZK48aN/T0MAADgpRKnS27/vWxN1ZvbJ0jbxJD/6okQxZzVmokI9UD11JP7y549eyQqMloio6IqPO50OqW4uETOPutsSUutX+V6YUX2QvPc2OhYCQsv+yD6wulwSKG90CyfExMdazK7viopLpbiEnul2+GNYNiOpKQE+WHG9wSsAAAEidfW5svK7FKzdMe9nZP8PRzAb5izWjMhHaxqRlUD1UaJx0jHpn0rfc62pLXy27pp0iKxm2SktqrwWKmjRBZu+F6cdrsc32awJMel+zyG7IJMWbB+uiTEpkvPVgMlItz3Nu7rdy2R9fsXSZtG3aRNwy4+vz4YtiO3cJ8s3zPL/JsRrAIAEPj2251y/+Icc/2hLkmSEsXsM4QuyoBrJqSDVaUZPA1UU+MbVvp4SlwD2bp3rSzb8rO0y+jpCcJKHMXy06pPpKg4X07rdKGkJTTy+Xfvy9spizbOlLTERnJyhxESGe57RnTFtl9l4+5l0rnFyXJckxN8fr1VtgMAAASWR5bmyL5ipxyXHCHXHhPv7+EAARGs7rY7xeF0SXiY7xWIoSjkT3EdrmRWS1n7thsu+fYcWbxpVoUAL6dgr5xy7MgaB3izV34sSXH1ahXgrdgyV45r1rdWgWqwbwcAAAgs63NL5cU1ZUvVPNMzRSL4Yo4QVz86TPT/AqdLZI+deaveCvlg1RtaFtul+cmyZPNs2Zu3wxIBHoEqAAA4Uh5YnCMlTpHBGdEyuHGMv4cD+J1mUtOiy0KvTIJVrxGseqlri1MlLipRvl08SbLzM4M6wCNQBQAAR8qyrBJ5b2OBuf5Yt2R/DwcIqOyqyixi3qq3CFa95BKR+JhkKSzOk7aNugVtgEegCgAAjqT7F2eb700jm8dKz3q+f0cArEq7YivKgL1HsOpDgKddcxslt5TlW3+RklK7FJfaZenmn82yL8EQ4BGoAgCAI2l+ZrF8uqXIzM17uAtL1QDl1Y+hDNhXBKs+Bnh6KSrJlz82zZDM3K0yb/00ySvaH/ABHoEqAAA40u5dnG1+XtY6To5L8X0ZO8DK0qPLOgLvKSJY9RbBqo8BXmJsmpm/umzLHLGXFpnnafAayAGeVQJVbzLYAADAP2btsst3O+wSYRN5oDNZVeBgZFZ9F/LrrPoS4K3ePl+Wbf1Fjm81SBJjUmXZ5p/McwuL8wM2wLNKoKol2EX2Qp9fBwAAjjw9ofyvRWVZ1WuOiZfWiXzFBKqcs0qDJa+RWfUhwGua1k5iIxPk+2XvSnx0suzO2WLut5eUdbwLtADPKoGqbsfCDd+L08lZKAAAAtE324tkzp5iiQkXubcTWVWg2m7AZFa9RrDqQ4Cn3YCHdrtKTu4wUvbl7ZAwW1ndeZ697ExioAV4VglUdTvyCrMkNjrW59cDAIAjy2myqjnm+o3tEqRxXNn3IwAVpevZHLoB+4Rg1ccAz2azSbuMHjKyz23SrF57c9/u7M0BGeBZJVDV7Ti+zd8kLJyDHwAAgeaTzYXyx/4SSYy0yV0dE/09HCBgkVn1HRMKahjgxUbFy8DOl8iG3cul3oHnBlqAZ5VAVbdDTxIAAIDA4nC65L7FZVnV0R0SPJkjANXPWdV53ny/PTwyqwea99Q0wGvVoKMJ6gIxwLNKoFqT7QAAAEfeuxsKZFVOqaRFhcnoY8mqAt50Ay52iuSVssqFN0I+WNWzGtq8hwDPOtsBAACOvGKHSx5cUpZV1fLfpKiQ/1oJVCsuIkxiw8uyqay16p2Q/6uiy6Fo855QD/Cssh0AvPPEE0+Y8qNbb73V30MBEKTeWJcvG/Md0igmTG5sH+/v4QBBgbVWfRPywaouh6LNe0I5wLPKdgDwzvz58+XVV1+VLl26+HsoAIJUQalTHl1WllW9t3OSyRgBODzWWvVNyP9l0eVQkuPSQzbAs8p2APBOXl6eXHLJJfL6669Lamqqv4cDIEhNWJ0vOwqd0jI+XK5pS1YV8BYdgX0T8sFqTZZDsUqAZ5XtAOC9G264QYYNGyYDBw487HPtdrvk5ORUuABATrFTnliea64/0CVJog7MwQNweOnRrLXqi5APVkM1wLPKdhxs+PDhMmTIkEof++mnn8wcvSVLloiVXHHFFXLOOef4exgIAu+//778/vvvMm7cOK+er89LTk72XJo1a3bExwgg8D2zMlf2FTulQ1KEXNoqzt/DAYIKc1Z9Q7AaggGeVbajMldffbVMnz5dtm7deshjEydOlOOPP555ekdBcXGxv4eAg2zZskVuueUWmTx5ssTExHj1mrFjx0p2drbnou8BILTttTvkmVV55vrDXZMkIoysKuAL5qz6hmA1xAI8q2xHVc4880ypX7++TJo06ZB5elOmTDHB7N69e+Wiiy6SJk2aSFxcnHTu3Fn+97//HdJ466mnnpK2bdtKdHS0NG/eXB577DHz2MyZM02GNisry/P8RYsWmfs2btxobj/44IPSrVu3Cu/53HPPScuWLQ/JiD7++OPSsGFDSUlJkYcfflhKS0vlzjvvlLS0NGnatKkJsmvjmWeeMdsYHx9vMmPXX3+92R8qPz9fkpKS5KOPPqrwms8++8w8Pze3rMxLg5Tzzz/fjFHHdfbZZ3u2tfy26D5q3LixtG/fvlZjRt1buHCh7N69W3r06CERERHmMmvWLHn++efNdYfj0IOmfvb181H+AiC0Pbk8V3JLXNItNVJGNo/193CAoENm1TcEqyEU4FllO6qjX7ovv/xyE6zqGrpuGqjql3ENUouKiqRnz57y1VdfybJly+Taa6+Vyy67TObNm1cho6RLe9x3332yYsUKee+990xAWdd+/PFH2b59u8yePdsElQ888IAJuLXxzW+//SbXXXed/OMf/6g0U+ytsLAwE5AsX75c3nrrLfM7x4wZYx7TgPTCCy88JCDW2+edd54kJiZKSUmJDB482FzXUuo5c+ZIQkKCKbcun0H94YcfZPXq1SazPXXq1FrsFRwJp59+uixdutScWHFftNJAmy3p9fAazN8HEFq2Fzjk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zZdhnQvnpl0J27dvH9IlwEr3oe43ncNVPnujt3W/oSLtMaKB6qeffio//vjjIeXlui+1U2v5/alzozdv3sz+FJHTTz9dli5dKosWLfJcdC6blmC6r7P/qqYl5wcvlaRzL7XTudLPowYA5feflrzq3ED2X5mCgoJDjhF6wk7/7in2ofe82Vf6U08+6YkqN/3bqftb57bWuTpv2QQAQC1s3brV1bZtW9fpp59uru/YscNzKd+dsGHDhq7LLrvMtWzZMtf777/viouLc7366qt+HXug0P2h3RsnTZpkOjdee+21rpSUFNfOnTv9PbSA889//tOVnJzsmjlzZoXPWkFBgec51113nat58+auH3/80bVgwQJX3759zQWVK98NWLH/qjZv3jxXRESE67HHHnOtXbvWNXnyZPO37N133/U854knnjD//37++eeuJUuWuM4++2zTHb2wsNCvYw8Uo0aNcjVp0sQ1depU14YNG1yffPKJKz093TVmzBjPc9iHFTt3//HHH+aioeAzzzxjrm/atMnrfTVkyBBX9+7dXb/99pvr559/Np3AL7roIteRQLAKAAgoEydONAfQyi7lLV682Cz5oEGZflHRAyz+8sILL5gAISoqyixl8+uvv/p7SAGpqs+afg7d9Eva9ddfb5ZL0kDi3HPPrXDyBNUHq+y/6n355ZeuTp06mb9lHTp0cL322msVHtflRO677z5zgk6foyfyVq9e7bfxBpqcnBzzedO/dzExMa7WrVu7/vWvf1VY2ox9+JcZM2ZU+jdPg35v99XevXtNcJqQkOBKSkpyXXnllSYIPhJs+p+6z9cCAAAAAFBzzFkFAAAAAAQcglUAAAAAQMAhWAUAAAAABByCVQAAAABAwCFYBQAAAAAEHIJVAAAAHDVOp1OuvfZaycjIMD9ZmAJAVQhWAQAAcNR8++23smbNGpk2bZqsWrVKvvnmG38PCUCAIlgFAADAUZOcnCypqanStm1bSUtLMxcAqAzBKgAAAOpMq1at5Pvvv6/y8X79+klxcbEJWh0Oh/Tp0+eojg9A8CBYBQAAQJ1YsmSJ7N+/X0499dQqn1NSUiLz58+XMWPGmJ+lpaVHdYwAggfBKgAAACrYuHGj2Gy2Qy79+/ev9nWff/65DBkyRCIjI6t8zldffSVRUVHy8MMPS3h4uHz99ddHYAsAWAHBKgAAACpo1qyZ7Nixw3P5448/pF69enLKKadU+7ovvvhCzj777GqfM3HiRLnoootMQKs/9TYAVMbmol84AAAAqlBUVGQyqvXr1zeZ07CwynMd27Ztk9atW8uuXbskJSWl0ufoY02bNpUFCxZI165dZdGiRdK7d2/zWn1/ACiPzCoAAACqdNVVV0lubq689957VQaq7qzqSSedVGWgqt59913p0KGDCVRVt27dpF27djJ58uQjMnYAwY1gFQAAAJV69NFHzbqoGogmJiZW+1x9zllnnVXtc7Tkd/ny5RIREeG5rFixQiZNmlTHIwdgBZQBAwAA4BAff/yxmVM6bdo0Of3006t9bl5enqSnp8uqVaukZcuWlT5HO//qMjUzZ86ssLZqVlaWmQu7cOFC6d69e51vB4DgFeHvAQAAACCwLFu2TC6//HK56667pGPHjrJz505zv3bxLR9oun3zzTemnLeqQNWdVdX5qZU1aerbt695nGAVQHmUAQMAAKACbYBUUFBgyoAzMjI8lxEjRlT6fG28VF0JsDZp+t///icjR46s9HG9X+fEFhcX19k2AAh+lAEDAACgxkpLS6Vhw4amXFgzpwBQV8isAgAAoMb27dsnt912m/Tq1cvfQwFgMWRWAQAAAAABh8wqAAAAACDgEKwCAAAAAAIOwSoAAAAAIOAQrAIAAAAAAg7BKgAAAAAg4BCsAgAAAAACDsEqAAAAACDgEKwCAAAAAAIOwSoAAAAAIOAQrAIAAAAAJND8P9FBP+hRs6aqAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, (ax_fit, ax_bars) = plt.subplots(1, 2, figsize=(10, 4), width_ratios=[1.6, 1.0])\n", + "\n", + "ax_fit.errorbar(q, r_measured, yerr=0.05 * r_true, fmt='.', ms=4, alpha=0.5, label='simulated data')\n", + "ax_fit.plot(q, model.interface.fit_func(q, model.unique_name), color='#00a3e3', lw=2, label='constrained fit')\n", + "ax_fit.set_yscale('log')\n", + "ax_fit.set_xlabel('q / Å⁻¹')\n", + "ax_fit.set_ylabel('R(q)')\n", + "ax_fit.legend()\n", + "\n", + "bars = {\n", + " 'truth\\n(violates budget)': (45.0, 55.0),\n", + " 'constrained fit': (t_a.value, t_b.value),\n", + "}\n", + "for position, (label, (a_value, b_value)) in enumerate(bars.items()):\n", + " ax_bars.barh(position, a_value, color='#4ec1ef', edgecolor='black', label='t_A' if position == 0 else None)\n", + " ax_bars.barh(position, b_value, left=a_value, color='#f5a623', edgecolor='black', label='t_B' if position == 0 else None)\n", + " ax_bars.text(a_value + b_value + 1.5, position, f'{a_value + b_value:.1f} Å', va='center')\n", + "ax_bars.axvline(90.0, color='crimson', ls='--', lw=1.5)\n", + "ax_bars.text(90.0, 1.55, ' budget: 90 Å', color='crimson', fontsize=9)\n", + "ax_bars.set_yticks(range(len(bars)), bars.keys())\n", + "ax_bars.set_xlabel('thickness / Å')\n", + "ax_bars.set_xlim(0, 120)\n", + "ax_bars.legend(loc='lower right')\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "show_sample(model, 'after the constrained fit')" + ] + }, + { + "cell_type": "markdown", + "id": "8f438b29", + "metadata": {}, + "source": [ + "The fit lands **exactly on the boundary** $t_A + t_B = 90$ Å — the closest allowed film to the data — with the ordering $t_A < t_B$ respected as well." + ] + }, + { + "cell_type": "markdown", + "id": "8690e53a", + "metadata": {}, + "source": [ + "## 4. Everything survives save / load\n", + "\n", + "Equality constraints, derived parameters and inequality specs are all part of the project file.\n", + "We serialize the project to a plain dictionary (this is exactly what `save_as_json` writes), wipe the session, and rebuild:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91222868", + "metadata": {}, + "outputs": [], + "source": [ + "constrain(r_b, '1.5 * r', r=r_a) # add the equality constraint again\n", + "\n", + "project_dict = json.loads(json.dumps(project.as_dict()))\n", + "\n", + "global_object.map._clear() # simulate a fresh Python session\n", + "reloaded = Project()\n", + "reloaded.from_dict(project_dict)\n", + "reloaded_model = reloaded.models[0]\n", + "\n", + "print('inequalities :', [str(spec) for spec in reloaded.inequality_constraints])\n", + "\n", + "reloaded_r_a = reloaded_model.sample[1].layers[0].roughness\n", + "reloaded_r_b = reloaded_model.sample[2].layers[0].roughness\n", + "reloaded_r_a.value = 6.0\n", + "print(f'equality : r_A = {reloaded_r_a.value:g} -> r_B = {reloaded_r_b.value:g} (still 1.5x)')\n", + "print(f'derived : total_thickness = {reloaded_model.total_thickness.value:g} Å')" + ] + }, + { + "cell_type": "markdown", + "id": "e9ace3c1", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| constraint | API | enters the fit? | engines |\n", + "|---|---|---|---|\n", + "| equality (`r_B = 1.5 r_A`) | `constrain`, `constrain_equal`, `unconstrain` | the follower leaves the fit | all |\n", + "| derived value (`total_thickness`) | `Model.total_thickness`, `derived_parameter`, `constrain_to_sum` | never | all |\n", + "| inequality (`t_A + t_B < 90`) | `InequalitySpec` + `Project.add_inequality_constraint` | both parameters stay free; a penalty steers the fit | BUMPS minimizers & DREAM only |\n", + "\n", + "Useful checks before fitting with inequalities: `project.violated_inequality_constraints()` (feasible start point) and remember that `Bumps_lm` enforces them only weakly (the penalty is spread over the residuals) — prefer `Bumps` (amoeba) or `Bumps_newton`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cpython-3.11.12-windows-x86_64-none (3.11.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb index ab9402ee..1520b4ac 100644 --- a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb +++ b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb @@ -401,7 +401,14 @@ { "cell_type": "code", "id": "041f4e4c", - "source": "from easyreflectometry import constrain # noqa: E402\nfrom easyreflectometry import unconstrain # noqa: E402\n\nconstrain(d13d2o.head_layer.thickness, '0.5 * t', t=d13d2o.tail_layer.thickness)\nd13d2o.tail_layer.thickness.value = 22\nd13d2o.head_layer.thickness.value, d70d2o.head_layer.thickness.value, d83acmw.head_layer.thickness.value", + "source": [ + "from easyreflectometry import constrain # noqa: E402\n", + "from easyreflectometry import unconstrain # noqa: E402\n", + "\n", + "constrain(d13d2o.head_layer.thickness, '0.5 * t', t=d13d2o.tail_layer.thickness)\n", + "d13d2o.tail_layer.thickness.value = 22\n", + "d13d2o.head_layer.thickness.value, d70d2o.head_layer.thickness.value, d83acmw.head_layer.thickness.value" + ], "metadata": {}, "execution_count": null, "outputs": [] diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index 7b59c331..ce25fbab 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -49,6 +49,10 @@ These are basic fitting examples using the EasyReflectometry library. These are advanced fitting examples using the EasyReflectometry library. - [Multi-Contrast Fitting](advancedfitting/multi_contrast.ipynb) +- [Constraints & Inequalities](advancedfitting/constraints.ipynb) – + Equality constraints, derived read-only parameters (`total_thickness`, + `constrain_to_sum`) and inequality constraints enforced during BUMPS + fits. ## Extra diff --git a/docs/src/api/api.rst b/docs/src/api/api.rst index 55bd611a..94f89a23 100644 --- a/docs/src/api/api.rst +++ b/docs/src/api/api.rst @@ -37,6 +37,15 @@ Fitting helpers and objective functions. fitting +Constraints +=========== +Equality constraints, derived parameters and inequality constraints. + +.. toctree:: + :maxdepth: 1 + + constraints + Assemblies ========== Assemblies are collections of layers that are used to represent a specific physical setup. diff --git a/docs/src/api/constraints.rst b/docs/src/api/constraints.rst new file mode 100644 index 00000000..14f7373f --- /dev/null +++ b/docs/src/api/constraints.rst @@ -0,0 +1,76 @@ +Constraints +=========== + +EasyReflectometry offers three kinds of constraints between model parameters. + +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, constrain_equal, 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 dependency expression and the +ids of the parameters it refers to are stored with every parameter and the +graph is rebuilt when the project is loaded. + +Derived (read-only) parameters +------------------------------ + +A *derived parameter* is a dependent parameter that belongs to no layer: a +live calculation that can be shown, referenced from other constraints and used +in inequalities — the counterpart of a bumps ``Calculation`` slot:: + + from easyreflectometry import derived_parameter, constrain_to_sum + + 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) + +Every :class:`~easyreflectometry.model.Model` exposes +:attr:`~easyreflectometry.model.Model.total_thickness`: the summed thickness of +the layers between the superphase and the subphase, re-derived whenever the +layer structure changes. + +Inequality constraints (fit penalties) +-------------------------------------- + +Cross-parameter inequalities such as ``t_head < t_tail`` or +``t1 + t2 <= total`` are not dependencies: no parameter is removed from the +fit. They are declared as :class:`~easyreflectometry.inequality_constraints.InequalitySpec` +objects on the project and enforced by the **BUMPS** engines (``Bumps*`` +minimizers and the DREAM sampler) as penalties on the fit problem. LMFit and +DFO-LS cannot enforce them; ``fit`` raises ``ValueError`` in that case:: + + from easyreflectometry import InequalitySpec + from easyscience.fitting import AvailableMinimizers + + project.minimizer = AvailableMinimizers.Bumps + t_a = project.parameter_path(layer_a.thickness) # 'models/0/sample/1/layers/0/thickness' + t_b = project.parameter_path(layer_b.thickness) + project.add_inequality_constraint(InequalitySpec('a', '<', 'b', {'a': t_a}, {'b': t_b}, name='order')) + project.add_inequality_constraint(InequalitySpec('a + b', '<', '90', {'a': t_a, 'b': t_b}, {})) + + project.violated_inequality_constraints() # check the start point first + project.fitter.fit_single_data_set_1d(dataset) # penalties applied automatically + +Parameters are referenced by *structural path* (see +:meth:`~easyreflectometry.Project.parameter_path`) so the constraints are +saved with the project. While a constraint is violated BUMPS skips the model +evaluation and adds a penalty growing with the violation, steering the +optimizer back into the feasible region; the ``Bumps_lm`` method spreads the +penalty over the residuals instead and enforces inequalities more weakly. + +API reference +------------- + +.. automodule:: easyreflectometry.constraints + :members: + +.. automodule:: easyreflectometry.inequality_constraints + :members: diff --git a/pixi.lock b/pixi.lock index 16dba96f..6d45e7c0 100644 --- a/pixi.lock +++ b/pixi.lock @@ -203,6 +203,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -322,7 +323,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl @@ -514,6 +514,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -637,7 +638,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-py3-none-any.whl @@ -815,6 +815,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -940,7 +941,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-py3-none-any.whl @@ -1138,6 +1138,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/06/8a/5e156e31ba656ce93c1cc895dd8f051ec351cb382940dca655aaec475005/python_bidi-0.6.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -1258,7 +1259,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl @@ -1443,6 +1443,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl @@ -1565,7 +1566,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-py3-none-any.whl @@ -1738,6 +1738,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -1861,7 +1862,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/0d/8882a4c7a5ebe59a46b709e82411d9c730d67250d41a2e11bc4bcd4d431d/scipp-26.3.1-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl @@ -2061,6 +2061,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2180,7 +2181,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl @@ -2372,6 +2372,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2495,7 +2496,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-py3-none-any.whl @@ -2673,6 +2673,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: ./ + - pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2798,7 +2799,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-py3-none-any.whl @@ -8314,7 +8314,7 @@ packages: name: easyreflectometry requires_dist: - bumps - - easyscience + - easyscience @ git+https://github.com/easyscience/easyscience.git@constraints_factory - orsopy - plotly - pooch @@ -8358,6 +8358,55 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' +- pypi: git+https://github.com/easyscience/easyscience.git?rev=constraints_factory#ee0072619a874ca705aa609ed595cf0aaaca80c3 + name: easyscience + version: 2.5.1+dev8 + requires_dist: + - asteval + - bumps>=1.0.4 + - dfo-ls + - lmfit + - numpy + - scipp + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' + - gitpython ; extra == 'dev' + - interrogate ; extra == 'dev' + - ipykernel ; extra == 'dev' + - ipympl ; extra == 'dev' + - ipython ; extra == 'dev' + - ipywidgets ; extra == 'dev' + - jinja2 ; extra == 'dev' + - jupyterlab ; extra == 'dev' + - jupyterquiz ; extra == 'dev' + - jupytext ; extra == 'dev' + - matplotlib ; extra == 'dev' + - mike ; extra == 'dev' + - mkdocs ; extra == 'dev' + - mkdocs-autorefs ; extra == 'dev' + - mkdocs-jupyter ; extra == 'dev' + - mkdocs-markdownextradata-plugin ; extra == 'dev' + - mkdocs-material ; extra == 'dev' + - mkdocs-plugin-inline-svg ; extra == 'dev' + - mkdocstrings-python ; extra == 'dev' + - nbmake ; extra == 'dev' + - nbqa ; extra == 'dev' + - nbstripout ; extra == 'dev' + - pooch ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyyaml ; extra == 'dev' + - radon ; extra == 'dev' + - ruff ; extra == 'dev' + - spdx-headers ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl name: backrefs version: '7.0' @@ -13137,56 +13186,6 @@ packages: - nodejs ; extra == 'all' - pythreejs ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl - name: easyscience - version: 2.5.1 - sha256: e44c3efb10f8e040ba88523c6ba58e131e3174721f24801d036fdc5a24e622f2 - requires_dist: - - asteval - - bumps>=1.0.4 - - dfo-ls - - lmfit - - numpy - - scipp - - build ; extra == 'dev' - - copier ; extra == 'dev' - - docstripy ; extra == 'dev' - - format-docstring ; extra == 'dev' - - gitpython ; extra == 'dev' - - interrogate ; extra == 'dev' - - ipykernel ; extra == 'dev' - - ipympl ; extra == 'dev' - - ipython ; extra == 'dev' - - ipywidgets ; extra == 'dev' - - jinja2 ; extra == 'dev' - - jupyterlab ; extra == 'dev' - - jupyterquiz ; extra == 'dev' - - jupytext ; extra == 'dev' - - matplotlib ; extra == 'dev' - - mike ; extra == 'dev' - - mkdocs ; extra == 'dev' - - mkdocs-autorefs ; extra == 'dev' - - mkdocs-jupyter ; extra == 'dev' - - mkdocs-markdownextradata-plugin ; extra == 'dev' - - mkdocs-material ; extra == 'dev' - - mkdocs-plugin-inline-svg ; extra == 'dev' - - mkdocstrings-python ; extra == 'dev' - - nbmake ; extra == 'dev' - - nbqa ; extra == 'dev' - - nbstripout ; extra == 'dev' - - pooch ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pydoclint ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - pyyaml ; extra == 'dev' - - radon ; extra == 'dev' - - ruff ; extra == 'dev' - - spdx-headers ; extra == 'dev' - - validate-pyproject[all] ; extra == 'dev' - - versioningit ; extra == 'dev' - requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl name: cycler version: 0.12.1 diff --git a/pyproject.toml b/pyproject.toml index 9f90457f..0a2fd105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,8 @@ classifiers = [ ] requires-python = '>=3.11' dependencies = [ - 'easyscience', + 'easyscience @ git+https://github.com/easyscience/easyscience.git@constraints_factory', + # 'easyscience', 'scipp', 'refnx', 'refl1d>=1.0.0', diff --git a/src/easyreflectometry/__init__.py b/src/easyreflectometry/__init__.py index 72ebcf74..e27bd9cf 100644 --- a/src/easyreflectometry/__init__.py +++ b/src/easyreflectometry/__init__.py @@ -8,7 +8,10 @@ from .analysis.bayesian import PosteriorResults from .constraints import constrain from .constraints import constrain_equal +from .constraints import constrain_to_sum +from .constraints import derived_parameter from .constraints import unconstrain +from .inequality_constraints import InequalitySpec from .project import Project try: @@ -17,10 +20,13 @@ __version__ = '0.0.0' __all__ = [ + 'InequalitySpec', 'Project', 'PosteriorResults', '__version__', 'constrain', 'constrain_equal', + 'constrain_to_sum', + 'derived_parameter', 'unconstrain', ] diff --git a/src/easyreflectometry/constraints.py b/src/easyreflectometry/constraints.py index bb575b81..9a439d86 100644 --- a/src/easyreflectometry/constraints.py +++ b/src/easyreflectometry/constraints.py @@ -9,16 +9,32 @@ by EasyScience; constrained parameters are excluded from the free fit parameters. -Note: custom constraints are not yet preserved when a project is saved -and reloaded. The nested-parameter serialization in EasyScience drops -the dependency information. Re-apply constraints after loading a -project. +Constraints are preserved when a project is saved and reloaded: the +dependency expression and the ids of the parameters it refers to are +serialized with each parameter, and ``Project.from_dict`` re-establishes +the dependency graph once every parameter exists again. + +Cross-parameter *inequalities* (``t_head < t_tail``) are not dependencies +but fit penalties; see :mod:`easyreflectometry.inequality_constraints`. """ +from __future__ import annotations + +import numbers +from typing import Iterable +from typing import Optional +from typing import Union + from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter -__all__ = ['constrain', 'constrain_equal', 'unconstrain'] +__all__ = [ + 'constrain', + 'constrain_equal', + 'constrain_to_sum', + 'derived_parameter', + 'unconstrain', +] def constrain_equal(parameter: Parameter, to: DescriptorNumber) -> None: @@ -85,3 +101,100 @@ def unconstrain(parameter: Parameter) -> None: """ if not parameter.independent: parameter.make_independent() + + +def derived_parameter( + name: str, + expression: str, + unit: Optional[str] = None, + **parameters: DescriptorNumber, +) -> Parameter: + """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. It can be used anywhere a parameter + can — for example on either side of an inequality constraint or inside + another :func:`constrain` expression — which makes it the + EasyReflectometry counterpart of a bumps ``Calculation`` slot. + + .. 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) + + Parameters + ---------- + name : str + Display name of the new parameter. + expression : str + Mathematical expression over the placeholder names in `parameters`. + unit : Optional[str], optional + Unit the result is converted to. By default the unit produced by + the expression is kept. + parameters : DescriptorNumber + Placeholder-name to parameter mapping for `expression`. + + Returns + ------- + Parameter + A new dependent parameter. + """ + if not parameters: + raise ValueError('derived_parameter needs at least one parameter to depend on.') + return Parameter.from_dependency( + name=name, + dependency_expression=expression, + dependency_map=dict(parameters), + desired_unit=unit, + ) + + +def constrain_to_sum( + parameter: Parameter, + of_parameters: Iterable[DescriptorNumber], + *, + total: Union[DescriptorNumber, numbers.Number, None] = None, +) -> 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 + ---------- + parameter : Parameter + Parameter to make dependent (it absorbs the remainder). + of_parameters : Iterable[DescriptorNumber] + The parameters whose sum is constrained. + total : Union[DescriptorNumber, numbers.Number, None], optional + The target sum: a parameter/descriptor (e.g. a + :func:`derived_parameter`) or a plain number in `parameter`'s + unit. By default the current sum of `of_parameters` is frozen as + a constant. + """ + others = [p for p in of_parameters if p is not parameter] + if not others and total is None: + raise ValueError('constrain_to_sum needs at least one other parameter or an explicit total.') + if total is None: + total = float(parameter.value) + sum(float(p.value) for p in others) + if isinstance(total, numbers.Number): + total = DescriptorNumber(name=f'{parameter.name}_sum_total', value=float(total), unit=str(parameter.unit)) + elif not isinstance(total, DescriptorNumber): + raise TypeError('total must be a number, a DescriptorNumber/Parameter or None.') + + dependency_map = {'total': total} + terms = [] + for index, other in enumerate(others): + alias = f'p{index}' + dependency_map[alias] = other + terms.append(alias) + expression = 'total' if not terms else f'total - ({" + ".join(terms)})' + parameter.make_dependent_on(dependency_expression=expression, dependency_map=dependency_map) diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index 37ba637e..581200f3 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -278,6 +278,24 @@ def __init__(self, *args: Model, objective: str = 'hybrid'): # to, and the spin channel each one is evaluated on (None = unpolarized). self.fit_datasets: list[DataSet1D] = [] self.fit_channels: list[Any] = [] + # Optional zero-argument callable returning the ``constraints_factory`` + # for the next fit (or ``None``). ``Project.fitter`` binds it to + # ``Project.build_constraints_factory`` so inequality constraints + # registered on the project are applied without passing them + # explicitly; an explicit ``constraints_factory=`` argument wins. + self.constraints_factory_provider: Callable[[], Callable | None] | None = None + + def _resolve_constraints_factory(self, explicit: Callable | None) -> Callable | None: + if explicit is not None: + return explicit + if self.constraints_factory_provider is not None: + return self.constraints_factory_provider() + return None + + def _constraints_kwargs(self, explicit: Callable | None) -> dict: + """``{'constraints_factory': ...}`` when one applies, else ``{}`` (keeps the plain call shape).""" + factory = self._resolve_constraints_factory(explicit) + return {'constraints_factory': factory} if factory is not None else {} @classmethod def for_experiments( @@ -362,7 +380,13 @@ def for_experiments( fitter.fit_channels = channels return fitter - def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> sc.DataGroup: + def fit( + self, + data: sc.DataGroup, + id: int = 0, + objective: str | None = None, + constraints_factory: Callable | None = None, + ) -> sc.DataGroup: """Perform the fitting and populate the DataGroups with the result. Parameters @@ -374,6 +398,10 @@ def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> objective : str | None, optional Per-call override for the zero-variance objective. If ``None``, uses the instance default set at construction. By default, None. + constraints_factory : Callable | None, optional + Inequality constraints to enforce (BUMPS engines only); see + :mod:`easyreflectometry.inequality_constraints`. Defaults to what + :attr:`constraints_factory_provider` returns. By default, None. Returns ------- @@ -402,7 +430,7 @@ def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> dy.append(weights) original_arrays.append({'x': x_vals, 'y': y_vals, 'variances': variances}) - result = self.easy_science_multi_fitter.fit(x, y, weights=dy) + result = self.easy_science_multi_fitter.fit(x, y, weights=dy, **self._constraints_kwargs(constraints_factory)) self._fit_results = result self._classical_fit_metrics = [] new_data = data.copy() @@ -430,7 +458,12 @@ def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> new_data['success'] = result[i].success return new_data - def fit_single_data_set_1d(self, data: DataSet1D, objective: str | None = None) -> FitResults: + def fit_single_data_set_1d( + self, + data: DataSet1D, + objective: str | None = None, + constraints_factory: Callable | None = None, + ) -> FitResults: """Perform fitting on a single 1D dataset. Parameters @@ -441,6 +474,9 @@ def fit_single_data_set_1d(self, data: DataSet1D, objective: str | None = None) objective : str | None, optional Per-call override for the zero-variance objective. If ``None``, uses the instance default set at construction. By default, None. + constraints_factory : Callable | None, optional + Inequality constraints to enforce (BUMPS engines only). Defaults + to what :attr:`constraints_factory_provider` returns. By default, None. Returns ------- @@ -459,7 +495,9 @@ def fit_single_data_set_1d(self, data: DataSet1D, objective: str | None = None) if obj == 'legacy_mask' and len(x_out) == 0: raise ValueError('Cannot fit single dataset: all points have zero variance.') - result = self.easy_science_multi_fitter.fit(x=[x_out], y=[y_eff], weights=[weights])[0] + result = self.easy_science_multi_fitter.fit( + x=[x_out], y=[y_eff], weights=[weights], **self._constraints_kwargs(constraints_factory) + )[0] self._fit_results = [result] model_curve = self._fit_func[0](x_vals) self._classical_fit_metrics = [ @@ -467,7 +505,12 @@ def fit_single_data_set_1d(self, data: DataSet1D, objective: str | None = None) ] return result - def fit_polarized(self, data: PolarizedDataSet, objective: str | None = None) -> dict[str, FitResults]: + def fit_polarized( + self, + data: PolarizedDataSet, + objective: str | None = None, + constraints_factory: Callable | None = None, + ) -> dict[str, FitResults]: """Fit all measured spin channels of a polarized experiment simultaneously. Each channel dataset gets its own fit function evaluating the @@ -545,7 +588,7 @@ def fit_polarized(self, data: PolarizedDataSet, objective: str | None = None) -> dy.append(weights) original_arrays.append({'x': x_vals, 'y': y_vals, 'variances': variances}) - results = polarized_fitter.fit(x, y, weights=dy) + results = polarized_fitter.fit(x, y, weights=dy, **self._constraints_kwargs(constraints_factory)) # All channels are fitted against one parameter vector (the shared model), # so `result.n_pars` is identical across `results`; `reduced_chi` and # `classical_reduced_chi` below rely on that invariant. @@ -570,6 +613,7 @@ def mcmc_sample( initializer: str | None = None, progress_callback: Callable[..., Any] | None = None, abort_test: Callable[[], bool] | None = None, + constraints_factory: Callable | None = None, ) -> dict: """Run Bayesian MCMC sampling on reflectometry data using the DREAM sampler. @@ -587,6 +631,11 @@ def mcmc_sample( uses ``'eps'``). :param progress_callback: Optional callback for progress updates during sampling. Forwarded to the core MultiFitter. + :param abort_test: Optional callable returning ``True`` to abort sampling. + :param constraints_factory: Inequality constraints to enforce (see + :mod:`easyreflectometry.inequality_constraints`); the posterior is + penalised in the infeasible region. Defaults to what + :attr:`constraints_factory_provider` returns. :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, and ``'logp'``. :raises RuntimeError: If the current minimizer is not a BUMPS instance. @@ -655,6 +704,8 @@ def mcmc_sample( x=x, y=y, weights=dy, + # Bound to the sampler so ``extend()`` keeps the same penalised posterior. + constraints_factory=self._resolve_constraints_factory(constraints_factory), ) # Retained so the chain can be continued afterwards via ``self.sampler.extend()``. self._sampler = sampler diff --git a/src/easyreflectometry/inequality_constraints.py b/src/easyreflectometry/inequality_constraints.py new file mode 100644 index 00000000..70475f1c --- /dev/null +++ b/src/easyreflectometry/inequality_constraints.py @@ -0,0 +1,435 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Cross-parameter inequality constraints enforced by the fit. + +An :class:`InequalitySpec` is a purely declarative statement such as +``t_head < t_tail`` or ``t1 + t2 <= total``: two expressions over parameter +aliases and a relation. Unlike the equality constraints in +:mod:`easyreflectometry.constraints` it is *not* a parameter dependency — +no parameter is removed from the fit. Instead the specs are translated, +at the start of every fit, into penalty terms attached to the BUMPS +``FitProblem`` (``FitProblem(constraints=[...])``): while a constraint is +violated BUMPS skips the model, adds a large penalty and a term growing +with the violation, which steers the optimizer back into the feasible +region. Only the BUMPS engine family (``Bumps*`` minimizers and the DREAM +sampler) supports this; ``Fitter.fit`` rejects inequalities for LMFit and +DFO-LS. + +Specs reference parameters by *structural path* (``models/0/sample/1/layers/0/thickness``, +see :meth:`easyreflectometry.Project.parameter_path`) rather than by +object or unique name, so they survive project save/load and can be +rebuilt against a reloaded object tree. + +Design notes — why the translation happens inside the fit, and reads the +BUMPS parameters: + +* BUMPS evaluates the constraints *before* the model and skips the model + while any fails. The EasyScience parameter values are only written inside + the model call, so at constraint-evaluation time they lag the optimizer's + trial vector — and freeze entirely once a constraint fails. The operands + built here therefore read ``bumps.Parameter.value`` of the live problem, + which BUMPS sets for every trial point. +* Those BUMPS parameters only exist for the *free* EasyScience parameters + and are rebuilt per fit, which is why a *factory* (``constraints_factory``) + is passed down the fitting chain and invoked by easyscience's + ``build_curve_problem``. Fixed parameters are frozen as constants; + dependent (constrained or derived) parameters are expanded recursively + into their independent leaves. +* Each penalty term returns the *linear* violation; BUMPS squares it once, + giving a quadratic penalty. +""" + +from __future__ import annotations + +import keyword +import numbers +import re +from dataclasses import dataclass +from dataclasses import field +from typing import Any +from typing import Callable +from typing import Dict +from typing import Iterable +from typing import List +from typing import Optional + +import numpy as np +from asteval import Interpreter +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter + +__all__ = [ + 'RELATIONS', + 'InequalityEvaluation', + 'InequalitySpec', + 'build_constraints_factory', + 'check_units', + 'evaluate_spec', +] + +RELATIONS = ('<', '<=', '>', '>=') +_RELATION_ALIASES = {'≤': '<=', '≥': '>=', '=<': '<=', '=>': '>='} + +#: BUMPS prefixes every EasyScience parameter name; must match +#: ``easyscience.fitting.engine_base.PARAMETER_PREFIX``. +_BUMPS_PREFIX = 'p' + +_SAFE_SYMBOLS = { + 'pi': np.pi, + 'e': np.e, + 'sqrt': np.sqrt, + 'exp': np.exp, + 'log': np.log, + 'log10': np.log10, + 'sin': np.sin, + 'cos': np.cos, + 'tan': np.tan, + 'abs': abs, + 'min': min, + 'max': max, +} + +PathResolver = Callable[[str], DescriptorNumber] + + +def _normalize_relation(op: str) -> str: + op = _RELATION_ALIASES.get(op.strip(), op.strip()) + if op not in RELATIONS: + raise ValueError(f"Unsupported relation '{op}'. Use one of {', '.join(RELATIONS)}.") + return op + + +def _new_interpreter() -> Interpreter: + interpreter = Interpreter(minimal=True, use_numpy=False) + for name, value in _SAFE_SYMBOLS.items(): + interpreter.symtable[name] = value + return interpreter + + +def _evaluate(interpreter: Interpreter, expression: str, symbols: Dict[str, Any]) -> Any: + interpreter.symtable.update(symbols) + result = interpreter.eval(expression, raise_errors=True) + return result + + +def _identifiers(expression: str) -> List[str]: + """Identifiers used in `expression` that are not builtin math symbols.""" + names = set(re.findall(r'\b[A-Za-z_][A-Za-z0-9_]*\b', expression)) + return sorted(n for n in names if n not in _SAFE_SYMBOLS and not keyword.iskeyword(n)) + + +@dataclass +class InequalitySpec: + """Declarative cross-parameter inequality, e.g. ``t_head < t_tail``. + + Attributes + ---------- + lhs_expression : str + Expression over the aliases in `lhs_paths`, e.g. ``'a + b'``. + op : str + One of ``'<'``, ``'<='``, ``'>'``, ``'>='``. + rhs_expression : str + Expression over the aliases in `rhs_paths`; may also be a plain + number (dimensionless or understood in the unit of the left side). + lhs_paths, rhs_paths : dict[str, str] + Alias to structural parameter path (``models/0/...``); no live objects + are held so the spec is trivially serializable. + name : str + Optional user label. + enabled : bool + Disabled specs are kept but not applied to fits. + """ + + lhs_expression: str + op: str + rhs_expression: str + lhs_paths: Dict[str, str] = field(default_factory=dict) + rhs_paths: Dict[str, str] = field(default_factory=dict) + name: str = '' + enabled: bool = True + + def __post_init__(self) -> None: + self.op = _normalize_relation(self.op) + self.lhs_expression = str(self.lhs_expression).strip() + self.rhs_expression = str(self.rhs_expression).strip() + self.lhs_paths = dict(self.lhs_paths) + self.rhs_paths = dict(self.rhs_paths) + self.validate_syntax() + + # ----- validation ----- + + def validate_syntax(self) -> None: + """Check that both sides parse and every identifier has an alias mapping.""" + for side, expression, paths in ( + ('left', self.lhs_expression, self.lhs_paths), + ('right', self.rhs_expression, self.rhs_paths), + ): + if not expression: + raise ValueError(f'The {side}-hand side of an inequality cannot be empty.') + for alias in paths: + if not alias.isidentifier() or keyword.iskeyword(alias): + raise ValueError(f"Alias '{alias}' is not a valid identifier.") + missing = [n for n in _identifiers(expression) if n not in paths] + if missing: + raise ValueError(f"The {side}-hand side '{expression}' references unmapped names: {', '.join(missing)}.") + interpreter = _new_interpreter() + try: + _evaluate(interpreter, expression, {alias: 1.0 for alias in paths}) + except Exception as error: # asteval raises its own hierarchy + raise SyntaxError(f"Cannot evaluate the {side}-hand side '{expression}': {error}") from None + + @property + def paths(self) -> Dict[str, str]: + """All alias → path pairs of both sides.""" + merged = dict(self.lhs_paths) + merged.update(self.rhs_paths) + return merged + + # ----- serialization ----- + + def to_dict(self) -> dict: + return { + 'lhs_expression': self.lhs_expression, + 'op': self.op, + 'rhs_expression': self.rhs_expression, + 'lhs_paths': dict(self.lhs_paths), + 'rhs_paths': dict(self.rhs_paths), + 'name': self.name, + 'enabled': bool(self.enabled), + } + + @classmethod + def from_dict(cls, d: dict) -> 'InequalitySpec': + return cls( + lhs_expression=d['lhs_expression'], + op=d['op'], + rhs_expression=d['rhs_expression'], + lhs_paths=d.get('lhs_paths', {}), + rhs_paths=d.get('rhs_paths', {}), + name=d.get('name', ''), + enabled=d.get('enabled', True), + ) + + def __str__(self) -> str: + return f'{self.lhs_expression} {self.op} {self.rhs_expression}' + + +@dataclass +class InequalityEvaluation: + """Result of evaluating a spec against the current parameter values.""" + + lhs: float + rhs: float + satisfied: bool + violation: float + + @property + def feasible(self) -> bool: + return self.satisfied + + +# ----- value sources reading the live BUMPS trial vector ----- + + +class _Constant: + __slots__ = ('value',) + + def __init__(self, value: float) -> None: + self.value = float(value) + + def __call__(self) -> float: + return self.value + + +class _BumpsValue: + """Reads a BUMPS parameter — i.e. the optimizer's current trial value.""" + + __slots__ = ('parameter',) + + def __init__(self, bumps_parameter: Any) -> None: + self.parameter = bumps_parameter + + def __call__(self) -> float: + return float(self.parameter.value) + + +class _Expression: + """Evaluates an expression whose symbols are value sources.""" + + __slots__ = ('expression', 'sources', '_interpreter') + + def __init__(self, expression: str, sources: Dict[str, Callable[[], float]]) -> None: + self.expression = expression + self.sources = sources + self._interpreter = _new_interpreter() + + def __call__(self) -> float: + return float(_evaluate(self._interpreter, self.expression, {k: src() for k, src in self.sources.items()})) + + def __float__(self) -> float: + return self() + + +class _Violation: + """The BUMPS constraint object: ``float()`` is ``0`` when satisfied, else the violation.""" + + __slots__ = ('lhs', 'op', 'rhs', 'label') + + def __init__(self, lhs: _Expression, op: str, rhs: _Expression, label: str) -> None: + self.lhs, self.op, self.rhs, self.label = lhs, op, rhs, label + + def __float__(self) -> float: + return _violation(self.lhs(), self.op, self.rhs()) + + def __str__(self) -> str: + return self.label + + def __bool__(self) -> bool: # mirror bumps.Constraint: never silently truthy + raise TypeError('Inequality constraints cannot be used as booleans') + + +def _violation(lhs: float, op: str, rhs: float) -> float: + """Linear violation of ``lhs op rhs`` (``0`` when satisfied). + + Strict and non-strict relations are treated alike: a penalty of exactly + zero at the boundary is the only continuous choice. + """ + if op in ('<', '<='): + diff = lhs - rhs + else: + diff = rhs - lhs + return diff if diff > 0.0 else 0.0 + + +def _source_for(parameter: DescriptorNumber, bumps_pars: Dict[str, Any], trail: tuple) -> Callable[[], float]: + """Translate an EasyScience parameter into a source reading the BUMPS trial vector. + + * free parameter → the BUMPS parameter of the problem; + * fixed parameter (or plain descriptor) → constant; + * dependent parameter → its dependency expression, expanded recursively. + """ + if id(parameter) in trail: + raise ValueError(f"Circular dependency while expanding parameter '{parameter.name}'.") + if isinstance(parameter, Parameter) and not parameter.independent: + expression = getattr(parameter, '_clean_dependency_string', None) + dependency_map = getattr(parameter, '_dependency_map', None) or {} + if expression is None: + return _Constant(parameter.value) + sources = {alias: _source_for(dep, bumps_pars, trail + (id(parameter),)) for alias, dep in dependency_map.items()} + return _Expression(expression, sources) + key = _BUMPS_PREFIX + parameter.unique_name + if key in bumps_pars: + return _BumpsValue(bumps_pars[key]) + # Fixed, or not part of this fit (e.g. belongs to a model not being fitted). + return _Constant(parameter.value) + + +def _resolve_all(spec: InequalitySpec, resolve: PathResolver) -> Dict[str, DescriptorNumber]: + resolved = {} + for alias, path in spec.paths.items(): + parameter = resolve(path) + if not isinstance(parameter, DescriptorNumber): + raise ValueError(f"Path '{path}' (alias '{alias}') does not point to a parameter.") + resolved[alias] = parameter + return resolved + + +def build_constraints_factory(specs: Iterable[InequalitySpec], resolve: PathResolver) -> Optional[Callable]: + """Build the ``constraints_factory`` hook for the given specs. + + The returned callable is what ``easyscience``'s BUMPS engine invokes + with the ``{prefixed unique name: bumps.Parameter}`` mapping of a + freshly built problem; it returns one penalty object per enabled spec. + Paths are resolved and dependent parameters expanded at that moment, so + the factory always reflects the model as it is when the fit starts. + + Parameters + ---------- + specs : Iterable[InequalitySpec] + Specs to apply; disabled ones are skipped. + resolve : Callable[[str], DescriptorNumber] + Structural-path resolver, typically ``project.resolve_parameter_path``. + + Returns + ------- + Callable | None + The factory, or ``None`` when no spec is enabled (so callers can pass + it straight through as ``constraints_factory=...``). + """ + active = [spec for spec in specs if spec.enabled] + if not active: + return None + + def factory(bumps_pars: Dict[str, Any]) -> list: + constraints = [] + for spec in active: + parameters = _resolve_all(spec, resolve) + lhs = _Expression( + spec.lhs_expression, + {alias: _source_for(parameters[alias], bumps_pars, ()) for alias in spec.lhs_paths}, + ) + rhs = _Expression( + spec.rhs_expression, + {alias: _source_for(parameters[alias], bumps_pars, ()) for alias in spec.rhs_paths}, + ) + constraints.append(_Violation(lhs, spec.op, rhs, spec.name or str(spec))) + return constraints + + return factory + + +def evaluate_spec(spec: InequalitySpec, resolve: PathResolver) -> InequalityEvaluation: + """Evaluate a spec against the *current* parameter values. + + Used for the start-point feasibility check before a fit is launched and + for displaying the constraint state; dependent parameters contribute + their current (already propagated) value. + """ + parameters = _resolve_all(spec, resolve) + interpreter = _new_interpreter() + lhs = float(_evaluate(interpreter, spec.lhs_expression, {a: float(parameters[a].value) for a in spec.lhs_paths})) + rhs = float(_evaluate(interpreter, spec.rhs_expression, {a: float(parameters[a].value) for a in spec.rhs_paths})) + violation = _violation(lhs, spec.op, rhs) + return InequalityEvaluation(lhs=lhs, rhs=rhs, satisfied=violation == 0.0, violation=violation) + + +def check_units(spec: InequalitySpec, resolve: PathResolver) -> None: + """Raise ``ValueError`` when the two sides of `spec` have incompatible units. + + Each side is evaluated with the unit-carrying ``DescriptorNumber`` + objects themselves (the same arithmetic the equality constraints use), + so ``t1 + sld`` is rejected by EasyScience and ``t_head < t_tail`` + passes. A plain numeric side is accepted against any unit: it is read in + the unit of the other side. The same applies to a side mixing literals + with parameters (``90 - b``): it is checked numerically and its literals + are read in the unit of the other side. + """ + parameters = _resolve_all(spec, resolve) + units = [] + for expression, paths in ((spec.lhs_expression, spec.lhs_paths), (spec.rhs_expression, spec.rhs_paths)): + interpreter = _new_interpreter() + try: + result = _evaluate(interpreter, expression, {alias: parameters[alias] for alias in paths}) + except Exception as unit_error: + # Mixed literal/parameter arithmetic such as ``90 - b`` cannot be + # evaluated with unit-carrying objects (a bare number has no unit). + # Fall back to a numeric evaluation and read the literals in the + # unit of the other side; purely wrong mixes still fail here. + try: + numeric = _evaluate(_new_interpreter(), expression, {alias: float(parameters[alias].value) for alias in paths}) + except Exception: + raise ValueError(f"Cannot evaluate '{expression}' with units: {unit_error}") from None + if not isinstance(numeric, numbers.Number): + raise ValueError(f"'{expression}' does not evaluate to a number.") from None + units.append(None) + continue + if isinstance(result, DescriptorNumber): + units.append(str(result.unit)) + elif isinstance(result, numbers.Number): + units.append(None) + else: + raise ValueError(f"'{expression}' does not evaluate to a number.") + lhs_unit, rhs_unit = units + if lhs_unit is not None and rhs_unit is not None and lhs_unit != rhs_unit: + raise ValueError(f"Incompatible units in '{spec}': left side is in '{lhs_unit}', right side in '{rhs_unit}'.") diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index 993dc3e8..da5d8d9d 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -115,6 +115,20 @@ def __init__( self._scale = scale self._background = background + # Derived, read-only "calculation" parameter (see `total_thickness`). + # Rebuilt by every constructor call — including `from_dict` — and + # deliberately *not* serialized: it is a function of the layers. + self._total_thickness = Parameter( + name='total_thickness', + value=0.0, + unit='angstrom', + fixed=True, + description='Total thickness of the film: the sum of all layer thicknesses ' + 'between the superphase and the subphase (derived, read-only).', + ) + self._total_thickness_sources: list[Parameter] = [] + self.refresh_derived() + # Set interface last — propagates to children via BaseCore.generate_bindings # and then sets the resolution function on the calculator (see setter). if interface is not None: @@ -146,6 +160,52 @@ def background(self) -> Parameter: def background(self, value: float) -> None: self._background.value = value + # ----- derived parameters ----- + + def _film_layers(self) -> list: + """Layers between the superphase and the subphase (first and last layer of the sample).""" + layers = [layer for assembly in self.sample for layer in assembly.layers] + return layers[1:-1] if len(layers) >= 3 else [] + + def refresh_derived(self) -> None: + """Re-derive the model's computed parameters from the current layer structure. + + Cheap (a few string operations) and idempotent: the dependency is + only rebuilt when the set of contributing layers changed. Called on + every access to :attr:`total_thickness`, so layers added or removed + through *any* path (`Model.add_assemblies`, `assembly.layers.append`, + ...) are picked up without explicit notification. + """ + sources = [layer.thickness for layer in self._film_layers()] + current = self._total_thickness_sources + if len(sources) == len(current) and all(a is b for a, b in zip(sources, current)): + return + self._total_thickness_sources = sources + total = self._total_thickness + if not sources: + if not total.independent: + total.make_independent() + total.value = 0.0 + return + dependency_map = {f't{index}': source for index, source in enumerate(sources)} + total.make_dependent_on( + dependency_expression=' + '.join(dependency_map.keys()), + dependency_map=dependency_map, + ) + + @property + def total_thickness(self) -> Parameter: + """Read-only parameter: the summed thickness of the film layers. + + The superphase (first layer of the sample) and the subphase (last + layer) are semi-infinite and excluded. The parameter is *dependent*: + it tracks the layer thicknesses, never enters a fit and cannot be + set; it can be referenced from constraint expressions and + inequality constraints like any other parameter. + """ + self.refresh_derived() + return self._total_thickness + # ----- assembly management ----- def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None: diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index 817fc5f0..cb1acce1 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -63,13 +63,26 @@ def next_color_index(self) -> Optional[int]: """Index of the next colour to assign — kept around so it round-trips.""" return self._next_color_index + def next_color(self) -> str: + """Colour the next appended model should get. + + Appending advances the cycle, so callers that build a ``Model`` + themselves can keep the per-model colours distinct:: + + model = Model(sample=sample, color=collection.next_color()) + collection.add_model(model) + """ + return self._current_color() + def add_model(self, model: Optional[Model] = None): """Add a model to the collection. Parameters ---------- model : Optional[Model], optional - Model to add. By default, None. + Model to add. By default, None (a new model is created with + the collection's next colour; a supplied model keeps its own + colour — use :meth:`next_color` when building one). """ if model is None: model = Model(name='Model', interface=self.interface, color=self._current_color()) @@ -86,6 +99,9 @@ def duplicate_model(self, index: int): to_be_duplicated = self[index] duplicate = Model.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) duplicate.name = duplicate.name + ' duplicate' + # A duplicate sharing its source's colour would be indistinguishable + # in the plots; give it the collection's next colour instead. + duplicate.color = self._current_color() self.append(duplicate) @classmethod diff --git a/src/easyreflectometry/orso_utils.py b/src/easyreflectometry/orso_utils.py index aa320933..15cb717e 100644 --- a/src/easyreflectometry/orso_utils.py +++ b/src/easyreflectometry/orso_utils.py @@ -87,9 +87,23 @@ def load_orso_model(orso_data) -> Sample: stacklevel=2, ) return None - stack_str = sample_model.stack - layers_dict = sample_model.layers if hasattr(sample_model, 'layers') else None - orso_sample = model_language.SampleModel(stack=stack_str, layers=layers_dict) + if isinstance(sample_model, model_language.SampleModel): + # Use the file's model as parsed: rebuilding it from `stack` and + # `layers` alone (as done previously) silently dropped the + # `materials` / `sub_stacks` / `composits` definitions, so named + # materials could not be resolved and their SLDs read as 0. + orso_sample = sample_model + else: + stack_str = sample_model.stack + layers_dict = sample_model.layers if hasattr(sample_model, 'layers') else None + orso_sample = model_language.SampleModel( + stack=stack_str, + layers=layers_dict, + materials=getattr(sample_model, 'materials', None), + sub_stacks=getattr(sample_model, 'sub_stacks', None), + composits=getattr(sample_model, 'composits', None), + globals=getattr(sample_model, 'globals', None), + ) # Try to resolve layers using different methods try: @@ -147,8 +161,11 @@ def load_orso_model(orso_data) -> Sample: def _convert_orso_layer_to_erl(layer): r"""Helper function to convert an ORSO layer to an EasyReflectometry laye.""" material = layer.material - # Prefer original_name for material name, fall back to formula if available + # Prefer original_name for the material name, fall back to the formula; a + # material defined only by its SLD has neither, so never leave it None. m_name = layer.original_name if layer.original_name is not None else material.formula + if m_name is None: + m_name = 'material' # Get SLD values (use formula for density calculation if available) formula_for_calc = material.formula if material.formula is not None else m_name @@ -187,9 +204,10 @@ def _get_sld_values(material, material_name): if isinstance(material.sld, ComplexValue): raw_sld = material.sld.real m_sld = raw_sld * 1e6 - m_isld = material.sld.imag * 1e6 + m_isld = (material.sld.imag or 0.0) * 1e6 else: - raw_sld = material.sld + # A plain number, or an orsopy ``Value`` (unwrap its magnitude). + raw_sld = getattr(material.sld, 'magnitude', material.sld) m_sld = raw_sld * 1e6 m_isld = 0.0 if raw_sld != 0.0 and abs(raw_sld) > 1e-2: diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 1c7fd29e..04e4b0db 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -5,6 +5,7 @@ import json import logging import os +import weakref from pathlib import Path from typing import Dict from typing import List @@ -14,6 +15,7 @@ import numpy as np from easyscience import global_object from easyscience.fitting import AvailableMinimizers +from easyscience.variable import DescriptorNumber as DescriptorNumberType from easyscience.variable import Parameter from easyscience.variable.parameter_dependency_resolver import resolve_all_parameter_dependencies from scipp import DataGroup @@ -28,6 +30,11 @@ from easyreflectometry.data.measurement import extract_orso_title from easyreflectometry.data.measurement import load_data_from_orso_file from easyreflectometry.fitting import MultiFitter +from easyreflectometry.inequality_constraints import InequalityEvaluation +from easyreflectometry.inequality_constraints import InequalitySpec +from easyreflectometry.inequality_constraints import build_constraints_factory +from easyreflectometry.inequality_constraints import check_units +from easyreflectometry.inequality_constraints import evaluate_spec from easyreflectometry.limits import apply_default_limits from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection @@ -75,6 +82,22 @@ DEFAULT_MINIMIZER = AvailableMinimizers.LMFit_leastsq +#: Properties not descended into when *generating* structural parameter +#: paths: non-structural objects and convenience aliases of ``layers[i]`` +#: (so a layer parameter is always addressed as ``.../layers//...``). +#: ``resolve_parameter_path`` still accepts them. +_PATH_SKIPPED_PROPERTIES = frozenset({'interface', 'parent', 'front_layer', 'back_layer', 'head_layer', 'tail_layer'}) + + +def _weak_constraints_provider(project: 'Project'): + project_ref = weakref.ref(project) + + def provider(): + target = project_ref() + return None if target is None else target.build_constraints_factory() + + return provider + class Project: def __init__(self): @@ -98,6 +121,7 @@ def __init__(self): self._current_layer_index = 0 self._fitter_model_index = None self._current_experiment_index = 0 + self._inequality_constraints: List[InequalitySpec] = [] # Project flags self._created = False @@ -329,8 +353,148 @@ def fitter(self) -> MultiFitter: self._fitter = MultiFitter(self._models[self._current_model_index]) self._fitter.easy_science_multi_fitter.switch_minimizer(self._minimizer_selection) self._fitter_model_index = self._current_model_index + # Fits run through this fitter pick up the project's inequality + # constraints automatically (resolved at fit time). A weak + # reference avoids a project -> fitter -> project cycle that + # would keep a discarded project (and its unique names) alive. + self._fitter.constraints_factory_provider = _weak_constraints_provider(self) return self._fitter + # ----- structural parameter paths ----- + + @staticmethod + def _child_candidates(obj) -> list: + """``(token, child)`` pairs to descend into from `obj`.""" + from collections.abc import Sequence + + from easyreflectometry.sample.base_core import BaseCore + from easyreflectometry.sample.collections.base_collection import BaseCollection + + if isinstance(obj, (BaseCollection, list, tuple)) or (isinstance(obj, Sequence) and not isinstance(obj, str)): + return [(str(index), item) for index, item in enumerate(obj)] + if isinstance(obj, BaseCore) or hasattr(obj, 'get_all_parameters'): + candidates = [] + for attr_name in dir(type(obj)): + if attr_name.startswith('_') or attr_name in _PATH_SKIPPED_PROPERTIES: + continue + class_attr = getattr(type(obj), attr_name, None) + if not isinstance(class_attr, property): + continue + try: + value = getattr(obj, attr_name) + except Exception as exception: + logger.debug("Skipping property '%s' on %r: %s", attr_name, obj, exception) + continue + if isinstance(value, (DescriptorNumberType, BaseCore, BaseCollection, list, tuple)): + candidates.append((attr_name, value)) + return candidates + return [] + + def parameter_path(self, parameter) -> Optional[str]: + """Structural path of `parameter` within this project, e.g. + ``models/0/sample/1/layers/0/thickness``. + + Paths are stable across save/load (unlike unique names, which are + regenerated) and are the way inequality constraints reference + parameters. Returns ``None`` when the parameter is not reachable + from the project's models. + """ + target = id(parameter) + visited: set[int] = set() + + def _search(obj, tokens: List[str]) -> Optional[str]: + if id(obj) == target: + return '/'.join(tokens) + if isinstance(obj, DescriptorNumberType): + return None + if id(obj) in visited: + return None + visited.add(id(obj)) + for token, child in self._child_candidates(obj): + found = _search(child, tokens + [token]) + if found is not None: + return found + return None + + return _search(self._models, ['models']) + + def resolve_parameter_path(self, path: str): + """Return the parameter at a structural `path` (see :meth:`parameter_path`).""" + tokens = [t for t in str(path).split('/') if t != ''] + if not tokens or tokens[0] != 'models': + raise KeyError(f"Parameter path must start with 'models': '{path}'.") + obj = self._models + for token in tokens[1:]: + if token.lstrip('-').isdigit(): + try: + obj = obj[int(token)] + except (IndexError, KeyError, TypeError): + raise KeyError(f"Parameter path '{path}': index {token} is out of range.") from None + else: + if token.startswith('_') or not hasattr(obj, token): + raise KeyError(f"Parameter path '{path}': unknown attribute '{token}'.") + obj = getattr(obj, token) + if not isinstance(obj, DescriptorNumberType): + raise KeyError(f"Parameter path '{path}' does not point to a parameter.") + return obj + + # ----- inequality constraints ----- + + @property + def inequality_constraints(self) -> List[InequalitySpec]: + """The project's inequality constraints (a copy of the list).""" + return list(self._inequality_constraints) + + def add_inequality_constraint(self, spec: InequalitySpec, validate: bool = True) -> InequalitySpec: + """Register an inequality constraint; returns it. + + With ``validate`` the paths are resolved and the units of both sides + compared, raising ``KeyError``/``ValueError`` on problems. + """ + if not isinstance(spec, InequalitySpec): + raise TypeError('spec must be an InequalitySpec') + if validate: + check_units(spec, self.resolve_parameter_path) + self._inequality_constraints.append(spec) + return spec + + def remove_inequality_constraint(self, which: Union[int, str, InequalitySpec]) -> None: + """Remove a constraint by index, by name or by identity.""" + if isinstance(which, InequalitySpec): + self._inequality_constraints = [s for s in self._inequality_constraints if s is not which] + return + if isinstance(which, str): + matches = [s for s in self._inequality_constraints if s.name == which] + if not matches: + raise KeyError(f"No inequality constraint named '{which}'.") + for spec in matches: + self._inequality_constraints.remove(spec) + return + del self._inequality_constraints[int(which)] + + def clear_inequality_constraints(self) -> None: + self._inequality_constraints = [] + + def evaluate_inequality_constraints(self) -> List[InequalityEvaluation]: + """Evaluate every constraint (enabled or not) at the current values.""" + return [evaluate_spec(spec, self.resolve_parameter_path) for spec in self._inequality_constraints] + + def violated_inequality_constraints(self) -> List[InequalitySpec]: + """Enabled constraints that the *current* parameter values violate. + + A fit started from an infeasible point begins on the BUMPS penalty + plateau; callers should refuse or warn before launching. + """ + violated = [] + for spec in self._inequality_constraints: + if spec.enabled and not evaluate_spec(spec, self.resolve_parameter_path).satisfied: + violated.append(spec) + return violated + + def build_constraints_factory(self): + """``constraints_factory`` hook for the enabled inequality constraints, or ``None``.""" + return build_constraints_factory(self._inequality_constraints, self.resolve_parameter_path) + @property def calculator(self) -> str: """Calculator function.""" @@ -508,7 +672,10 @@ def add_sample_from_orso(self, sample: Sample) -> None: """ if sample is None: raise ValueError('The ORSO file does not contain a valid sample model definition.') - model = Model(sample=sample) + # Take the collection's next colour: a supplied model keeps its own + # colour, and the Model default would give every loaded sample the + # same first palette colour. + model = Model(sample=sample, color=self._models.next_color()) self.models.add_model(model) # Set interface after adding to collection model.interface = self._calculator @@ -1372,6 +1539,8 @@ def as_dict(self, include_materials_not_in_model=False): project_dict['calculator'] = self._calculator.current_interface_name if self._colors is not None: project_dict['colors'] = self._colors + if self._inequality_constraints: + project_dict['inequality_constraints'] = [spec.to_dict() for spec in self._inequality_constraints] return project_dict def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict): @@ -1470,6 +1639,9 @@ def from_dict(self, project_dict: dict): # Resolve any pending parameter dependencies (constraints) after all objects are loaded resolve_all_parameter_dependencies(self) + # Inequality constraints are declarative (paths), nothing to resolve yet: + # they are bound to parameters when a fit starts. + self._inequality_constraints = [InequalitySpec.from_dict(raw) for raw in project_dict.get('inequality_constraints', [])] def _from_dict_extract_experiments(self, project_dict: dict) -> Dict[int, Union[DataSet1D, PolarizedDataSet]]: """From dict extract experiments.""" diff --git a/src/easyreflectometry/sample/assemblies/base_assembly.py b/src/easyreflectometry/sample/assemblies/base_assembly.py index fcb2f06d..a4ff714e 100644 --- a/src/easyreflectometry/sample/assemblies/base_assembly.py +++ b/src/easyreflectometry/sample/assemblies/base_assembly.py @@ -8,6 +8,15 @@ from ..elements.layers.layer import Layer +def follows_equal(follower, leader) -> bool: + """Whether `follower` is constrained to be exactly equal to `leader`.""" + if getattr(follower, 'independent', True): + return False + dependency_map = getattr(follower, '_dependency_map', None) or {} + expression = (getattr(follower, '_clean_dependency_string', None) or '').strip() + return len(dependency_map) == 1 and expression in dependency_map and dependency_map[expression] is leader + + class BaseAssembly(BaseCore): """Assembly of layers. @@ -97,6 +106,51 @@ def back_layer(self, layer: Layer) -> None: else: self.layers[-1] = layer + # ----- public constraint toggles ----- + + def _layers_follow_front(self, attribute: str) -> bool: + """True when every layer's `attribute` is tied *equal* to the front layer's. + + Only the conformal idiom counts (expression ``'a'`` over ``{'a': leader}``, + as set up by ``_setup_*_constraints``); a parameter that merely *uses* + the front layer's value in some other expression is not conformal. + """ + if len(self.layers) < 2: + return False + leader = getattr(self.front_layer, attribute) + for layer in list(self.layers)[1:]: + follower = getattr(layer, attribute) + if not follows_equal(follower, leader): + return False + return True + + @property + def conformal_thickness(self) -> bool: + """Whether every layer shares the front layer's thickness.""" + return self._layers_follow_front('thickness') + + @conformal_thickness.setter + def conformal_thickness(self, status: bool) -> None: + """Tie (or release) every layer's thickness to the front layer's.""" + if status: + self._setup_thickness_constraints() + elif self._layers_follow_front('thickness'): + for layer in list(self.layers)[1:]: + layer.thickness.make_independent() + + @property + def conformal_roughness(self) -> bool: + """Whether every layer shares the front layer's roughness.""" + return self._layers_follow_front('roughness') + + @conformal_roughness.setter + def conformal_roughness(self, status: bool) -> None: + """Tie (or release) every layer's roughness to the front layer's.""" + if status: + self._setup_roughness_constraints() + elif self._layers_follow_front('roughness'): + self._disable_roughness_constraints() + def _setup_thickness_constraints(self) -> None: """Setup thickness constraint, front layer is the deciding layer.""" independent_param = self.front_layer.thickness diff --git a/src/easyreflectometry/summary/summary.py b/src/easyreflectometry/summary/summary.py index 89b82f48..9e71f382 100644 --- a/src/easyreflectometry/summary/summary.py +++ b/src/easyreflectometry/summary/summary.py @@ -345,10 +345,12 @@ def _refinement_section(self) -> str: model = self._project._models[self._project.current_model_index] parameters = model.get_all_parameters() - num_free_params = sum(1 for parameter in parameters if parameter.free) - num_fixed_params = sum(1 for parameter in parameters if not parameter.free) - num_params = num_free_params + num_fixed_params + # Dependent parameters (user constraints, derived values such as the + # total thickness) are neither free nor fixed: they never enter a fit. + num_free_params = sum(1 for parameter in parameters if parameter.independent and parameter.free) + num_fixed_params = sum(1 for parameter in parameters if parameter.independent and not parameter.free) num_constraints = sum(1 for parameter in parameters if not parameter.independent) + num_params = num_free_params + num_fixed_params + num_constraints goodness_of_fit = self._compute_goodness_of_fit() diff --git a/src/easyreflectometry/utils.py b/src/easyreflectometry/utils.py index 43ac58d8..53d51301 100644 --- a/src/easyreflectometry/utils.py +++ b/src/easyreflectometry/utils.py @@ -80,13 +80,17 @@ def _collect(item): def count_free_parameters(project) -> int: - """Count free parameters.""" - return sum(1 for parameter in project.parameters if parameter.free) + """Count free parameters. + + Dependent parameters (constrained or derived) are neither free nor fixed: + they never enter a fit, whatever their ``free`` flag says. + """ + return sum(1 for parameter in project.parameters if parameter.independent and parameter.free) def count_fixed_parameters(project) -> int: - """Count fixed parameters.""" - return sum(1 for parameter in project.parameters if not parameter.free) + """Count fixed parameters (independent parameters that are not free).""" + return sum(1 for parameter in project.parameters if parameter.independent and not parameter.free) def count_parameter_user_constraints(project) -> int: diff --git a/tests/model/test_model_collection.py b/tests/model/test_model_collection.py index dda554b1..59db8f33 100644 --- a/tests/model/test_model_collection.py +++ b/tests/model/test_model_collection.py @@ -94,6 +94,27 @@ def test_add_model_preserves_explicit_color(self): assert collection[-1].color == custom_color assert collection._next_color_index == (expected_index + 1) % len(COLORS) + def test_next_color_matches_what_appending_assigns(self): + collection = ModelCollection(populate_if_none=False) + collection.add_model() + announced = collection.next_color() + + collection.add_model(Model(name='Prebuilt', color=announced)) + + assert collection[-1].color == announced + assert collection[0].color != collection[1].color + # and the cycle moved on + assert collection.next_color() != announced or len(COLORS) <= 2 + + def test_duplicate_model_gets_a_distinct_color(self): + collection = ModelCollection(populate_if_none=False) + collection.add_model() + + collection.duplicate_model(0) + + assert collection[1].name.endswith('duplicate') + assert collection[0].color != collection[1].color + def test_delete_model(self): # When model_1 = Model(name='Model1') diff --git a/tests/test_orso_utils.py b/tests/test_orso_utils.py index 4ad2e9ab..b9e3368e 100644 --- a/tests/test_orso_utils.py +++ b/tests/test_orso_utils.py @@ -174,3 +174,78 @@ def test_load_orso_model_returns_none_and_warns_when_no_sample_model(): assert result is None assert len(w) == 1 assert 'does not contain a sample model definition' in str(w[0].message) + + +def test_load_orso_model_resolves_named_materials(tmp_path): + """Named materials from the file's `materials` section must be resolved. + + Regression: `load_orso_model` used to rebuild the ORSO ``SampleModel`` + from `stack` and `layers` alone, dropping the `materials` definitions — + every named material then read as SLD 0. Also covers SLDs stored as + orsopy ``Value`` objects and layers whose material has neither a name + nor a formula (must not produce ``None`` names). + """ + import datetime + + import numpy as np + from orsopy import fileio + from orsopy.fileio import model_language + + sample_model = model_language.SampleModel( + stack='ambient | film | substrate', + layers={ + 'ambient': model_language.Layer( + thickness=fileio.Value(0.0, 'angstrom'), roughness=fileio.Value(0.0, 'angstrom'), material='vac' + ), + 'film': model_language.Layer( + thickness=fileio.Value(35.0, 'angstrom'), roughness=fileio.Value(3.0, 'angstrom'), material='MatA' + ), + 'substrate': model_language.Layer( + thickness=fileio.Value(0.0, 'angstrom'), roughness=fileio.Value(2.0, 'angstrom'), material='Si' + ), + }, + materials={ + 'vac': model_language.Material(sld=fileio.Value(0.0, '1/angstrom^2')), + 'MatA': model_language.Material(sld=fileio.Value(3.0e-6, '1/angstrom^2')), + 'Si': model_language.Material(sld=fileio.Value(2.07e-6, '1/angstrom^2')), + }, + globals=model_language.ModelParameters(length_unit='angstrom'), + ) + header = fileio.Orso( + data_source=fileio.DataSource( + owner=fileio.Person(name='test', affiliation='test'), + experiment=fileio.Experiment( + title='t', instrument='sim', start_date=datetime.datetime(2026, 1, 1), probe='neutron' + ), + sample=fileio.Sample(name='named materials', model=sample_model), + measurement=fileio.Measurement( + instrument_settings=fileio.InstrumentSettings( + incident_angle=fileio.Value(1.0, 'deg'), wavelength=fileio.Value(6.0, 'angstrom') + ), + data_files=[], + ), + ), + reduction=fileio.Reduction(software=fileio.Software(name='test')), + columns=[fileio.Column('Qz', '1/angstrom'), fileio.Column('R')], + data_set=0, + ) + q = np.linspace(0.01, 0.1, 5) + path = tmp_path / 'named_materials.ort' + fileio.save_orso([fileio.OrsoDataset(header, np.array([q, np.ones_like(q)]).T)], str(path)) + + from orsopy.fileio import orso as orso_io + + sample = load_orso_model(orso_io.load_orso(str(path))) + + assert sample is not None + film = sample[1].layers[0] + assert film.name == 'film' + assert film.thickness.value == pytest.approx(35.0) + assert film.roughness.value == pytest.approx(3.0) + assert film.material.sld.value == pytest.approx(3.0) # 1e-6/A^2, resolved from `materials` + assert sample[2].layers[0].material.sld.value == pytest.approx(2.07) + # No layer or material name may come back as None + for assembly in sample: + for layer in assembly.layers: + assert layer.name is not None + assert layer.material.name is not None diff --git a/tests/test_project.py b/tests/test_project.py index 62dd67d2..bdfbc0ce 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -808,9 +808,10 @@ def test_parameters(self): # Then parameters = project.parameters - # Expect - assert len(parameters) == 14 + # Expect: 14 layer/material/model parameters + the model's derived total thickness + assert len(parameters) == 15 assert isinstance(parameters[0], Parameter) + assert any(parameter is project.models[0].total_thickness for parameter in parameters) def test_parameters_enabled_flags(self): global_object.map._clear() @@ -973,6 +974,10 @@ def test_add_sample_from_orso_multiple_additions(self): assert material_1 in project._materials assert material_2 in project._materials assert project.current_model_index == 1 + # Each loaded sample gets its own colour from the collection's cycle; + # without this, every ORSO-loaded model rendered in the first palette + # colour and their curves were indistinguishable. + assert project._models[0].color != project._models[1].color def test_add_sample_from_orso_with_shared_materials(self): # When diff --git a/tests/unit/test_constraints.py b/tests/unit/test_constraints.py index 3bdbe938..2e153051 100644 --- a/tests/unit/test_constraints.py +++ b/tests/unit/test_constraints.py @@ -149,17 +149,9 @@ def test_idempotent_on_independent_parameter(self): class TestProjectRoundTrip: - @pytest.mark.xfail( - reason=( - 'EasyScience serializes nested Parameters via the generic arg-spec walk ' - '(SerializerBase._convert_to_dict), which drops _dependency_string / ' - '_independent / serializer ids, and decodes them via cls(**data) instead of ' - 'Parameter.from_dict — so user constraints between model parameters do not ' - 'survive save/load. Fix belongs in easyscience core; this test flips when it lands.' - ), - strict=True, - ) def test_constraint_survives_as_dict_from_dict(self): + # Requires the easyscience serializer to route nested Parameters through + # ``Parameter.as_dict`` and park the dependency as pending on rebuild. src_project = Project() src_project._info['name'] = 'Test' src_project.default_model() @@ -180,5 +172,5 @@ def test_constraint_survives_as_dict_from_dict(self): follower = sample[2].layers[0].thickness assert follower.independent is False - leader.value = 42.0 - assert follower.value == 84.0 + leader.value = 60.0 # within the default [50, 200] thickness limits + assert follower.value == 120.0 diff --git a/tests/unit/test_derived_parameters.py b/tests/unit/test_derived_parameters.py new file mode 100644 index 00000000..ef01cc76 --- /dev/null +++ b/tests/unit/test_derived_parameters.py @@ -0,0 +1,180 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for derived read-only parameters: helpers, ``Model.total_thickness`` and assembly toggles.""" + +import json + +import pytest +from easyscience import global_object +from easyscience.variable import Parameter + +from easyreflectometry import Project +from easyreflectometry.constraints import constrain_to_sum +from easyreflectometry.constraints import derived_parameter +from easyreflectometry.model import Model +from easyreflectometry.sample import Layer +from easyreflectometry.sample import Material +from easyreflectometry.sample import Multilayer +from easyreflectometry.sample import Sample + + +@pytest.fixture(autouse=True) +def clear_global_map(): + global_object.map._clear() + yield + global_object.map._clear() + + +def _thickness(value, name): + return Parameter(name, value, unit='angstrom', min=0.0, max=1000.0) + + +class TestDerivedParameter: + def test_follows_expression_and_is_read_only(self): + a, b = _thickness(10.0, 'a'), _thickness(20.0, 'b') + total = derived_parameter('total', 'a + b', a=a, b=b) + + assert total.value == 30.0 + assert str(total.unit) == 'Å' + assert total.independent is False + a.value = 15.0 + assert total.value == 35.0 + with pytest.raises(AttributeError): + total.value = 1.0 + + def test_requires_at_least_one_dependency(self): + with pytest.raises(ValueError): + derived_parameter('total', '1 + 1') + + def test_can_drive_other_dependencies(self): + a, b, c = _thickness(10.0, 'a'), _thickness(20.0, 'b'), _thickness(0.0, 'c') + total = derived_parameter('total', 'a + b', a=a, b=b) + c.make_dependent_on('t * 2', {'t': total}) + b.value = 30.0 + assert c.value == 80.0 + + +class TestConstrainToSum: + def test_absorbs_remainder_against_numeric_total(self): + a, b = _thickness(40.0, 'a'), _thickness(60.0, 'b') + constrain_to_sum(b, [a, b], total=120.0) + assert b.value == 80.0 + a.value = 50.0 + assert b.value == 70.0 + assert b.independent is False + + def test_default_total_is_current_sum(self): + a, b, c = _thickness(10.0, 'a'), _thickness(20.0, 'b'), _thickness(30.0, 'c') + constrain_to_sum(c, [a, b, c]) + assert c.value == 30.0 + a.value = 25.0 + assert c.value == 15.0 + + def test_total_can_be_a_parameter(self): + a, b, total = _thickness(10.0, 'a'), _thickness(20.0, 'b'), _thickness(100.0, 'T') + constrain_to_sum(b, [a], total=total) + assert b.value == 90.0 + total.value = 50.0 + assert b.value == 40.0 + + def test_rejects_nothing_to_constrain_against(self): + a = _thickness(10.0, 'a') + with pytest.raises(ValueError): + constrain_to_sum(a, [a]) + with pytest.raises(TypeError): + constrain_to_sum(a, [_thickness(1.0, 'b')], total='12') + + +def _model_with_film(*thicknesses): + sample = Sample(populate_if_none=False) + sample.add_assembly(Multilayer(Layer(Material(0.0, 0.0, 'air'), thickness=0.0, roughness=0.0, name='air'))) + for index, thickness in enumerate(thicknesses): + sample.add_assembly( + Multilayer(Layer(Material(2.0, 0.0, f'm{index}'), thickness=thickness, roughness=2.0, name=f'L{index}')) + ) + sample.add_assembly(Multilayer(Layer(Material(2.07, 0.0, 'Si'), thickness=0.0, roughness=2.0, name='Si'))) + return Model(sample=sample) + + +class TestModelTotalThickness: + def test_sums_film_layers_only(self): + model = _model_with_film(40.0, 60.0) + total = model.total_thickness + assert total.value == 100.0 + assert total.independent is False + assert total not in model.get_fit_parameters() + with pytest.raises(AttributeError): + total.value = 5.0 + + def test_tracks_edits_and_structure_changes(self): + model = _model_with_film(40.0, 60.0) + model.sample[1].layers[0].thickness.value = 45.0 + assert model.total_thickness.value == 105.0 + + # layer appended inside an assembly: no notification path exists, the + # property re-derives on access + model.sample[2].layers.append(Layer(Material(1.0, 0.0, 'x'), thickness=10.0, roughness=1.0, name='X')) + assert model.total_thickness.value == 115.0 + + model.remove_assembly(1) + assert model.total_thickness.value == 70.0 + + def test_no_film_gives_zero_and_independent(self): + model = _model_with_film() + assert model.total_thickness.value == 0.0 + assert model.total_thickness.independent is True + model.add_assemblies(Multilayer(Layer(Material(1.0, 0.0, 'x'), thickness=10.0, roughness=1.0, name='X'))) + # the new assembly became the last layer (subphase); the former Si (0 A) is now film + assert model.total_thickness.value == 0.0 + assert model.total_thickness.independent is False + + def test_not_serialized_but_rebuilt_on_load(self): + project = Project() + project.default_model() + model = project.models[0] + before = model.total_thickness.value + project_dict = json.loads(json.dumps(project.as_dict())) + assert 'total_thickness' not in json.dumps(project_dict) + + global_object.map._clear() + reloaded = Project() + reloaded.from_dict(project_dict) + assert reloaded.models[0].total_thickness.value == before + assert reloaded.models[0].total_thickness.independent is False + + +class TestAssemblyConformalToggles: + def _assembly(self): + layers = [ + Layer(Material(1.0, 0.0, f'm{i}'), thickness=10.0 * (i + 1), roughness=float(i + 1), name=f'L{i}') for i in range(3) + ] + return Multilayer(layers) + + def test_conformal_thickness(self): + assembly = self._assembly() + assert assembly.conformal_thickness is False + assembly.conformal_thickness = True + assert assembly.conformal_thickness is True + assert [layer.thickness.value for layer in assembly.layers] == [10.0, 10.0, 10.0] + assembly.layers[0].thickness.value = 25.0 + assert assembly.layers[2].thickness.value == 25.0 + assembly.conformal_thickness = False + assert assembly.conformal_thickness is False + assert assembly.layers[1].thickness.independent is True + + def test_conformal_roughness(self): + assembly = self._assembly() + assert assembly.conformal_roughness is False + assembly.conformal_roughness = True + assert assembly.conformal_roughness is True + assembly.layers[0].roughness.value = 7.0 + assert [layer.roughness.value for layer in assembly.layers] == [7.0, 7.0, 7.0] + assembly.conformal_roughness = False + assert assembly.layers[2].roughness.independent is True + + def test_single_layer_assembly_is_never_conformal(self): + assembly = Multilayer(Layer(Material(1.0, 0.0, 'm'), thickness=1.0, roughness=1.0, name='L')) + assert assembly.conformal_thickness is False + assembly.conformal_thickness = True + assert assembly.conformal_thickness is False diff --git a/tests/unit/test_inequality_constraints.py b/tests/unit/test_inequality_constraints.py new file mode 100644 index 00000000..73864db1 --- /dev/null +++ b/tests/unit/test_inequality_constraints.py @@ -0,0 +1,273 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for cross-parameter inequality constraints (specs, translation, project integration).""" + +import json + +import numpy as np +import pytest +from easyscience import global_object +from easyscience.fitting import AvailableMinimizers +from easyscience.variable import Parameter + +from easyreflectometry import Project +from easyreflectometry.constraints import constrain +from easyreflectometry.constraints import derived_parameter +from easyreflectometry.data import DataSet1D +from easyreflectometry.inequality_constraints import InequalitySpec +from easyreflectometry.inequality_constraints import build_constraints_factory +from easyreflectometry.inequality_constraints import check_units +from easyreflectometry.inequality_constraints import evaluate_spec +from easyreflectometry.sample import Layer +from easyreflectometry.sample import Material +from easyreflectometry.sample import Multilayer + + +@pytest.fixture(autouse=True) +def clear_global_map(): + global_object.map._clear() + yield + global_object.map._clear() + + +class _FakeBumpsParameter: + def __init__(self, value): + self.value = value + + +def _resolver(mapping): + return lambda path: mapping[path] + + +# --------------------------------------------------------------------------- spec + + +class TestInequalitySpec: + def test_normalizes_relation_aliases(self): + assert InequalitySpec('a', '≤', 'b', {'a': 'x'}, {'b': 'y'}).op == '<=' + assert InequalitySpec('a', '≥', 'b', {'a': 'x'}, {'b': 'y'}).op == '>=' + + def test_rejects_unknown_relation(self): + with pytest.raises(ValueError, match='Unsupported relation'): + InequalitySpec('a', '==', 'b', {'a': 'x'}, {'b': 'y'}) + + def test_rejects_unmapped_identifiers(self): + with pytest.raises(ValueError, match='unmapped names: c'): + InequalitySpec('a + c', '<', 'b', {'a': 'x'}, {'b': 'y'}) + + def test_rejects_empty_side_and_bad_syntax(self): + with pytest.raises(ValueError, match='cannot be empty'): + InequalitySpec('', '<', 'b', {}, {'b': 'y'}) + with pytest.raises(SyntaxError): + InequalitySpec('a +', '<', 'b', {'a': 'x'}, {'b': 'y'}) + + def test_numeric_rhs_and_math_symbols_need_no_mapping(self): + spec = InequalitySpec('sqrt(a) * pi', '<', '90', {'a': 'x'}, {}) + assert spec.rhs_paths == {} + + def test_round_trip_dict(self): + spec = InequalitySpec('a + b', '<=', 'c', {'a': 'p/a', 'b': 'p/b'}, {'c': 'p/c'}, name='n', enabled=False) + restored = InequalitySpec.from_dict(json.loads(json.dumps(spec.to_dict()))) + assert restored == spec + assert str(restored) == 'a + b <= c' + + +# --------------------------------------------------------------------------- translation + + +class TestTranslation: + def _params(self): + a = Parameter('a', 10.0, unit='angstrom', min=0, max=100) + b = Parameter('b', 20.0, unit='angstrom', min=0, max=100) + c = Parameter('c', 5.0, unit='angstrom', min=0, max=100, fixed=True) + return a, b, c + + def test_operands_read_bumps_values_not_easyscience_values(self): + a, b, c = self._params() + spec = InequalitySpec('a + b', '<', '25', {'a': 'pa', 'b': 'pb'}, {}) + factory = build_constraints_factory([spec], _resolver({'pa': a, 'pb': b})) + bumps_a, bumps_b = _FakeBumpsParameter(10.0), _FakeBumpsParameter(20.0) + (constraint,) = factory({'p' + a.unique_name: bumps_a, 'p' + b.unique_name: bumps_b}) + + assert float(constraint) == pytest.approx(5.0) # 30 - 25, linear violation + bumps_a.value = 1.0 # optimizer trial point: EasyScience `a` is still 10 + assert a.value == 10.0 + assert float(constraint) == 0.0 + assert str(constraint) == 'a + b < 25' + + def test_fixed_parameters_are_constants(self): + a, b, c = self._params() + spec = InequalitySpec('a', '>', 'c', {'a': 'pa'}, {'c': 'pc'}) + factory = build_constraints_factory([spec], _resolver({'pa': a, 'pc': c})) + (constraint,) = factory({'p' + a.unique_name: _FakeBumpsParameter(3.0)}) # c not in the problem + assert float(constraint) == pytest.approx(2.0) # 5 - 3 + + def test_dependent_parameters_are_expanded_to_free_leaves(self): + a, b, c = self._params() + total = derived_parameter('total', 'x + y + z', x=a, y=b, z=c) + half = Parameter('half', 0.0, unit='angstrom', min=-1e6, max=1e6) + constrain(half, 't / 2', t=total) # dependent on a dependent + spec = InequalitySpec('a', '<', 'h', {'a': 'pa'}, {'h': 'ph'}) + factory = build_constraints_factory([spec], _resolver({'pa': a, 'ph': half})) + bumps_a, bumps_b = _FakeBumpsParameter(10.0), _FakeBumpsParameter(20.0) + (constraint,) = factory({'p' + a.unique_name: bumps_a, 'p' + b.unique_name: bumps_b}) + + # half = (10 + 20 + 5) / 2 = 17.5 > a = 10 -> satisfied + assert float(constraint) == 0.0 + bumps_b.value = 0.0 # half = 7.5 < a = 10 -> violated by 2.5, read from the trial vector + assert float(constraint) == pytest.approx(2.5) + + def test_disabled_specs_give_no_factory(self): + a, b, c = self._params() + spec = InequalitySpec('a', '<', 'b', {'a': 'pa'}, {'b': 'pb'}, enabled=False) + assert build_constraints_factory([spec], _resolver({'pa': a, 'pb': b})) is None + assert build_constraints_factory([], _resolver({})) is None + + @pytest.mark.parametrize( + 'op, lhs, rhs, violation', + [('<', 3.0, 5.0, 0.0), ('<', 5.0, 5.0, 0.0), ('<', 7.0, 5.0, 2.0), ('>', 3.0, 5.0, 2.0), ('>=', 6.0, 5.0, 0.0)], + ) + def test_evaluate_spec_violation(self, op, lhs, rhs, violation): + a = Parameter('a', lhs, min=-100, max=100) + b = Parameter('b', rhs, min=-100, max=100) + spec = InequalitySpec('a', op, 'b', {'a': 'pa'}, {'b': 'pb'}) + result = evaluate_spec(spec, _resolver({'pa': a, 'pb': b})) + assert result.lhs == lhs and result.rhs == rhs + assert result.violation == pytest.approx(violation) + assert result.satisfied is (violation == 0.0) + + def test_check_units(self): + a, b, c = self._params() + sld = Parameter('sld', 2.0, unit='1/angstrom**2', min=-10, max=10) + check_units( + InequalitySpec('a + b', '<', 'c', {'a': 'pa', 'b': 'pb'}, {'c': 'pc'}), + _resolver({'pa': a, 'pb': b, 'pc': c}), + ) + check_units(InequalitySpec('a', '<', '90', {'a': 'pa'}, {}), _resolver({'pa': a})) + with pytest.raises(ValueError, match='Incompatible units'): + check_units(InequalitySpec('a', '<', 's', {'a': 'pa'}, {'s': 'ps'}), _resolver({'pa': a, 'ps': sld})) + + def test_check_units_mixed_literals_fall_back_to_numeric(self): + # '90 - b' cannot be evaluated with units (a literal has none); it is + # checked numerically and its literals read in the other side's unit. + a, b, c = self._params() + check_units(InequalitySpec('a', '<', '90 - b', {'a': 'pa'}, {'b': 'pb'}), _resolver({'pa': a, 'pb': b})) + # broken syntax is still rejected (at spec construction) + with pytest.raises(SyntaxError): + InequalitySpec('a', '<', '90 - b +', {'a': 'pa'}, {'b': 'pb'}) + + +# --------------------------------------------------------------------------- project integration + + +def _two_layer_project(): + project = Project() + project.default_model() + model = project.models[0] + film_a = Multilayer(Layer(Material(3.0, 0.0, 'A'), thickness=40.0, roughness=3.0, name='A'), name='A') + film_b = Multilayer(Layer(Material(5.0, 0.0, 'B'), thickness=60.0, roughness=3.0, name='B'), name='B') + substrate = model.sample[-1] + model.remove_assembly(len(model.sample) - 1) + model.remove_assembly(len(model.sample) - 1) + model.add_assemblies(film_a, film_b, substrate) + return project, model + + +class TestProjectPaths: + def test_parameter_path_round_trip(self): + project, model = _two_layer_project() + t_a = model.sample[1].layers[0].thickness + path = project.parameter_path(t_a) + assert path == 'models/0/sample/1/layers/0/thickness' + assert project.resolve_parameter_path(path) is t_a + assert project.parameter_path(model.scale) == 'models/0/scale' + assert project.parameter_path(model.total_thickness) == 'models/0/total_thickness' + sld_path = project.parameter_path(model.sample[1].layers[0].material.sld) + assert sld_path == 'models/0/sample/1/layers/0/material/sld' + assert project.resolve_parameter_path(sld_path) is model.sample[1].layers[0].material.sld + + def test_unreachable_parameter_and_bad_paths(self): + project, model = _two_layer_project() + assert project.parameter_path(Parameter('loose', 1.0)) is None + with pytest.raises(KeyError): + project.resolve_parameter_path('models/0/sample/99/layers/0/thickness') + with pytest.raises(KeyError): + project.resolve_parameter_path('models/0/nope') + with pytest.raises(KeyError): + project.resolve_parameter_path('models/0/sample') # not a parameter + with pytest.raises(KeyError): + project.resolve_parameter_path('models/0/_sample/0') # private attributes are off limits + + +class TestProjectInequalities: + def test_registry_validation_and_persistence(self): + project, model = _two_layer_project() + t_a = model.sample[1].layers[0].thickness + t_b = model.sample[2].layers[0].thickness + pa, pb = project.parameter_path(t_a), project.parameter_path(t_b) + project.add_inequality_constraint(InequalitySpec('a', '<', 'b', {'a': pa}, {'b': pb}, name='order')) + project.add_inequality_constraint(InequalitySpec('a + b', '<', '90', {'a': pa, 'b': pb}, {}, name='budget')) + with pytest.raises(ValueError, match='Incompatible units'): + project.add_inequality_constraint( + InequalitySpec('a', '<', 's', {'a': pa}, {'s': project.parameter_path(model.scale)}) + ) + assert [s.name for s in project.violated_inequality_constraints()] == ['budget'] + + project_dict = json.loads(json.dumps(project.as_dict())) + assert len(project_dict['inequality_constraints']) == 2 + global_object.map._clear() + reloaded = Project() + reloaded.from_dict(project_dict) + assert [str(s) for s in reloaded.inequality_constraints] == ['a < b', 'a + b < 90'] + evaluations = reloaded.evaluate_inequality_constraints() + assert [e.satisfied for e in evaluations] == [True, False] + + reloaded.remove_inequality_constraint('budget') + assert [s.name for s in reloaded.inequality_constraints] == ['order'] + reloaded.remove_inequality_constraint(0) + assert reloaded.inequality_constraints == [] + assert 'inequality_constraints' not in reloaded.as_dict() + + def test_old_project_files_without_inequalities_load(self): + project, _ = _two_layer_project() + project_dict = project.as_dict() + project_dict.pop('inequality_constraints', None) + global_object.map._clear() + reloaded = Project() + reloaded.from_dict(project_dict) + assert reloaded.inequality_constraints == [] + + +class TestInequalityFit: + def test_bumps_fit_respects_inequality_and_lmfit_is_rejected(self): + project, model = _two_layer_project() + project.minimizer = AvailableMinimizers.Bumps + layers = [layer for assembly in model.sample for layer in assembly.layers] + t_a, t_b = layers[1].thickness, layers[2].thickness + q = np.linspace(0.01, 0.3, 150) + t_a.value, t_b.value = 45.0, 55.0 # truth sums to 100 + r_true = model.interface.fit_func(q, model.unique_name) + t_a.value, t_b.value = 30.0, 50.0 # feasible start + for layer in layers: + for par in (layer.thickness, layer.roughness, layer.material.sld, layer.material.isld): + par.fixed = True + t_a.fixed = False + t_b.fixed = False + model.scale.fixed = True + model.background.fixed = True + pa, pb = project.parameter_path(t_a), project.parameter_path(t_b) + project.add_inequality_constraint(InequalitySpec('a + b', '<', '90', {'a': pa, 'b': pb}, {}, name='budget')) + project.add_inequality_constraint(InequalitySpec('a', '<', 'b', {'a': pa}, {'b': pb}, name='order')) + dataset = DataSet1D(name='sim', x=q, y=r_true, ye=(0.05 * r_true) ** 2) + + result = project.fitter.fit_single_data_set_1d(dataset) + + assert result.success + assert t_a.value + t_b.value <= 90.0 + 1e-3 + assert t_a.value + t_b.value == pytest.approx(90.0, abs=0.05) # lands on the boundary + assert t_a.value <= t_b.value + 1e-3 + + project.minimizer = AvailableMinimizers.LMFit + with pytest.raises(ValueError, match='require the BUMPS engine'): + project.fitter.fit_single_data_set_1d(dataset) From 14cc894648e97fb4aee53f9f0fa3a6921844115c Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Tue, 25 Aug 2026 20:42:13 +0200 Subject: [PATCH 7/7] .bounds -> min, max --- .../advancedfitting/bayesian_bumps.ipynb | 14 +++-- .../advancedfitting/multi_contrast.ipynb | 30 +++++++---- .../advancedfitting/polarized_fitting.ipynb | 9 ++-- .../tutorials/fitting/material_solvated.ipynb | 24 ++++++--- docs/docs/tutorials/fitting/monolayer.ipynb | 12 +++-- docs/docs/tutorials/fitting/repeating.ipynb | 3 +- .../tutorials/fitting/simple_fitting.ipynb | 48 +++++++++++------ .../elements/layers/test_layer_magnetism.py | 3 +- tests/test_fitting.py | 54 ++++++++++++------- tests/test_polarized_fitting.py | 15 ++++-- 10 files changed, 141 insertions(+), 71 deletions(-) diff --git a/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb b/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb index bf136533..b4c3ca72 100644 --- a/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb +++ b/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb @@ -133,21 +133,25 @@ "\n", "# ---- Make key parameters free with realistic bounds (essential for MCMC) -----\n", "film_layer.thickness.fixed = False\n", - "film_layer.thickness.bounds = (100, 400)\n", + "film_layer.thickness.min = 100\n", + "film_layer.thickness.max = 400\n", "\n", "film.sld.fixed = False\n", - "film.sld.bounds = (0.5, 4.0)\n", + "film.sld.min = 0.5\n", + "film.sld.max = 4.0\n", "\n", "model.scale.fixed = False\n", - "model.scale.bounds = (0.8, 1.2)\n", + "model.scale.min = 0.8\n", + "model.scale.max = 1.2\n", "\n", "model.background.fixed = False\n", - "model.background.bounds = (1e-7, 1e-5)\n", + "model.background.min = 1e-7\n", + "model.background.max = 1e-5\n", "\n", "print('Model created with the following free parameters:')\n", "for p in model.get_parameters():\n", " if not p.fixed:\n", - " print(f' {p.name}: value={p.value}, bounds={p.bounds}')" + " print(f' {p.name}: value={p.value}, min={p.min}, max={p.max}')" ] }, { diff --git a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb index 1520b4ac..3340315b 100644 --- a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb +++ b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb @@ -488,17 +488,27 @@ "metadata": {}, "outputs": [], "source": [ - "d13d2o_model.scale.bounds = (0.05, 1.5)\n", - "d13d2o_model.background.bounds = (4e-8, 1e-5)\n", - "d70d2o_model.scale.bounds = (0.05, 1.5)\n", - "d70d2o_model.background.bounds = (4e-8, 1e-5)\n", - "d83acmw_model.scale.bounds = (0.05, 1.5)\n", - "d83acmw_model.background.bounds = (4e-8, 1e-5)\n", + "d13d2o_model.scale.min = 0.05\n", + "d13d2o_model.scale.max = 1.5\n", + "d13d2o_model.background.min = 4e-8\n", + "d13d2o_model.background.max = 1e-5\n", + "d70d2o_model.scale.min = 0.05\n", + "d70d2o_model.scale.max = 1.5\n", + "d70d2o_model.background.min = 4e-8\n", + "d70d2o_model.background.max = 1e-5\n", + "d83acmw_model.scale.min = 0.05\n", + "d83acmw_model.scale.max = 1.5\n", + "d83acmw_model.background.min = 4e-8\n", + "d83acmw_model.background.max = 1e-5\n", "\n", - "d13d2o.tail_layer.area_per_molecule_parameter.bounds = (40, 50)\n", - "d13d2o.head_layer.solvent_fraction_parameter.bounds = (0.2, 0.6)\n", - "d13d2o.tail_layer.thickness.bounds = (18, 24)\n", - "d13d2o.head_layer.thickness.bounds = (8, 12)" + "d13d2o.tail_layer.area_per_molecule_parameter.min = 40\n", + "d13d2o.tail_layer.area_per_molecule_parameter.max = 50\n", + "d13d2o.head_layer.solvent_fraction_parameter.min = 0.2\n", + "d13d2o.head_layer.solvent_fraction_parameter.max = 0.6\n", + "d13d2o.tail_layer.thickness.min = 18\n", + "d13d2o.tail_layer.thickness.max = 24\n", + "d13d2o.head_layer.thickness.min = 8\n", + "d13d2o.head_layer.thickness.max = 12" ] }, { diff --git a/docs/docs/tutorials/advancedfitting/polarized_fitting.ipynb b/docs/docs/tutorials/advancedfitting/polarized_fitting.ipynb index cb820cbf..caae5b09 100644 --- a/docs/docs/tutorials/advancedfitting/polarized_fitting.ipynb +++ b/docs/docs/tutorials/advancedfitting/polarized_fitting.ipynb @@ -378,7 +378,8 @@ "fit_model_2ch, fit_film_2ch = build_magnetic_model(rho_m=1.0, theta_m=THETA_M_TRUE, name='Fit: two channels (rho_m only)')\n", "\n", "fit_film_2ch.magnetism.rho_m.fixed = False\n", - "fit_film_2ch.magnetism.rho_m.bounds = (0.0, 5.0)\n", + "fit_film_2ch.magnetism.rho_m.min = 0.0\n", + "fit_film_2ch.magnetism.rho_m.max = 5.0\n", "fit_film_2ch.magnetism.theta_m.fixed = True # moment direction assumed known\n", "\n", "initial_channels_2ch = fit_model_2ch.interface.polarized_reflectivity_profiles(Q_PLOT, fit_model_2ch.unique_name)\n", @@ -469,9 +470,11 @@ "fit_model_4ch, fit_film_4ch = build_magnetic_model(rho_m=1.5, theta_m=60.0, name='Fit: four channels (rho_m and theta_m)')\n", "\n", "fit_film_4ch.magnetism.rho_m.fixed = False\n", - "fit_film_4ch.magnetism.rho_m.bounds = (0.0, 5.0)\n", + "fit_film_4ch.magnetism.rho_m.min = 0.0\n", + "fit_film_4ch.magnetism.rho_m.max = 5.0\n", "fit_film_4ch.magnetism.theta_m.fixed = False\n", - "fit_film_4ch.magnetism.theta_m.bounds = (0.0, 90.0)\n", + "fit_film_4ch.magnetism.theta_m.min = 0.0\n", + "fit_film_4ch.magnetism.theta_m.max = 90.0\n", "\n", "fit_data_4ch = PolarizedDataSet(\n", " name='Fe film (pp, pm, mp, mm)',\n", diff --git a/docs/docs/tutorials/fitting/material_solvated.ipynb b/docs/docs/tutorials/fitting/material_solvated.ipynb index cc7f8695..64c62637 100644 --- a/docs/docs/tutorials/fitting/material_solvated.ipynb +++ b/docs/docs/tutorials/fitting/material_solvated.ipynb @@ -269,18 +269,26 @@ "outputs": [], "source": [ "# Thicknesses\n", - "sio2_layer.thickness.bounds = (15, 50)\n", - "solvated_film.thickness.bounds = (200, 300)\n", + "sio2_layer.thickness.min = 15\n", + "sio2_layer.thickness.max = 50\n", + "solvated_film.thickness.min = 200\n", + "solvated_film.thickness.max = 300\n", "# Roughnesses\n", - "sio2_layer.roughness.bounds = (1, 15)\n", - "solvated_film.roughness.bounds = (1, 15)\n", - "subphase.roughness.bounds = (1, 15)\n", + "sio2_layer.roughness.min = 1\n", + "sio2_layer.roughness.max = 15\n", + "solvated_film.roughness.min = 1\n", + "solvated_film.roughness.max = 15\n", + "subphase.roughness.min = 1\n", + "subphase.roughness.max = 15\n", "# Scattering length density\n", - "film.sld.bounds = (0.1, 3)\n", + "film.sld.min = 0.1\n", + "film.sld.max = 3\n", "# Background\n", - "model.background.bounds = (1e-8, 1e-5)\n", + "model.background.min = 1e-8\n", + "model.background.max = 1e-5\n", "# Scale\n", - "model.scale.bounds = (0.5, 1.5)" + "model.scale.min = 0.5\n", + "model.scale.max = 1.5" ] }, { diff --git a/docs/docs/tutorials/fitting/monolayer.ipynb b/docs/docs/tutorials/fitting/monolayer.ipynb index e3f90336..03ec7c78 100644 --- a/docs/docs/tutorials/fitting/monolayer.ipynb +++ b/docs/docs/tutorials/fitting/monolayer.ipynb @@ -354,11 +354,15 @@ "metadata": {}, "outputs": [], "source": [ - "model.scale.bounds = (0.05, 1.5)\n", - "model.background.bounds = (4e-7, 1e-6)\n", + "model.scale.min = 0.05\n", + "model.scale.max = 1.5\n", + "model.background.min = 4e-7\n", + "model.background.max = 1e-6\n", "\n", - "dspc.tail_layer.area_per_molecule_parameter.bounds = (30, 60)\n", - "dspc.head_layer.solvent_fraction_parameter.bounds = (0.4, 0.7)" + "dspc.tail_layer.area_per_molecule_parameter.min = 30\n", + "dspc.tail_layer.area_per_molecule_parameter.max = 60\n", + "dspc.head_layer.solvent_fraction_parameter.min = 0.4\n", + "dspc.head_layer.solvent_fraction_parameter.max = 0.7" ] }, { diff --git a/docs/docs/tutorials/fitting/repeating.ipynb b/docs/docs/tutorials/fitting/repeating.ipynb index ea35dffc..10514c69 100644 --- a/docs/docs/tutorials/fitting/repeating.ipynb +++ b/docs/docs/tutorials/fitting/repeating.ipynb @@ -227,7 +227,8 @@ "metadata": {}, "outputs": [], "source": [ - "ti_layer.thickness.bounds = (10, 60)" + "ti_layer.thickness.min = 10\n", + "ti_layer.thickness.max = 60" ] }, { diff --git a/docs/docs/tutorials/fitting/simple_fitting.ipynb b/docs/docs/tutorials/fitting/simple_fitting.ipynb index ce6b09eb..2ddaca24 100644 --- a/docs/docs/tutorials/fitting/simple_fitting.ipynb +++ b/docs/docs/tutorials/fitting/simple_fitting.ipynb @@ -358,14 +358,20 @@ "outputs": [], "source": [ "# Thicknesses\n", - "sio2_layer.thickness.bounds = (15, 50)\n", - "film_layer.thickness.bounds = (200, 300)\n", + "sio2_layer.thickness.min = 15\n", + "sio2_layer.thickness.max = 50\n", + "film_layer.thickness.min = 200\n", + "film_layer.thickness.max = 300\n", "# Roughnesses\n", - "sio2_layer.roughness.bounds = (1, 15)\n", - "film_layer.roughness.bounds = (1, 15)\n", - "subphase.roughness.bounds = (1, 15)\n", + "sio2_layer.roughness.min = 1\n", + "sio2_layer.roughness.max = 15\n", + "film_layer.roughness.min = 1\n", + "film_layer.roughness.max = 15\n", + "subphase.roughness.min = 1\n", + "subphase.roughness.max = 15\n", "# Scattering length density\n", - "film_layer.material.sld.bounds = (0.1, 3)" + "film_layer.material.sld.min = 0.1\n", + "film_layer.material.sld.max = 3" ] }, { @@ -384,9 +390,11 @@ "outputs": [], "source": [ "# Background\n", - "model.background.bounds = (1e-8, 1e-5)\n", + "model.background.min = 1e-8\n", + "model.background.max = 1e-5\n", "# Scale\n", - "model.scale.bounds = (0.5, 1.5)" + "model.scale.min = 0.5\n", + "model.scale.max = 1.5" ] }, { @@ -556,14 +564,22 @@ " sample=sample_refl1d, scale=1, background=1e-6, resolution_function=resolution_function_refl1d, name='Film Model (Refl1D)'\n", ")\n", "\n", - "sio2_layer_refl1d.thickness.bounds = (15, 50)\n", - "film_layer_refl1d.thickness.bounds = (200, 300)\n", - "sio2_layer_refl1d.roughness.bounds = (1, 15)\n", - "film_layer_refl1d.roughness.bounds = (1, 15)\n", - "subphase_refl1d.roughness.bounds = (1, 15)\n", - "film_layer_refl1d.material.sld.bounds = (0.1, 3)\n", - "model_refl1d.background.bounds = (1e-8, 1e-5)\n", - "model_refl1d.scale.bounds = (0.5, 1.5)\n", + "sio2_layer_refl1d.thickness.min = 15\n", + "sio2_layer_refl1d.thickness.max = 50\n", + "film_layer_refl1d.thickness.min = 200\n", + "film_layer_refl1d.thickness.max = 300\n", + "sio2_layer_refl1d.roughness.min = 1\n", + "sio2_layer_refl1d.roughness.max = 15\n", + "film_layer_refl1d.roughness.min = 1\n", + "film_layer_refl1d.roughness.max = 15\n", + "subphase_refl1d.roughness.min = 1\n", + "subphase_refl1d.roughness.max = 15\n", + "film_layer_refl1d.material.sld.min = 0.1\n", + "film_layer_refl1d.material.sld.max = 3\n", + "model_refl1d.background.min = 1e-8\n", + "model_refl1d.background.max = 1e-5\n", + "model_refl1d.scale.min = 0.5\n", + "model_refl1d.scale.max = 1.5\n", "\n", "interface_refl1d = CalculatorFactory()\n", "interface_refl1d.switch('refl1d')\n", diff --git a/tests/sample/elements/layers/test_layer_magnetism.py b/tests/sample/elements/layers/test_layer_magnetism.py index a2932dec..1688ffc0 100644 --- a/tests/sample/elements/layers/test_layer_magnetism.py +++ b/tests/sample/elements/layers/test_layer_magnetism.py @@ -217,7 +217,8 @@ def test_fit_recovers_rho_m_from_synthetic_data(self): model.interface = self._interface('refl1d') rho_m = model.sample[1].layers[0].magnetism.rho_m rho_m.fixed = False - rho_m.bounds = (0.0, 5.0) + rho_m.min = 0.0 + rho_m.max = 5.0 fitter = MultiFitter(model) result = fitter.fit_single_data_set_1d(data) diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 9fd02a4b..b1a1cf32 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -51,25 +51,33 @@ def test_fitting(minimizer): model = Model(sample, 1, 1e-6, resolution_function, 'Film Model') # Thicknesses sio2_layer.thickness.fixed = False - sio2_layer.thickness.bounds = (15, 50) + sio2_layer.thickness.min = 15 + sio2_layer.thickness.max = 50 film_layer.thickness.fixed = False - film_layer.thickness.bounds = (200, 300) + film_layer.thickness.min = 200 + film_layer.thickness.max = 300 # Roughnesses si_layer.roughness.fixed = True - sio2_layer.roughness.bounds = (1, 15) + sio2_layer.roughness.min = 1 + sio2_layer.roughness.max = 15 film_layer.roughness.fixed = False - film_layer.roughness.bounds = (1, 15) + film_layer.roughness.min = 1 + film_layer.roughness.max = 15 superphase.roughness.fixed = True - superphase.roughness.bounds = (1, 15) + superphase.roughness.min = 1 + superphase.roughness.max = 15 # Scattering length density film.sld.fixed = False - film.sld.bounds = (0.1, 3) + film.sld.min = 0.1 + film.sld.max = 3 # Background model.background.fixed = False - model.background.bounds = (1e-7, 1e-5) + model.background.min = 1e-7 + model.background.max = 1e-5 # Scale model.scale.fixed = False - model.scale.bounds = (0.5, 1.5) + model.scale.min = 0.5 + model.scale.max = 1.5 interface = CalculatorFactory() model.interface = interface fitter = MultiFitter(model) @@ -121,15 +129,20 @@ def test_fitting_with_zero_variance(): # Set some parameters as fittable sio2_layer.thickness.fixed = False - sio2_layer.thickness.bounds = (15, 50) + sio2_layer.thickness.min = 15 + sio2_layer.thickness.max = 50 film_layer.thickness.fixed = False - film_layer.thickness.bounds = (200, 300) + film_layer.thickness.min = 200 + film_layer.thickness.max = 300 film.sld.fixed = False - film.sld.bounds = (0.1, 3) + film.sld.min = 0.1 + film.sld.max = 3 model.background.fixed = False - model.background.bounds = (1e-7, 1e-5) + model.background.min = 1e-7 + model.background.max = 1e-5 model.scale.fixed = False - model.scale.bounds = (0.5, 1.5) + model.scale.min = 0.5 + model.scale.max = 1.5 interface = CalculatorFactory() model.interface = interface @@ -204,11 +217,14 @@ def test_fitting_with_manual_zero_variance(): # Set some parameters as fittable sio2_layer.thickness.fixed = False - sio2_layer.thickness.bounds = (15, 50) + sio2_layer.thickness.min = 15 + sio2_layer.thickness.max = 50 film_layer.thickness.fixed = False - film_layer.thickness.bounds = (200, 300) + film_layer.thickness.min = 200 + film_layer.thickness.max = 300 film.sld.fixed = False - film.sld.bounds = (0.1, 3) + film.sld.min = 0.1 + film.sld.max = 3 interface = CalculatorFactory() model.interface = interface @@ -1238,10 +1254,12 @@ def test_fit_weight_convention_matches_analytic_wls(minimizer): assert scale_margin > 10 * scale_tolerance, 'test data cannot discriminate weight conventions' model.scale.fixed = False - model.scale.bounds = (0.5, 3.0) + model.scale.min = 0.5 + model.scale.max = 3.0 model.scale.value = 1.0 model.background.fixed = False - model.background.bounds = (1e-9, 1e-4) + model.background.min = 1e-9 + model.background.max = 1e-4 model.background.value = 1e-6 data = DataSet1D( diff --git a/tests/test_polarized_fitting.py b/tests/test_polarized_fitting.py index d84f536f..fad69925 100644 --- a/tests/test_polarized_fitting.py +++ b/tests/test_polarized_fitting.py @@ -431,7 +431,8 @@ def test_two_channel_nsf_fit_recovers_rho_m(self): model.interface = _refl1d_interface() rho_m = model.sample[1].layers[0].magnetism.rho_m rho_m.fixed = False - rho_m.bounds = (0.0, 5.0) + rho_m.min = 0.0 + rho_m.max = 5.0 data = _polarized_data({'pp': reference['pp'], 'mm': reference['mm']}, model=model) fitter = MultiFitter(model) @@ -450,9 +451,11 @@ def test_four_channel_fit_recovers_rho_m_and_theta_m(self): model.interface = _refl1d_interface() magnetism = model.sample[1].layers[0].magnetism magnetism.rho_m.fixed = False - magnetism.rho_m.bounds = (0.0, 5.0) + magnetism.rho_m.min = 0.0 + magnetism.rho_m.max = 5.0 magnetism.theta_m.fixed = False - magnetism.theta_m.bounds = (0.0, 90.0) + magnetism.theta_m.min = 0.0 + magnetism.theta_m.max = 90.0 data = _polarized_data(dict(reference), model=model) fitter = MultiFitter(model) @@ -473,7 +476,8 @@ def test_shared_structural_parameter_fitted_across_channels(self): thickness = model.sample[1].layers[0].thickness thickness.value = 90.0 thickness.fixed = False - thickness.bounds = (50.0, 150.0) + thickness.min = 50.0 + thickness.max = 150.0 data = _polarized_data({'pp': reference['pp'], 'mm': reference['mm']}, model=model) fitter = MultiFitter(model) @@ -612,7 +616,8 @@ def test_prepared_fitter_recovers_rho_m_when_run(self): model.interface = _refl1d_interface() rho_m = model.sample[1].layers[0].magnetism.rho_m rho_m.fixed = False - rho_m.bounds = (0.0, 5.0) + rho_m.min = 0.0 + rho_m.max = 5.0 data = _polarized_data({'pp': reference['pp'], 'mm': reference['mm']}, model=model) fitter = MultiFitter.for_experiments([data])