diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9e5c6b..99f6e0cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Unreleased +## Parameter constraints + +- New equality-constraint helpers `constrain`, `constrain_equal`, + `unconstrain`, `constrain_to_sum` and `derived_parameter` + (`easyreflectometry.constraints`), thin wrappers over the EasyScience + parameter-dependency mechanism. Constraints created through these + helpers survive project save/load; raw `make_dependent_on` calls do + not. A standalone `derived_parameter` is session-only: it has no + structural path and a saved project cannot reference it. +- New inequality constraints + (`easyreflectometry.inequality_constraints`): declarative + `InequalitySpec` objects (`t_head < t_tail`, `t1 + t2 <= 90`) + registered on the project (`Project.add_inequality_constraint` and + friends) and enforced as penalties on the BUMPS fit problem, including + via `MultiFitter.for_experiments`-built fitters driven through the raw + `easy_science_multi_fitter.fit(...)`. Engines that cannot enforce them + (LMFit, DFO-LS) raise instead of silently dropping physics. Older + project files without the new keys load unchanged; files saved with + constraints keep the file format at 2 (old readers ignore the additive + keys and lose the constraints). +- New `Model.total_thickness`: a read-only derived parameter equal to + the summed thickness of the layers between superphase and subphase, + rebuilt whenever the layer structure changes. New + `conformal_thickness` / `conformal_roughness` toggles on assemblies. +- Structural parameter paths (`Project.parameter_path` / + `Project.resolve_parameter_path`) address parameters stably across + save/load. +- `Parameter.bounds = (lo, hi)` assignments in tutorials, notebooks and + integration tests migrated to `.min` / `.max`. +- ORSO model loading now uses the parsed `SampleModel` as-is, so named + materials, sub-stacks and composits are no longer dropped (named + materials previously read back with SLD 0). + +## Polarization + All four polarization channels (pp, pm, mp, mm) are now available from the refl1d calculator. Previously only the non-spin-flip pp channel was returned. 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/constraints.ipynb b/docs/docs/tutorials/advancedfitting/constraints.ipynb new file mode 100644 index 00000000..35c807ad --- /dev/null +++ b/docs/docs/tutorials/advancedfitting/constraints.ipynb @@ -0,0 +1,964 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c8077e80", + "metadata": {}, + "source": "# Constraints: equalities, derived parameters and inequalities\n\nReal 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 they survive saving and reloading a project (the one exception — a standalone `derived_parameter` — is called out below):\n\n1. **Equality constraints** (dependencies) — a parameter follows an expression of other parameters and leaves the fit: `constrain`, `constrain_equal`, `unconstrain`.\n2. **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.\n3. **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\nIn 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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:48.603463Z", + "iopub.status.busy": "2026-08-28T19:29:48.603463Z", + "iopub.status.idle": "2026-08-28T19:29:49.102984Z", + "shell.execute_reply": "2026-08-28T19:29:49.102984Z" + } + }, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ae11c47d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:49.104988Z", + "iopub.status.busy": "2026-08-28T19:29:49.103990Z", + "iopub.status.idle": "2026-08-28T19:29:52.252657Z", + "shell.execute_reply": "2026-08-28T19:29:52.252657Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.254699Z", + "iopub.status.busy": "2026-08-28T19:29:52.254699Z", + "iopub.status.idle": "2026-08-28T19:29:52.288764Z", + "shell.execute_reply": "2026-08-28T19:29:52.288764Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.291806Z", + "iopub.status.busy": "2026-08-28T19:29:52.290774Z", + "iopub.status.idle": "2026-08-28T19:29:52.460966Z", + "shell.execute_reply": "2026-08-28T19:29:52.460966Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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khtvvNG8b2RkpJx66qmyZMkS1+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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.463549Z", + "iopub.status.busy": "2026-08-28T19:29:52.462541Z", + "iopub.status.idle": "2026-08-28T19:29:52.470916Z", + "shell.execute_reply": "2026-08-28T19:29:52.470916Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.473238Z", + "iopub.status.busy": "2026-08-28T19:29:52.473238Z", + "iopub.status.idle": "2026-08-28T19:29:52.477248Z", + "shell.execute_reply": "2026-08-28T19:29:52.477248Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.479492Z", + "iopub.status.busy": "2026-08-28T19:29:52.478497Z", + "iopub.status.idle": "2026-08-28T19:29:52.486399Z", + "shell.execute_reply": "2026-08-28T19:29:52.486399Z" + } + }, + "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. It is **session-only**: it belongs to no model, so it has no structural path — it cannot be named in an inequality constraint, and a project whose equality constraints depend on one cannot be saved. Use a model-owned value such as `Model.total_thickness` when persistence matters." + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6d05728e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.487952Z", + "iopub.status.busy": "2026-08-28T19:29:52.487952Z", + "iopub.status.idle": "2026-08-28T19:29:52.494899Z", + "shell.execute_reply": "2026-08-28T19:29:52.494899Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.495904Z", + "iopub.status.busy": "2026-08-28T19:29:52.495904Z", + "iopub.status.idle": "2026-08-28T19:29:52.877638Z", + "shell.execute_reply": "2026-08-28T19:29:52.877638Z" + } + }, + "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+PLvXFmaViBxYS65sUu8v5sDwI9j1AstkSz9H3wiUHeomszLbZfg1i7r4YsG5R9//LHpHdfeX+0VP/744819Tz75pOkxv/POO+X7778348s1tduTxh0dHV3hc3sCa+8zn5oeXnaZsmdGyy6jwsPDD/g8+rpNe7RrQseRayq9jkXW7bFo0SKZNGmSuc87bV171T3j+nV7XXbZZSXfDU2V1qECup28L6tXr5aLLrqoVI86ApcG2TfddJO8/fbbEhUVVaXHaH0GrWHgfQFQS73p+yu9X985ruRgHYCzxIS6xJNMQ/p7+dhDwlHBrV3Wozxa+EyDZR1b/uabb5o0bk/gqWOstRjaxRdfLD179jTp7xp0emjKuQbrOubaFy1Wpzyp4koD17LLbN++vVSwXnaZ+qCBuQb5EyZMkKOPPlo6depkxsSXpdti48aNJg1fx+Rryr/HEUccIX/99Zc0btzYFNfzvmg6NIKDfhY0M0PfT63loBcdHqLvuV7XTBMA9eO7bXny654CiQ51yS1dGZsOOJUemzJFW+UI1OGY4NYu61ERTc8+//zzTequBtRaldw7ENeCZz///LNJc9dibTt27Ci5X3sbtbddC65pkK/p3fPnz5dXX33V3K8Bqo7V1cruGsBqUToNhL2dcMIJsmvXLjPOXR+vvdjTpk2TuqJpyWV7vLUHVduqPfnPPvusrFu3Tv73v//Jiy++eMDjtajY8OHDTbX5U045xVSb99DCYykpKebkhg4hWL9+vRmbfuONNx5UkTvULx3+8Mcff5T6jOh0hfr+6nUdZgGg7ukJ3If/KO5N/3fHWGkcxXcPcDKmaKscgTocEdzaZT2qmv6emppq0tp1yjCPe++91/Qq6u0aUDdt2tRMLeZNq73fdtttcv/995viaRr0e8aJa2q6Tue2cuVKk1L++OOPmwJt3vQxzz//vAnQtddeK8prAbu6ooHz4YcfXuoyduxY89paTV7b2K1bN5P2XN74ZN1emg7vKSLnoVPYaVV6naJOg3ldN11Wx6iTCh08tMaAfga8LzpcQWsg6HUA9WPuznz5aVe+SXe9/VDGpgNOR4965VwWpfYcR6erOmXQEDm69b8kKbaJ7YPbYF+P1KwdMn/Tp/LdjGlUFa8D2tt+yy23mNR4isI5g56o0tkPnn766Sotr9Oz6XAHzeDgJA1QM4Nm7JKZ2/Pkmk6x8nyfiqdIBGB/w2bvli/+zpXJfRvI6I7OGQqTXo1jirB6axWCSrAHt3ZbjwKvAmioHVr9XYcH6LzxOgyAIN05vKfXA1D35u3KM0F6mEvkTnrTAdCjXiWkvsO2wa1d1mPtjqWSX5BX7cehYjqOXqe00yEAOqYfAFA3xi4tHps+qn2MtImjjwgAY9SrgkAdtgxu7bQea7cvlojwyGo/FhXTonhacE6r3GsRPgBA7VuwO0++3ZYnoS6Ru7sxdARAMXrUK0egDlsGt3Zaj0Oa9pJw0rIBAEFo7NIM8+8l7WOkfTy96QCKJXkCdXrUy0WgDlsGt3Zaj0Oa9Kj24wEA8LdfdufLtK259KYDKD/1nR71chGow5bBrZPXAwCAQPDQ/nnTR7SLkQ70pgPwlfqezwRk5SFQdziCW3utBwAAgWDhnnz5akuuhLhE7u1GpXcApVFMrnKc3nQwrSa+Yecyxwe3dlkPAAACxUP7K71f1DZGOiaE+7s5AAIMxeQqR4+6Q+m83FpN3OnBrV3WAwCAQPH73nz5kt50AFXoUc8psiS3iPR3XwjUHUrn5dZq4k4Obu2yHgAABOLY9AvaREvnRHrTARwoIdxlTuapVHrVfSJQdyidl7sm1cTtEtzaZT0AAAgkv+7Ol88254oef9/bnUrvAHwLcbmYoq0SBOoOVZN5ue0S3NplPQAACDRjFu8z/45sHyNd6U0HUAFPoL6HHnWfCNThqODWLusBAECgmbEtV2Zuz5PwEJGxPehNB1Cx5Iji3PdUetR9IlCHY4Jbu6wHAACBxrIsuev34t70azrGSds4JhYCULUedQJ13wjU4Yjg1i7rAQBAIPp4U44s2lsgcWEuuac7ld4BVI5AvWIE6rB9cGuX9QAAIBAVui25Z3FxpffbusZJ46hQfzcJQBBN0cZc6r4RqMPWwa1d1kNTCgEACERT1mbL6oxCSYkMkVu70psOoGroUa8YgTpsG9zaZT0KiwokNy+n2o8DAKCuZRa45YGlxWPT7+kWLwn7D7wBoDIE6hWj0odDFRUVybbUdSaI9VboLpQVf8+T7LwMOaxVP8nI2Wsu1ZGRmyrLN8+TmMh4aZNyqGzdu6ba7du8Z7Vs2r1SWqd0kdiIBNm4a0W1Hm+n9fhj01wplMJqvzYAAHVt/PIM2ZrjlvZxoXJNpzh/NwdAMKa+E6j7RKDuQHl5eZKWvU9+zfm+3BRrV7jIou1zavDslpinCBPJdefLj5um1uQZ9H8ikSJrM1aYS7Wfw2brESou874BABAoNmQWyv+tyDDXJ/RuIJGhxVMtAUD1etQZ4ukLgboDRUZGSkLDJEm5cIhENWno7+agErk79sjud6eZ9w0AgEBx+2/7RGtADWwaKcNaRvm7OQCCNVCnmJxPBOoOFRoSIvEtm0ls6+b+bgoqkRUeIakhjPkDAASO2dtz5aNNORLiEnn6yAbictGbDqB6khmjXiGO/gEAAFBlBW5LbliYZq5f0zFWujUI93eTAAShpAhXyRh1Zjg6EIE6AAAAqkzHpS9LK56ObWzPBH83B0CQStpfTK7IEsksJFAvi0AdAAAAVbImo1DGLk0315/qnSgNI0P93SQAQSom1CWeGR1Jfz8QgToAAAAqpampVy9INQXkTm4WKSPaxfi7SQCCmNa28BSU20tBuQMQqAMAAKBSb67Llpnb8yQqVOSFPkkUkANQa3Op06N+IAJ1AAAAVGhTVqHctL+A3APdE+SQeCYOAnDwmEu9fATqAAAAKJfbsmTUz6myr8CSvikR8p9D4/3dJAA2Qep7+QjUAQAAUK6Jf2bK7B15EhvmkreOSZYwnTwdAGoBc6mXj0AdAAAAPi1JzZe7F+8z15/u3UA6kPIOoE5S3wnUyyJQBwAAwAHS8t1yzty9UuAWObNllFzRgSrvAGpXUkRxhs5eAvUDEKgDAADggHHpI37ca+ZNbxMbKq8eTZV3ALWPqu/lI1AHAABAKWOXpsvXW3PNVGyfDGgoKXoFAGoZqe/lI1AHAASsvLw8fzcBcJxPNuXIQ39kmOsv9U2SIxpG+LtJAOweqFP1/QAE6gCAgDF+/Hhp1qyZTJ061fx94okn+rtJgKPM2ZEnF/24x1y/oXOcjGwf6+8mAXBA6jtj1A9EoA4g6GRnZ8vw4cOlefPm8sADD/i7OahFb7zxhixcuFBeeeUV+eGHH/zdHMBRFu/NlzNn7xbt2DqrVZRM7J3o7yYBsDlS38tHoA4gKIO5iIgI+fLLL+WDDz6QP//8099NQi1p0aKFubz99tvy8MMPy549xT17AOrWX+kFcuqs3ZJeYMnxjSPk3WMbMl86gHoL1NPyLVPEEv8gUAcQdBITE6Vx48bSsWNHadCggfkb9qDvZ2FhocTGxsqzzz4rO3bs8HeTANtbnlYgA77bJTty3dIzKVw+PyFFokIJ0gHUX6CuIfq+fAJ1bwTqgM3MmzdPQkNDZejQobX2nGeeeaa0bt1aoqKizPjhkSNHytatW0sts3TpUjnuuOPMMq1atZInnnii0ufdtGmTaWdMTIwJvG+//XYTpFXmggsukO+//16SkpLkyCOPNCnwsIePPvpIwsLCzPXOnTtLWlqav5sE2Novu/PlhOm7ZHuuW3o0CJfvBqZI4v4DZwCoa5GhLonZf2KQ9PfS2BMDNvPqq6/KDTfcIHPnzj0gmK4pLeilKearVq2Sjz/+WNauXSvnnHNOyf3p6elyyimnSJs2bWTRokXy5JNPyoMPPigvvfRSuc9ZVFRkgvT8/Hz5+eefTTr7lClT5P7776+0PZoOvXr1arnjjjvMY2E/c+bM8XcTAEdUd9cgfXeeW45MDpfvT24kjZmGDUA9S2IudZ8I1AEbyczMlPfff1+uueYaEwRr4FsbbrnlFjn66KNNIN6/f3+56667ZP78+VJQUGDu1/HEGnC/9tprcthhh5ke7xtvvFEmTpxY7nN+9913smLFCnnrrbekV69eMmTIEDMmedKkSea5KqKvd8QRR5h2LF++3PTmw14mT57s7yYAtlXktuT+JfvknLl7JKfIktOaR8mskxuVVF8GgPqUvD+Lh8rvpbFHBmxEe727dOliUoYvvvhiEzhbXoU5NmzYIC6XS2bPnl3j19i7d68JlDVgDw8PL0m3HzBggCnw5jF48GDTA5+amurzefQx3bt3lyZNmpR6jPbOa/Bdkddff92sn45NP/30083fsBfvzy2A2rMpq1BOmbVbHv4jw4wJvb5zrHx+QkOJD+eQEIB/JEWQ+u4Le2XAZmnvGsCqU089Vfbt21cqhVgDaw3idUx4dd15552mwFfDhg3N2PLPP/+85L7t27eXCriV52+9z5eaPEb99ttvpsr7+eefb/7W9dUTB57efdiDnlACUHu0mvLzqzLlsC93yKzteRIb5pK3j0mWZ49Koro7gMCYok3nhkQJAnXAJrT3+pdffpELL7zQ/K0FuTSY1eDdQ6e9WrlypfTp06faz6+F3n7//XeTsq7F6i655BK/9Hpq77n2vKekpJi/TzvtNDPe/auvvqr3tgBAMJi3K89Udb/u1zTJLLSkf6MIWTSksVzUrvonbQGgtnmG3ZD6XlpxaV0AQU8Dcq2Y7l0BXQPpyMhIee655w56CjMNjPXSqVMn6dq1q6nsruPU+/XrJ02bNj1gGi3P33qfL3q7nliozmPy8vLknXfeMen0nsrgSgN1DeDPOuusg1pHBA5S34GDtyQ1X+5bnC5fbsk1f2sv+vjDE+XaTrESQtYKgEDrUSdQL4VAHbABDdDffPNNmTBhgqm+7k2D13fffVeuvvrqWns9t9tdEjgrDdbvuecek37uGbc+ffp0k2avU6j5oo959NFHZefOnWZqNs9jEhIS5NBDD/X5mC+++MIUmtOefe3V99AsAc0k8H4uBLeXX37Z300AglKB25LPN+fIc6syZc7O4sKcmtl+WfsYeaBHgrSK5dAPQKAG6pyk90bqO2ADU6dONb3MV1xxhXTr1q3U5eyzzy5Jf9+yZYspNle2J7siCxYsMD3yixcvlo0bN8qsWbNMUHzIIYeYYFtddNFFppCcvr4WgtPK8//973/l1ltvLXmeTz/91Ly2h55Q0IBc52RfsmSJfPvtt3LvvffKddddZ7IAfNFec61m37NnzwPWUce3awV52ENcXNwBt2nmxGeffSbDhg3zS5uAQA7OZ2zLlasXpErLT7bJuT/sNUG6Tk18fptoWXF6E3mlXzJBOoCArvpOj3ppBOqADWggPmjQIJ/p7RrELly40Exhpj3eOpY9Ozu7ys+thec++eQTGThwoOkh12C8R48epkidJ6DW19Wx6+vXr5fevXvLbbfdZuZDHz16dMnzaGE7fW0P7RHXEwz6rwb8WhROx70/9NBDPtuhc8Lra+j6+Co8Nnz4cKq/25R+dnWKQB3WoSeJqlto7oUXXjCfWc3W0It+3qZNm1Zn7QXqWnq+W37cmSdPLs+Q02btluQPtsrJM3fL5L+yZGeuWxpHhci93eJlw1lN5b3jGkrnxOJMJwAI5B71vRSTK8VlMRDQcbTHc9Dpp0nr2y6V2Nb/jGdGYMratFU2TZgiM6Z+beYoB+woKytLoqKiSoY0eKYBnDJlignUdbjFU089JZdffrnP3vaKfPnll+Z5O3bsaMa+v/HGG/Lkk0+aIRRV+U7plIF6MkpPNmmgD9THPOfas6RB98asQtmQVSQbMgtldUahLEktkPWZRQc8plFkiPyrVbSc0yZaTmgSKeFUcgcQJL7dmiunztotvZLC5fehpWcEspvqHFOQAwUA8Pt49AceeMAUK7zsssvkp59+MtkW3bt3N1kWF1xwgbRs2dJkjVQ3SFdnnHFGqb+1NoL2smsxRE5+oT6mRduWo0F3kekt0qrG+u8ez795btld5l8N0ivrRWkZEypHJIeboPzEJpHSIymcAnEAghLF5HwjUAcA+NXDDz9sahi0b99emjVrJjfccIOpW6BDLWqbjnP/8MMPTQ++p8ZCWVok0VMo0XP2G6hKL/iq9EL5ZU++/LonX1anF/eEa494QQ2PPRtEuKRNbJi0jQ2VtnFh0i4uVHo0CDdBecPIfwpqAkAwS4ooPsnI9GylEagDAPxKp/zT4LlFixbSoEED08OuFfy10ODgwYOrPSbdlz/++MME5rm5uaZXXk8MlDe7wLhx42Ts2LEH/ZpwRnD+zdZc+XBTjny1Jdf0iPuiRd103LgWTNL5ghtG7r8eESIpUSHSMCK0+F+9b/9tel8Y6esAHDSPekaBJYVui33ffgTqAAC/euedd2TixImmF1tnJtDp9nRsuqa9h4SEyPnnn2+WO5iAXXvndeYCHRP20UcfyahRo0xBRF/B+pgxY0rNWKA96q1atarxa8N+cgotmbQ6U55ZmSmbs/8ZL67zlGs6ep+GEdKtQbi0iyvuDW8RE8qBJwCUo0H4P/XN0/LdkhJFxpAiUAcA+FXjxo1l/PjxJX/r9HtaOE4Lvn3xxRcmaNcgXadlO+ecc8zliCOOqNZr6PSBHTp0MNd1ZoJff/3VTCE4efLkA5bV2QzKmyIQzqbFCN/dkCN3/L5PtuwP0LXn++J2MfKvVlFyTGOKuAFAdYWGuCQx3CX7CixTp4NAvRiBOgAgIIWFhZlp9/SyY8cOefPNN03Q/vjjj5ux5gdDq8h7j0MHKrMnr0iuWZBm0txV69hQeaB7glzULkaiNLcdAFBjKZEhsq+gyAwhqv0KNcGJQB0AEPCaNGkit99+u7n88ssv1XqsprIPGTJEWrduLRkZGSbVfvbs2fLtt9/WWXthL2syCuXUmbtkbWaRhLlE7u+eILcfFk+ADgC1RHvRdR9bXq0PJyJQBwAElT59+lRreS1Mp+Pdt23bZuYu7dGjhwnSTz755DprI+xj4Z58GTJrtzl41KrrHxzXUI5sGOHvZgGArTTaX1BuVy6BugeBOgDA1l599VV/NwFBauW+Ahk8c7eZMqh3crh8dWKKNIlm7CQA1EXqu9qdd3BD2+yEQB0AAKCMbdlFcuqs4iC9T8NwmTGokcR7VSYGANSeRlH7e9RJfS/BLw4AAIAXncf33B/2yMasIukYHyZTT0whSAeAOpQSWZytxBj1f/CrAwAIGCtWrJDU1FS56aab5Morr5R169b5u0lwoEf+SJefduVLQrhLvj4pRRoxVRAA1EvqO2PU/0HqOwAgYEyYMEGio6Nl8ODBkpKSIrfddpt8+umn/m4WHOTHnXny8LIMc/3FPknSIZ5DJQCor9R3xqj/g18fAEBAcblccsYZZ5jrDRs29Hdz4CAFbktGL0gVtyVySfsYubBdjL+bBACOQI/6gQjUAQABIz4+XtLT0+W5554zPep79+71d5PgIM+uzJQ/9xWaaYL+e2QDfzcHABxY9Z1A3YMx6gCAgDFu3Dhp2bKlTJs2TebNmyeTJk3yd5PgoCrvD/6Rbq6POzxRGkRwiAQA9cVTCySz0JLcIsvfzQkI9KgDAAKGjk9/6KGH/N0MONCDS9Mlo8CSoxqGy2WHkPIOAPUpMdwlYS6RQkvT34ukVSxhKlvAoYoKCyX1j9WSvW2nv5uCSuTtTjPvF+AkL7zwglxzzTX+bgYcYlNWoby+Lstcn9C7gYS4XP5uEgA4rj5Nk+hQ2ZJdJDty3dIq1t8t8j8CdQfKy8uT3Iw9svPDj/zdFFSR2x1q3jfAKX744QcCddSbccsypMAtcmKTSDmucaS/mwMAjtQ0KsQE6ttzqPyuCNQdKDIyUho2TJDrx6RIi1ZR/m4OKrFlc648N263ed8Ap7AsxqehfmzOKpRX1xb3pj/QI8HfzQEAx2oarePUC2R7DgXlFIG6Q4WGhErb9vHSoTN5JYEuPDxLQkNS/d0MoN5T4ID68NSfmaY3/fjGEXJ8E06IAoA/e9TV9lx61BUlTQEAgCNlFbrltf296XccFu/v5gCAoxX3qAup7/sRqAMAAo5O0QbUtbfXZ8u+Akvax4XKqc0ZCgYAARGo55L6rgjUAQAB54knnvB3E+CAOgjPrSruTb+uUxyV3gHAz0pS3+lRNxijDgAIOIWFhbJ8+XLZvn27+btp06Zy6KGHSnh4uL+bBpv4YWe+/JFWINGhLrnsEOq1AIC/kfpeGoE6ACBguN1uuf/++2XSpEmyb9++UvclJibK9ddfL2PHjpWQEBLCcHA8Y9MvahstSZF8ngDA30h9L41fJgBAwLjrrrvkpZdekvHjx8u6deskKyvLXPT6448/bu4bM2aMv5uJIJdd6JaPN+WY65fSmw4AAZX6nlVoSYZOx+Fw9KgDAALGm2++Kf/73/9k8ODBpW5v27atjB49Wtq0aSOXXHKJCdqBmvpsc65kFlrSLi5UjmkU4e/mAABEJC48RBLCXZJeYMmW7CLpkujsPmVnrz0AIKBkZGRI8+bNy72/WbNmpocdOBhvriv+DI1sFyMuisgBQMBoHVuc/r4pi3HqBOoAgIBxwgknyH/+8x/ZvXv3AffpbXfeeadZBqipbdlFMn17nrl+cbsYfzcHAOClVUxxwvfmbAJ1Ut8BAAHjxRdflNNOO830nHfv3l2aNGlibt+xY4f88ccfpvL71KlT/d1MBLEPN2WL2xI5OiVCOiYwiwAABGaPeqE4HYE6ACBgtGrVSpYsWSLffvutzJ8/v2R6tj59+shjjz0mp5xyChXfcVA+2V9E7vw20f5uCgCgjFYxxYH6ZnrUCdQBAIFFA/EhQ4aYC1CbduYWyQ+78s31f7UiUAeAQMMY9X/QLQEACBpaSG7u3Ln+bgaC1Oebc0zae+/kcGkTR18FAARqj/omAnUCdQBA8FizZo2ceOKJ/m4GgtQnm4vT3s9uTW86AASi1rGeYnKFYlmWOBmBOgAAsL20fLfM3F/tfThp7wAQkFrGhEqISyS3SGR7jlucjLwvAEDASE5OrvD+oiJS4VAz07bkSoFbpGtimHROpNo7AASiiFCXtIkNlfWZRfJXRqE0258K70QE6gCAgJGXlyfXXHONmZrNl40bN8rYsWPrvV0Ifl9vzTX/ntmS3nQACGSd4sNMoL46vVAGNIkUpyJQBwAEjF69epkp2kaNGuXzfp26jUAd1eW2LPlmf6A+pHmUv5sDAKhA54Rw+XZbnqzOKBAnY4w6ACBgDB06VNLS0ipMjb/kkkvqtU0Ifgv3FMjuPLckhLukf6MIfzcHAFCBTgnFfcnao+5k9KgDAALG3XffXeH92tv++uuv11t7YA9fbymu9n5ysygJ1ypFAICAD9RXOTxQp0cdAADY2rT9ae+nkfYOAEExRl2tzSyUArdzp2gjUAcAALa1K7dIft1TPM7xVAJ1AAh4rWJDzVAlnaljxT7njlMnUAcAALY1Y1ueaH9Mz6Rwae7gaX4AIFiEuFzSOzmipMaIUxGoAwAA25qxvTjtfVBT507xAwDB5siG4ebfRXvzxakI1AEAAWPdunX+bgJsxLIsmbk9z1wf1Iy0dwAIFkc29PSoE6gDAOB3PXr0kG7dupnq7wsWLPB3cxDk1mUWycasIgkPETmuMdOyAUCwOHJ/6vuS1ALJLnSLExGoAwACxu7du2XcuHGyc+dOGTZsmDRr1kyuuuoq+fLLLyU3tziFGaiqmfvT3o9OiZDYMA55ACBYtIsLlTaxoZLvFvl+f2aU0/CrBQAIGFFRUXLGGWfIK6+8Itu2bZOPP/5YGjZsKHfeeaekpKTIWWedJa+99prs2rXL301FEPCkvQ9sSto7AAQTl8slQ/bP1OGZYtNpCNQBAAH7I92/f38ZP368rFixQn7//Xc57rjjZMqUKdKyZUuZNGmSv5uIAOa2LJlVEqhTSA4Ags2Q/YH6V1tyzT7daQjUAQBBoWPHjnLbbbfJ3LlzZevWrXLKKaf4u0kIYMvSCmR3nltiw1zSZ39RIgBA8BjYLFLiw12yIavITLXpNATqAICgo+nwGrgD5Zm7s7hS8DGNIiQi1OXv5gAAqik2LEQuax9rrj+9MlOchkAdAADYzg87i3tfjmtM2jsABKvrO8dKiKt4nPpnm3PESQjUAQCA7eZP9wTqAwjUASBodUwIl9sPjTfXR/28V75zUGG5MH83AAAAoLbnT9+W45aIEJE+KYxPB4Bg9mCPBJm/K0/m7MyXwbN2myk3tfZIq9hQiQtzSXiIS8JcImEhLqmtgU5ntoySuHD/9mkTqAMAArJHdNGiRbJhwwZT/b1du3Zy+OGHm+tAZebu700/qmGERDE+HQCCWlSoS6ad1Ehu+y1NXvorS+bvzjeXurTurKYE6gAAePv+++/liiuukI0bN5qAXXmCdZ1DfcCAAf5uIgIc49MBwF6iw1zyfJ8kuadbgnyzNVf+yiiUzVmFklNkSaFbpNASKXDX3hRuUTow3s8I1AEAAWPNmjVy+umnS9++feWpp56SLl26mGBd51F/5pln5LTTTpOlS5dK+/btq/yc48aNk08++URWrlwp0dHRZm72xx9/XDp37lyn6wL/+WF/xffjGpP2DgB20iImVK7oUFwJ3u4oJgcACBhPP/20HH300TJr1iwZNmyYCaY1WB8+fLjpafcE8NUxZ84cue6662T+/Pkyffp0KSgoMHOwZ2Vl1dl6wH+2ZRfJmoxCM07xmEb0qAMAghM96gCAgDF79mzTA+6Lpr/ffPPNMmbMmGo95zfffFPq7ylTpkjjxo3NGHhfafR5eXnm4pGenl6t14N//bCr+L3rmRQuiVpNDgCAIMQvGAAgYGzatEm6d+9e7v3dunUzY9cPxr59+8y/ycnJPu/XEwWJiYkll1atWh3U66F+MT4dAGAHBOoAgICRmZkpMTEx5d6v92VnZ9f4+d1ut+mVP+aYY0zQ74v22Gsw77ls3ry5xq+H+sf4dACAHZD6DgAIKFo4bvv27T7v271790E9t45VX7Zsmfz444/lLhMZGWkuCD5p+W5ZmlpgrtOjDgAIZgTqAICAMnDgwJJp2cqOUdfbazqX+vXXXy9Tp06VuXPnSsuWLWuhpQg0v+zOF/3ktI8LlabRof5uDgAANUagDgAIGOvXr6/159Tg/oYbbpBPP/3UFKvT+dhhTwt2F6e9H51C2jsAILgRqAMAAkabNm0qvD8tLU2+/vrrSpcrm+7+zjvvyOeffy7x8fElafVaKE7nVYd9LNhTHKj3JVAHAAQ5iskBAIKGVnwfOXJktR7zwgsvmKJwJ5xwgjRr1qzk8v7779dZO1H/NHNifkmPOuPTAQDBjR51AICt+RrvDvtZl1kke/LcolOn6xzqAAAEM3rUAQSlyZMnm4JgWnhs586d/m4OgAAZn354coREhtas4CAAAIGCQB1A0MnIyJCxY8fKRx99JN27d5cJEyb4u0kA/OyftHfGpwMAgh+p7wCCjs5x3aBBA+nQoYO0aNFC3G63v5uEWvLMM89UeP+WLVvqrS0ILgt255l/+zYkUAcABD8CdcAmLr30UnnjjTdK/k5OTpajjjpKnnjiCenRo8dBPXfbtm1NEa+yrr32Wpk0aZK5npubK7fddpu89957kpeXJ4MHD5bnn39emjRpUuHY4QceeEBefvllU837mGOOMYW/OnbsWGF7IiIi5LLLLjPPrev5999/H9T6IXA89dRTlS7TunXremkLgkdukSW/pxaY61R8BwDYAanvgI2ceuqpsm3bNnOZOXOmhIWFyemnn37Qz/vrr7+WPK9epk+fbm4/99xzS5a55ZZb5Msvv5QPP/xQ5syZI1u3bpXhw4dX+Lx6EkF7UF988UVZsGCBxMbGmgBfg/7K/Pzzz2Zu7KysLFm9evVBryMCZx71qlwAb4v35kuBW6RRZIi0iwv1d3MAADho9KgDNksJb9q0qbmu/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/A67bTTzFj0xx9/3NRDiImJMTMXeCxdutTMOACUDdT7kvYOALARAnXARr755puSNHItJqeBsc5rrpXePbSX27u4XFVpyrs+Vqu9+/LUU0+Z5z377LPNCQMdH//888+XWkZ72XUqN4877rjDBN5ayVvT5o899lizDlFRUT5fQwNxLSpWNkhX+rqPPfaYGcOuPfwITg8//LAMHz7cTP2nRRHfeOMNU6jQ47XXXpNTTjnFr21E4HBblvziKSTXiEAdAGAfLksHd8JRtKLysLMGyeOTW0uHzrH+bg4qsWZVltz5703y+Wcz5LDDDvN3c4B6oSd0NFDX2gPe9u7da273Dt7rmg7J0JND2ibNFkHgWJFWIIdN3SExoS7Zd35zCQthDnUAQOCqzjEFPeoAgIDjK2tC6dAKoOy0bEc2DCdIBwDYCsXkAABAUJq/i0JyAAB7IlAHAABB3aPeNyXS300BAKBWEagDAICgk1nglj/SCsz1vvSoAwBshkAdAAAEnUV7C8RtibSMCZUWMaWLDgIAEOwI1AEAQPDOn05vOgDAhgjUAQBA0Jm/O8/827chgToAwH4I1AEAQND2qB/diEAdAGA/BOoAACCo/J1VKFtz3BLqEumdHO7v5gAAUOsI1AEAQFCZv783vXuDcIkJ41AGAGA//LoBAIDgTHunkBwAwKYI1AEAQFBZsIeK7wAAeyNQBwAAQaPAbcnCPQXmOoE6AMCuCNQBAEDQWJZWIDlFliSGu6RzQpi/mwMAQJ0gUAcAAEE3Pr1PSoSEuFz+bg4AAHWCQB0AAARdxXcKyQEA7IxAHQAABF2Pet+GBOoAAPsiUAcAAEFhb55bVqYXmusUkgMA2BmBOgAACAq/7J+WrUN8mKREhfq7OQAA1BkCdQAAEBTm7coz//ajNx0AYHME6gAAIChQSA4A4BQE6gAAIOC5LaukkFy/RgTqAAB7I1AHAAABb+W+QtlXYElMqEu6Nwj3d3MAAKhTBOoAACDgzdvfm35Uw3AJC3H5uzkAANQpAnUAABA049P7NYr0d1MAAKhzBOoAACBoKr5TSA4A4AQE6gAAIKDty3fLin2F5jqBOgDACQjUAQBAQPtlT75YItIuLlSaRIf6uzkAANQ5AnUAABDQ5u/aPz6d3nQAgEMQqAMAgKCo+H50CoXkAADOQKAOAAAClmVZMn93cSG5fo3oUQcAOAOBOgAACFir0wslNd+SqFCRnknh/m4OAAD1gkAdAAAE/PzpRyZHSHiIy9/NAQCgXhCoAwBsbe7cuXLGGWdI8+bNxeVyyWeffebvJqEG49NJewcAOAmBOgDA1rKysqRnz54yadIkfzcFNfDz/orvFJIDADhJmL8bAABAXRoyZIi5VFVeXp65eKSnp9dRy1CZvXluWZZWYK4f25gedQCAc9CjDgCAl3HjxkliYmLJpVWrVv5ukmP9tCtPLBHpkhAmjbWaHAAADkGgDgCAlzFjxsi+fftKLps3b/Z3kxxr7o7izIbjGpP2DgBwFlLfAQDwEhkZaS7wvx/2j08fQNo7AMBh6FEHAAABJ7PALYv2FAfq9KgDAJyGQB0AAATk/OmFlkjr2FBpE0cCIADAWfjlAwDYWmZmpqxZs6bk7/Xr18vixYslOTlZWrdu7de2oXw/7Cwenz6A3nQAgAMRqAMAbG3hwoVy4oknlvx96623mn9HjRolU6ZM8WPLUJG5Oz1p74xPBwA4D4E6AMDWTjjhBLEsneQLwSKvyJL5u+lRBwA4F2PUAQBAQFm0N19yi0QaRYZI5wT6FAAAzkOgDgAAAnb+dJfL5e/mAABQ7wjUAQBAQJmzv5Ac49MBAE5FoA4AAAJGfpElc3cUF5Ib2DTK380BAMAvCNQBAEBAzZ+eXWRJ46gQ6daA8ekAAGciUAcAAAFjxvZc8+/ApoxPBwA4F4E6AAAIGDO2FY9PH0TaOwDAwcgpc6jCwiJZOC9VNm/I9ndTUIkd2/LM+wUAdrcv3y2/7Ckenz6oGfOnAwCci0DdgfLy8mTHrmyZMM53kG5ZlliFhSIul4SEhpp/q8sqKhLL7RZXSIi49Dmq/QSWuIuKzL+usLAapT/aaT1CLLd53wDAzubsyJMiS6RDfJi0juUQBQDgXPwKOlBkZKQkJCVLk5PPkOjklFL3Ze/cIWu//VyikxpKu8FnSGh49afG2bH4V9n+2y/S9Ig+0qTXUdV+fFFBvqz/9kvJSd0jhwweJjGNm1T7Oey0Hmu++lgixDLvGwDY2YztnrR39ncAAGcjUHeokJAQSWjaXOKatSi5LWPLZtn0/TeS2LK1HHbh5RJWg8Bw0w+zZNcfv0v7QadJ6+NOqvbjC/PyZPm7r0lhVqYcftm1Et+iVbWfw27rEdukqYRlplf78QAQbKZtLS4kd0ozxqcDAJyNYnIoCQqXvf2q6fU9mOB245zp0ub4kw8quNVe5G4jrqhxcGu39ehw2nAqHwOwvdXpBbImo1DCQxifDgAAgTpsGdzaaj0iqp+2DwDB5ustxb3pAxpHSrxG6wAAOBi/hA5n2+DWwesBAMHoq/2B+mnNSXsHAIAx6g6WtWO7rPvuS4JbG60HAASjzAK3zNlZXEhuaAsCdQAA6FF3KJ0y7K+vPia4tdF6AECwmrk9TwrcIu3jQqVTAn0IAAAQqDtUbk62RCU3dHxwa5f1AIBg9sXfOebf01pEUTwTAAACdWdPz6bVxJ0c3NplPQAgmBW6Lfn87+Lx6f9qFe3v5gAAEBAI1B0qMjqmRtXE7RLc2mU9ACDYzdmRJ3vy3JISGWIqvgMAAAJ1x6pJaqFdglu7rAcA2MHHm4vT3s9qFS1hIaS9AwCgCNThqODWLusBAHZQ5Lbkk03Fgfo5rUl7BwDAg0Adjglu7bIeAGAXP+/Olx25bmkQ4ZITm7A/BADAg0Adjghu7bIeAGAn72/INv+e2TJaIkJJewcAwINAHbYPbu2yHgBgJ3lFlry7oTjtfUTbGH83BwCAgEKgDlsHt3ZZj4K8vGo/BgAC2VdbcmVvvltaxITKwKacvAQAwBuBOmwb3NplPbYtWiD5+QTqAOzljXVZ5t+L28VIKNXeAQAohUAdtgxu7bQeWxf+LBERkbU2Ld9nn31WK88FADW1M7dIvt6Sa65f0o60dwAAyiJQhy2DWzutR/Mj+0t4FR6/a9cuueaaa6R169YSGRkpTZs2lcGDB8tPP/1Ussy2bdtkyJAh5T7HpZdeKmeddVa12woA1fHWumwptESOTA6XQxuE+7s5AAAEnDB/NwCBwW7BrZ3WI7lDZ1m7cmmljzn77LMlPz9f3njjDWnfvr3s2LFDZs6cKXv27ClZRoP3YKPrFBER4e9mAKjFudMnrc4016/sEOvv5gAAEJDoUYctg1unrUdaWpr88MMP8vjjj8uJJ54obdq0kT59+siYMWPkzDPPrLXU94kTJ0r37t0lNjZWWrVqJddee61kZhYfcGdlZUlCQoJ89NFHpR6jr6fLZ2RkmL83b94s5513njRo0ECSk5Nl2LBhsmHDhgN69R999FFp3ry5dO7cucbtBRB4vt6aK+syiyQpwiUXtyftHQAAXwjUHc7Jwa2d1iMuLs5cNCjOq8MK8SEhIfLMM8/I8uXLTc/9rFmz5I477jD3aTB+wQUXyOuvv17qMfr3OeecI/Hx8VJQUGDS8fW6nljQtHxt96mnnmp6zj00E2DVqlUyffp0mTp1ap2tD4D698zKf3rTY8M4DAEAwBdS3x1Mq4lvX/yrY4NbO61HWFiYTJkyRa666ip58cUX5YgjjpDjjz/eBM49evSQ2nLzzTeXXG/btq088sgjcvXVV8vzzz9vbrvyyiulf//+Zix8s2bNZOfOnfL111/LjBkzzP3vv/++uN1ueeWVV0zvvieQ19712bNnyymnnFIS9OsypLwD9rIirUBmbM8TLfJ+bac4fzcHAICAxalsh9J5ubWauJODWzuth2eM+tatW+WLL74wPdQa+GrArgF8bdGAe+DAgdKiRQvTKz5y5EgzBj47O9vcr+n2hx12mOltV2+99ZZJwx8wYID5e8mSJbJmzRrzWE8WgKa/5+bmytq1a0teR9PrCdIB+3lsWfEQmGEto6RtHH0FAACUh0DdoXRebq0m7uTg1i7r4S0qKkpOPvlkue++++Tnn382470feOABqQ06jvz00083PfQff/yxLFq0SCZNmmTu805b1151z8kB7S2/7LLLSnrPdTx77969ZfHixaUuq1evlosuuqjkObRHHYC9/LmvQN7dWHxS755uCf5uDgAAAY1A3aF0Xu5mvfs6Nri1y3pU5tBDDzVF3mqDBuaatj5hwgQ5+uijpVOnTqYHv6yLL75YNm7caMayr1ixQkaNGlVyn/bw//XXX9K4cWPp0KFDqUtiYmKttBNAYHpoabq4reLe9N4NyZgBAKAiBOoOVZV5ue0a3NplPbxp+vlJJ51kUs2XLl0q69evlw8//FCeeOIJU1W9Ovbt23dAj7dWatdgWovBPfvss7Ju3Tr53//+Z8bDl5WUlCTDhw+X22+/3Yw5b9myZcl9I0aMkJSUFNMmLSan7dQU/RtvvFH+/vvvg9oGAALXH6kF8v7GHHP9wR70pgMAUBkCdTgquLXLepSlY7379u0rTz31lBkP3q1bN5P+rsXlnnvuuWo9lwbOhx9+eKnL2LFjpWfPnmZ6Np0CTp//7bfflnHjxvl8jiuuuMKkw19++eWlbo+JiZG5c+dK69atTTDftWtXs6yOUdep3QDYj2VZcv2vqWKJyDmto6VXMr3pAABUxmXpLygcRafWGnTaUDlkxFUS16yFY4LbYF2PzG1bZO3bL8uMr78yhdqCgfa233LLLSY1nqJwCHbp6elmaIZmm3BCqfreWpclI39OlehQl/x5RhNpQxE5AIBDpVfjmIJfS9gyuLXregQ6rf6uU7ONHz9e/v3vfxOkAw6XmueW//y2z1y/t3s8QToAAFVE6jtsH9zaZT2CgY6J79KlizRt2lTGjBnj7+YA8CNN2LtqQarsyHVL54Qwua1rvL+bBABA0CBQh62DW7usR7CMUHnwwQdNwbmZM2eacfMAnOvFv7Lk4005Eh4i8r/+yRIZWjxNIwAAqByBOmwb3NpmPfLzJS+neO5hAAgGP+/Kk1sWppnr43slylEpDIMBAKA6CNRhz+DWRuux5utPzPzlABAsU7EN/X635LmL50y/pSvZNQAAVBeBOmwZ3NppPXL37pGo6JhqPx4A6tvS1HwZPGuXpOVb0r9RhLxzbLK4XKS8AwBQXQTqsGVwa6f16Dj0bAkJDa32cwBAffp2a64c+90u2Zbjlm4NwmTqCSkSE8ZhBgAANcE8KbBlcGun9XCFcKALIHDlFFpy35J9MvHPTNGylyc0iZRPBjSUpEj2XQAA1BSBusPZNbi103pkbttS7ecBgLrmtix5f0OO3L80XdZkFJrbruwQK88d1YAK7wAAHCROdzuYVhO3a3Dr1PUAUL5JkyZJ27ZtJSoqSvr27Su//PKLv5sUlLZkF8kTyzOk6xc75KKf9pogvVl0iHx5QkN5+egkgnQAAGoBPeoOpfNyazXxgowMxwe3dlkPAOV7//335dZbb5UXX3zRBOlPP/20DB48WFatWiWNGzf2d/MCVnah2wTiv+0tkN/3FsjsHXmyNK2g5P7EcJfcfmi83NglTuJ1wnQAAFArCNQdSuflLsgvkJ6XXuPo4NYu6wGgYhMnTpSrrrpKLrvsMvO3BuxfffWVvPbaa3LXXXf5tW0/7syT9ZmFZny3siwx1w/4e/8NlvnP+28p9+/iJb3//uc5ddLH7EJLsgrdklVoSXaRXrdkX75btuQUmZ7z1HzPI/6h/eX9GkXI5YfEynltognQAQCoAwTqDqXzcnc882xHB7d2WQ8AFcvPz5dFixbJmDFjSm4LCQmRQYMGybx58w5YPi8vz1w80tPT67R9k1ZlynsbcyRQJYS7pFdSuByRHCFHNYyQU5pFSkoUM1EAAFCXCNQdSufljm3S1LHBrV3WA0Dldu/eLUVFRdKkSZNSt+vfK1euPGD5cePGydixY+utfT2SwmVPvtv0VHtGd+vU4y7zX/F1c5vn4iq73P5lfSxX7uPEJSEukehQl8SGeV9CJD7cJc2jQ6VFTKi0jAk1gTpzoQMAUL8I1B2qJvNy2yW4tct6AKgb2vOu49m9e9Rbtaq77+iYbgkypludPT0AAAhCBOpwVHBrl/UAUHUpKSkSGhoqO3bsKHW7/t206YGZRZGRkeYCAADgL1SAgWOCW7usB4DqiYiIkN69e8vMmTNL1enQv/v16+fXtgEAAPhCjzocEdzaZT0A1Iymso8aNUqOPPJI6dOnj5meLSsrq6QKPAAAQCAhUHew7N27Krw/a8d2+eurjyUquaG0OfFUyd27u9qvsW3RAtm68GdpfmR/Se7QWTK3banW4wvz881877l790jHoWeLKySk2s8R7OtR2fsEoHLnn3++7Nq1S+6//37Zvn279OrVS7755psDCswBAAAEApdleWZehVNs3bpVTjr5FNmXmVnuMu6iIsnNyTZTGEVGx9So4m9BXp7k5+dJRESkhNegB1s/mjrfu6aoapX6mhTAs8t6xMfEyOyZM6R58+bVfiyAg6PF5BITE2Xfvn2SkJDg7+YAAAAHHFPQo+5AGuzNmv6dpKam+rspqKKkpCSCdAAAAMAhCNQdSoM+Aj8AAAAACDxUfQcAAAAAIIAQqAMAAAAAEEAI1AEAAAAACCAE6gAAAAAABBCKyQEAUAHPLKY6pQoAAEBNeY4lqjJDOoE6AAAVyMjIMP+2atXK300BAAA2ObbQ+dQr4rKqEs4DAOBQbrdbtm7dKvHx8eJyuerlbLueFNi8ebMkJCTU+esFK7ZT1bCdqobtVHVsq6phO1WN07aTZVkmSNdpskNCKh6FTo86AAAV0B/Sli1b1vvr6gGLEw5aDhbbqWrYTlXDdqo6tlXVsJ2qxknbKbGSnnQPiskBAAAAABBACNQBAAAAAAggBOoAAASQyMhIeeCBB8y/KB/bqWrYTlXDdqo6tlXVsJ2qhu1UPorJAQAAAAAQQOhRBwAAAAAggBCoAwAAAAAQQAjUAQAAAAAIIATqAAAAAAAEEAJ1AAAAAAACCIE6AAABYMOGDXLFFVdIu3btJDo6Wg455BAzZU1+fn6p5ZYuXSrHHXecREVFSatWreSJJ54QJ5o0aZK0bdvWbIe+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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.879658Z", + "iopub.status.busy": "2026-08-28T19:29:52.878643Z", + "iopub.status.idle": "2026-08-28T19:29:52.883937Z", + "shell.execute_reply": "2026-08-28T19:29:52.883937Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.885941Z", + "iopub.status.busy": "2026-08-28T19:29:52.884943Z", + "iopub.status.idle": "2026-08-28T19:29:52.890958Z", + "shell.execute_reply": "2026-08-28T19:29:52.889950Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.892954Z", + "iopub.status.busy": "2026-08-28T19:29:52.892954Z", + "iopub.status.idle": "2026-08-28T19:29:52.900712Z", + "shell.execute_reply": "2026-08-28T19:29:52.900712Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.903718Z", + "iopub.status.busy": "2026-08-28T19:29:52.902718Z", + "iopub.status.idle": "2026-08-28T19:29:52.908755Z", + "shell.execute_reply": "2026-08-28T19:29:52.908755Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.910806Z", + "iopub.status.busy": "2026-08-28T19:29:52.910806Z", + "iopub.status.idle": "2026-08-28T19:29:52.918305Z", + "shell.execute_reply": "2026-08-28T19:29:52.918305Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:52.919629Z", + "iopub.status.busy": "2026-08-28T19:29:52.919629Z", + "iopub.status.idle": "2026-08-28T19:29:53.354475Z", + "shell.execute_reply": "2026-08-28T19:29:53.354475Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:53.357483Z", + "iopub.status.busy": "2026-08-28T19:29:53.356487Z", + "iopub.status.idle": "2026-08-28T19:29:53.361182Z", + "shell.execute_reply": "2026-08-28T19:29:53.361182Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:53.362469Z", + "iopub.status.busy": "2026-08-28T19:29:53.362469Z", + "iopub.status.idle": "2026-08-28T19:29:53.367375Z", + "shell.execute_reply": "2026-08-28T19:29:53.366784Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:53.368379Z", + "iopub.status.busy": "2026-08-28T19:29:53.368379Z", + "iopub.status.idle": "2026-08-28T19:29:54.473503Z", + "shell.execute_reply": "2026-08-28T19:29:54.472490Z" + } + }, + "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": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:54.475022Z", + "iopub.status.busy": "2026-08-28T19:29:54.475022Z", + "iopub.status.idle": "2026-08-28T19:29:54.950576Z", + "shell.execute_reply": "2026-08-28T19:29:54.950576Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9kAAAGFCAYAAADpZzBPAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAqJlJREFUeJzs3Qd4k2XXB/B/RtO9J4Wy994IDkCRoaK4Xyei4vjAhaLgQMX5OhAHigtRXycOHCAiKCBDpiDI3rO7dDf7u85dEtLSlpamTZr8f9f12IwnyZ0Qk5znnPvcGrvdbgcRERERERER1Zq29ndBRERERERERIJBNhEREREREZGbMMgmIiIiIiIichMG2URERERERERuwiCbiIiIiIiIyE0YZBMRERERERG5CYNsIiIiIiIiIjdhkE1ERERERETkJgyyiYiIiIiIiNyEQTYRERERERGRi8IFy7G/6xU4csk4WHPyUBMMsomIiIiIiIhc5LzxGZr8+i5CLz4X+V//ippgkE1EREREROQnDvS8GoXzl7nt/jIfewPp45+DrzG0aw672QK71YbATq1qdFsG2URERERERORxeV/Mx6FBY2p8u/zvFuHggBuxt9lQHL5wLEr+3lbm+uLV/+DQoFuwt+kQdf8la7ec9j7DLr8AB3tdg7wPv0PwOT1rNB4G2URERERERNQgFa/+B5kPvYKENyejxd5fEH7jJTh23URY8wrU9TKfOvWGRxB52xVosWs+Im+7HMdueATW3Pwq77fgu0XQRkfAcjhNPUZNMMgmIiIiIiLyI6bt+3Ho/Fuxt8UwHL16Aiypmepy88Fj2BN/bpkAtHw5ePHKjTh03miVNU695THYCorK3Hex4/rmpden3/dimdub9x1RQe6+9pfgQI+rkPPqx7DbbDD+sxOZE1+Fadtedd+ymQ+nnfa5FC1YjpAR5yCoVydodDpEjr4M2tAQFM4rLYmXv7pG8Yi46VJoAg3qry4hxnl9RWzFRhT+uARR469HQJtmyP9qQY1eXwbZREREREREfiTvs5+R+O6TaP7vD9AlxCLt7meqdTvr8Xyk3jQZEZIV3jMf4dddhPxvFp5yfeSd16DF7vkIv+ESFHz7m/N6W1EJjl55P4LP7YXm/3yP5J9mIH/uYuR/Ph+BXdsi7uUHYejQEi0PLFRbQJNEFWjvazWi0oDbbrMDdnu5C+0wbd2jTsrfwM6ty1wd2LmN8/qKFP7yJ2yFxQi/eijCrxmGwh//gK3EiOpikE1ERERERORHIm65DIY2zaANCULsk3ejZPkGWI6mn/Z2RQtXQJcUq7LFGr0eocPOLjNfWV2fHI+IGy4uvf7C/gg+1+X631ZBGxmOqLuugcYQoILoqDuuQsF3JwPx8mSfFnt+UX8rEjLkLBTO+1OVdEujstwPv1Ml3rb8QnW9BMvaiPAyt9FGhJ2SgXclmevg83pB3yheBdq2/CIULViB6tJXe08iIiIiIiJq8AKaJDlP6xNiVBm15VgGdPExVd7OkpoFvctt1X2lJMF+Isurrk9OKHO9vkki7MWl15sPHYNp+16VmXaQUnF947K3qYmQc3sh7tl7kDHhJVgzchA6dACCB/aGLiZSXa8NDYb1eNl1rm35BdDFRlXyHDNRvHQdEt56tHT8jRMRfE4P5H/1C8JGnV+tMTHIJiIiIiIi8iPmw6nO05aMHNiNJpW1lWBbqKA4sjT7a0nLgjao9HJ9UiwsLrdV1x9Ogy4u6uT1R9NPvf5EQCvBdGC3dmiy4N0Kx6XRnlmhdcRNI9Wmxm624ECvaxB5x9XqvKFjK+S+N6fM/sYtuxF117UV3pcqb7dakfn4G8iaMsOZDbebTLCkZ6uDEqfDcnEiIiIiIiI/kvfJjzDtPqgafGVPfQdB/bupDLQEw5J5zv/yF5VhLl6+AUWLVjlvF3LhAFiPZSLv0x9ht1hQuHCl2qfM9UfS1VJccn3R4tVlrg+V6zNykDvrezXH2W61qnEUr/hbXa+Lj4Y1PUuNq7okqDZu3qXGa83OReak1xDQtBFCLuhX+pgXn6cC/7z//Qy7yaz+WtOy1OUVyf96ASJGX4aUpR+jyR+z1Nb0r8+gi45Ewbcn559XRWO3l58lTkRERERERL7oQM+rEXHjSBT8vER1+g7q3QkJr09ylnkXLVununxLBlvmVGvCQgCzBQlvPaaul4A4Y9JrsBxMRcig3tBGRajMr/P65RuQMXl66fWD+0ATGgxtUCDiX52orpfHzHr6HZSs2VyaQW+ejKjx1yH88iEqYE699QmU/LUJsNnRZOlsdZtDZ9+ElBWfVjgvW5qpHbn4/2DeexiawACEjjgXsVPHQXciE6/G9Nc/yHzkVbVPQKsUxL/0IIL6djnlvqTD+eEhtyNl1WcwtEopc132S7NQOP9PpCz56LSvMYNsIiIiIiIiqhNHr56A4P7dET3hZvgLlosTERERERGRWxT9sQbWrOOqXDz/+0Uqsx16ScWl2b6Kjc+IiIiIiIjILYybdiDt7qmqeZq+aSO1HrehbXP4E5aLExEREREREbkJy8WJiIiIiIiI3IRBNhEREREREZGbMMgmIiIiIiIichM2PjsNm82Go0ePIjw8HBqNxtPDISIi8jrS3iU/Px/JycnQann8nmqGv7WIyNe+uxhkn4Z86KeklF2InIiIiE516NAhNGnSxNPDoAaGv7WIyNe+uxhkn4YcVXW8+BEREZ4eDhERkdfJy8tTQZLjO5OoJvhbi8j9bIXF2N95lDrdfMtcaEODPT0kv/ruYpB9Go6yJfnQ5wc/ERFR5VjqS2eCv7WI3M+mC0C4tjTUk/+vGGTX73cXJ05VYsaMGejYsSP69Onj6aEQERERERFRA8EguxLjxo3D1q1bsXbtWk8PhYiIiIiIiBoIlosTERERERH5Eo0G+pQk52mqXwyyiYh8hNVqhdls9vQwyAcFBARAp9N5ehhERFRN2pAgNNswx9PD8FsMsomIfGCdx9TUVBw/ftzTQyEfFhUVhaSkJDY3IyIiOg0G2UREDZwjwE5ISEBISAiDIHL7QZyioiKkp6er840aNfL0kIiIiLyaXwTZP//8Mx588EHYbDY88sgjuP322z09JCIit5WIOwLs2NhYTw+HfFRwcOnSLxJoy3uNpeNERN7NVmzE0UvHq9PJP74FbXCgp4fkV3w+yLZYLJgwYQL++OMPREZGolevXrj88sv5Y5SIfIJjDrZksInqkuM9Ju85BtlERF7OZoNx43bnaapfPr+E15o1a9CpUyc0btwYYWFhGDFiBBYuXOjpYRERuRVLxKmu8T1GRETkI0H2smXLMHLkSCQnJ6sv+Llz556yz4wZM9C8eXMEBQWhX79+KrB2OHr0qAqwHeT0kSNH6m38RERERERE5D+8vly8sLAQ3bp1w6233oorrrjilOu/+uorVQ4+c+ZMFWBPnz4dw4YNw44dO9S8sZoyGo1qc8jLy4M75ZpsWHkwD7syC3Fxq0i0ig9z6/0TERERNUQbN25UVYdUd+Q3bmAg5+b6hRIjIj09Bj/m9UG2lHfLVplp06Zh7NixGDNmjDovwfa8efMwa9YsTJo0SWXAXTPXcrpv376V3t8LL7yAp59+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++WZzUvrgY3R19HuKBrvbtm0z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gbVWfRPywaouh6LNe0I5wLPKdgDwzvz58+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\nEquality constraints, model-owned derived parameters (such as `total_thickness`) and inequality specs are all part of the project file. A standalone `derived_parameter` is not saved, and a constraint depending on one cannot be either.\nWe 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": 20, + "id": "91222868", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-28T19:29:54.952626Z", + "iopub.status.busy": "2026-08-28T19:29:54.951581Z", + "iopub.status.idle": "2026-08-28T19:29:55.087203Z", + "shell.execute_reply": "2026-08-28T19:29:55.086698Z" + } + }, + "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": [ + "inequalities : ['a < b', 'a + b < 90']\n", + "equality : r_A = 6 -> r_B = 9 (still 1.5x)\n", + "derived : total_thickness = 90 Å\n" + ] + } + ], + "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.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb index 1c482386..3340315b 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", @@ -383,11 +390,52 @@ "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. " + }, + { + "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": [ - "Even through only as single value (that for the d13-DSPC head thickness) was changed, all three values changed. \n", + "from easyreflectometry import constrain # noqa: E402\n", + "from easyreflectometry import unconstrain # noqa: E402\n", "\n", - "Having constructed each of the surfactant layer object and implemented the constraints, we can now build Samples and models. " - ] + "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": [] + }, + { + "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)\n", + "d13d2o.tail_layer.thickness.value = 20\n", + "d13d2o.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", @@ -440,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/basic/assemblies_library.md b/docs/docs/tutorials/basic/assemblies_library.md index da419f56..fdb7e9c5 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..72acc8a0 100644 --- a/docs/docs/tutorials/basic/model.md +++ b/docs/docs/tutorials/basic/model.md @@ -85,3 +85,96 @@ 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. +- 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() + ``` + +- Constraints applied with `constrain`, `constrain_equal` and + `constrain_to_sum` are preserved when a project is saved and reloaded. + Dependencies created by calling `make_dependent_on` directly are not, + and must be re-applied after loading. diff --git a/docs/docs/tutorials/fitting/material_solvated.ipynb b/docs/docs/tutorials/fitting/material_solvated.ipynb index e64f7f7a..64c62637 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", @@ -262,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 a04bba8f..03ec7c78 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", @@ -346,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 156a1229..10514c69 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", @@ -220,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 27d1e485..2ddaca24 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", @@ -351,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" ] }, { @@ -377,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" ] }, { @@ -549,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/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/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", 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..b126a20f --- /dev/null +++ b/docs/src/api/constraints.rst @@ -0,0 +1,86 @@ +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 expression and the structural +paths of the parameters it refers to are stored with the project and the graph +is rebuilt when it is loaded. This covers the helpers above; a dependency +created by calling ``make_dependent_on`` directly is not recorded. + +Derived (read-only) parameters +------------------------------ + +A *derived parameter* is a dependent parameter that belongs to no layer: a +live calculation that can be shown or referenced from an equality +constraint:: + + from easyreflectometry import 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) + +.. warning:: + + A standalone derived parameter is **session-only**: it has no structural + path, so it cannot be named in an inequality constraint, and a project + whose equality constraints depend on one cannot be saved + (``Project.as_dict`` raises). Numeric totals (as above) are fine — they + are embedded by value. + +For a derived value that persists and can be used in inequalities, use one +owned by the model: every :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/notebooks/polarized_fitting.ipynb b/notebooks/polarized_fitting.ipynb index 83c4cedc..a75ffd76 100644 --- a/notebooks/polarized_fitting.ipynb +++ b/notebooks/polarized_fitting.ipynb @@ -358,15 +358,18 @@ "film_layer = fit_model.sample[1].layers[0]\n", "\n", "film_layer.thickness.fixed = False\n", - "film_layer.thickness.bounds = (150, 250)\n", + "film_layer.thickness.min = 150\n", + "film_layer.thickness.max = 250\n", "film_layer.magnetism.rho_m.fixed = False\n", - "film_layer.magnetism.rho_m.bounds = (0, 8)\n", + "film_layer.magnetism.rho_m.min = 0\n", + "film_layer.magnetism.rho_m.max = 8\n", "film_layer.magnetism.theta_m.fixed = False\n", - "film_layer.magnetism.theta_m.bounds = (0, 90)\n", + "film_layer.magnetism.theta_m.min = 0\n", + "film_layer.magnetism.theta_m.max = 90\n", "\n", "print('Free parameters (start values):')\n", "for parameter in fit_model.get_fit_parameters():\n", - " print(f' {parameter.name:12s} = {float(parameter.value):8.3f} bounds={parameter.bounds}')\n", + " print(f' {parameter.name:12s} = {float(parameter.value):8.3f} bounds=[{parameter.min:g}, {parameter.max:g}]')\n", "\n", "fitter = MultiFitter(fit_model)\n", "results = fitter.fit_polarized(data)\n", diff --git a/notebooks/zero_variance_fitting.ipynb b/notebooks/zero_variance_fitting.ipynb index ea3ede96..085ab4be 100644 --- a/notebooks/zero_variance_fitting.ipynb +++ b/notebooks/zero_variance_fitting.ipynb @@ -283,23 +283,28 @@ "outputs": [], "source": [ "sio2_layer.thickness.fixed = False\n", - "sio2_layer.thickness.bounds = (15, 50)\n", + "sio2_layer.thickness.min = 15\n", + "sio2_layer.thickness.max = 50\n", "\n", "film_layer.thickness.fixed = False\n", - "film_layer.thickness.bounds = (200, 300)\n", + "film_layer.thickness.min = 200\n", + "film_layer.thickness.max = 300\n", "\n", "film.sld.fixed = False\n", - "film.sld.bounds = (0.1, 3)\n", + "film.sld.min = 0.1\n", + "film.sld.max = 3\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", "model.scale.fixed = False\n", - "model.scale.bounds = (0.5, 1.5)\n", + "model.scale.min = 0.5\n", + "model.scale.max = 1.5\n", "\n", "print('Free parameters:')\n", "for p in model.get_fit_parameters():\n", - " print(f' {p.name:20s} = {float(p.value):.4g} bounds={p.bounds}')" + " print(f' {p.name:20s} = {float(p.value):.4g} bounds=[{p.min:g}, {p.max:g}]')" ] }, { @@ -359,15 +364,20 @@ " _model = Model(_sample, 1, 1e-6, _resolution, 'Film Model')\n", "\n", " _sio2_layer.thickness.fixed = False\n", - " _sio2_layer.thickness.bounds = (15, 50)\n", + " _sio2_layer.thickness.min = 15\n", + " _sio2_layer.thickness.max = 50\n", " _film_layer.thickness.fixed = False\n", - " _film_layer.thickness.bounds = (200, 300)\n", + " _film_layer.thickness.min = 200\n", + " _film_layer.thickness.max = 300\n", " _film.sld.fixed = False\n", - " _film.sld.bounds = (0.1, 3)\n", + " _film.sld.min = 0.1\n", + " _film.sld.max = 3\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", " _model.scale.fixed = False\n", - " _model.scale.bounds = (0.5, 1.5)\n", + " _model.scale.min = 0.5\n", + " _model.scale.max = 1.5\n", "\n", " _model.interface = CalculatorFactory()\n", " return _model" diff --git a/pixi.lock b/pixi.lock index 16dba96f..e457f388 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 - 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 @@ -8313,8 +8313,9 @@ packages: - pypi: ./ name: easyreflectometry requires_dist: + - asteval - bumps - - easyscience + - easyscience @ git+https://github.com/easyscience/easyscience.git@sampler-engine-structure-280 - orsopy - plotly - pooch @@ -8358,6 +8359,55 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' +- pypi: git+https://github.com/easyscience/easyscience.git?rev=sampler-engine-structure-280#3e598107beb2de6820e0e4c0a68da8e3c9af4ae4 + name: easyscience + version: 2.5.1+dev7 + 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 +13187,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..f646fd75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = '>=3.11' dependencies = [ - 'easyscience', + 'easyscience @ git+https://github.com/easyscience/easyscience.git@sampler-engine-structure-280', 'scipp', 'refnx', 'refl1d>=1.0.0', @@ -31,6 +31,7 @@ dependencies = [ 'svglib<1.6 ; platform_system=="Linux" or sys_platform == "darwin"', 'xhtml2pdf', 'bumps', + 'asteval', 'pooch', 'plotly', ] diff --git a/src/easyreflectometry/__init__.py b/src/easyreflectometry/__init__.py index a18fee06..e27bd9cf 100644 --- a/src/easyreflectometry/__init__.py +++ b/src/easyreflectometry/__init__.py @@ -6,6 +6,12 @@ from importlib import metadata 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: @@ -14,7 +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/_bumps_constraints.py b/src/easyreflectometry/_bumps_constraints.py new file mode 100644 index 00000000..05094bac --- /dev/null +++ b/src/easyreflectometry/_bumps_constraints.py @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Back-port of the BUMPS inequality-constraints hook for cores that lack it. + +EasyScience gained a ``constraints_factory`` argument on ``Bumps.fit`` / +``DreamSampler.sample`` / ``Sampler`` that attaches inequality penalties +to the ``FitProblem``. Cores without it accept the keyword through +``**kwargs`` and silently drop it, so the penalties are never enforced. + +When the installed core is native (:data:`NATIVE`) nothing here is +installed and the library keeps passing the keyword. Otherwise +:func:`install` wraps ``build_curve_problem`` and :func:`applied` +supplies the factory for the duration of a fit, attaching the +constraints to the freshly built problem. + +``FitProblem.constraints`` is a plain list read live by +``constraints_nllf()``, and ``build_curve_problem`` returns the ``Curve`` +whose ``.pars`` is the very ``{prefixed name: BumpsParameter}`` mapping +the factory expects — so attaching after construction is equivalent to +passing ``constraints=`` to the constructor. + +Only problems built by ``build_curve_problem`` are covered. Calling +``Bumps.fit`` with a caller-supplied ``model=`` bypasses it (that branch +constructs ``FitProblem(model)`` itself) and the constraints would be +dropped; a native core raises for that combination instead. The library +never passes ``model=``, so this is reachable only by driving the core +minimizer directly. +""" + +from __future__ import annotations + +import contextlib +import contextvars +import functools +import inspect +from typing import Callable +from typing import Iterator +from typing import Optional + +from easyscience.fitting.minimizers import minimizer_bumps +from easyscience.fitting.samplers import sampler_dream + +#: Message the native core raises for non-BUMPS engines. Reproduced verbatim: +#: it is both matched by the test suite and printed in the constraints tutorial. +NON_BUMPS_ERROR = ( + "Inequality constraints (constraints_factory) require the BUMPS engine; the selected minimizer uses '{package}'." +) + +#: True when the core takes ``constraints_factory`` itself, in which case the +#: library threads the keyword through and this module stays inert. +NATIVE = 'constraints_factory' in inspect.signature(minimizer_bumps.Bumps.fit).parameters + +_active: contextvars.ContextVar[Optional[Callable]] = contextvars.ContextVar( + 'easyreflectometry_constraints_factory', default=None +) + + +def _patch(module) -> None: + """Wrap ``build_curve_problem`` in one consumer namespace.""" + original = module.build_curve_problem + if getattr(original, '_easyreflectometry_shim', False): + return + + @functools.wraps(original) + def build_curve_problem(*args, **kwargs): + problem, fit_function, curve = original(*args, **kwargs) + factory = _active.get() + if factory is not None: + # ``curve.pars`` is empty when no parameter is free; the factory + # then yields constant-only penalties, which is harmless. + problem.constraints = list(factory(dict(curve.pars))) + # Only for parity with the native core, which warns about an + # infeasible start point from ``FitProblem.__init__``. The + # penalties themselves are already live without this. + problem.model_reset() + return problem, fit_function, curve + + build_curve_problem._easyreflectometry_shim = True + module.build_curve_problem = build_curve_problem + + +def install() -> None: + """Patch the fitting and sampling entry points, unless the core is native. + + Both consumers bind ``build_curve_problem`` at import time, so each + namespace has to be patched; patching the defining module alone has no + effect. Idempotent. + """ + if NATIVE: + return + _patch(minimizer_bumps) + _patch(sampler_dream) + + +def is_applied() -> bool: + """Whether an :func:`applied` block is active in the current context. + + Lets an inner wrapper (``MultiFitter`` routes the raw + ``easy_science_multi_fitter.fit`` through the constraints machinery) + detect that an outer block already attached a factory — possibly an + explicit one that must not be overridden by re-resolving the provider. + """ + return _active.get() is not None + + +@contextlib.contextmanager +def applied(factory: Optional[Callable]) -> Iterator[None]: + """Attach `factory`'s constraints to problems built inside the block. + + A no-op when `factory` is ``None`` or the core is native (the keyword is + threaded through instead). + + Parameters + ---------- + factory : Optional[Callable] + Receives the ``{prefixed name: BumpsParameter}`` mapping of a freshly + built problem and returns the constraints to attach. + """ + if factory is None or NATIVE: + yield + return + install() + token = _active.set(factory) + try: + yield + finally: + _active.reset(token) diff --git a/src/easyreflectometry/analysis/bayesian.py b/src/easyreflectometry/analysis/bayesian.py index b5bdfb43..d6f23edf 100644 --- a/src/easyreflectometry/analysis/bayesian.py +++ b/src/easyreflectometry/analysis/bayesian.py @@ -1124,7 +1124,11 @@ def _save_parameter_state(model) -> dict: """ state = {} for param in model.get_parameters(): - state[param.unique_name] = (param.value, param.error) + # Dependent parameters (constraints, `Model.total_thickness`) are derived + # from the others: they cannot be written back, and restoring the + # parameters they follow already restores them. + if param.independent: + state[param.unique_name] = (param.value, param.error) return state diff --git a/src/easyreflectometry/constraints.py b/src/easyreflectometry/constraints.py new file mode 100644 index 00000000..3fe60ee8 --- /dev/null +++ b/src/easyreflectometry/constraints.py @@ -0,0 +1,220 @@ +# 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. + +Constraints created through this module are preserved when a project is +saved and reloaded: ``Project.as_dict`` records the expression and the +structural paths of the parameters it refers to, and ``Project.from_dict`` +re-applies them once every parameter exists again. Dependencies created by +calling ``make_dependent_on`` directly are outside that contract and are +not persisted. + +Cross-parameter *inequalities* (``t_head < t_tail``) are not dependencies +but fit penalties; see :mod:`easyreflectometry.inequality_constraints`. +""" + +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', + 'constrain_to_sum', + 'derived_parameter', + 'unconstrain', +] + +#: Marks a dependency created through this module, so ``Project`` can persist +#: user constraints without also re-applying the internal ones that assemblies +#: and materials rebuild themselves. Read together with ``independent``: see +#: :func:`easyreflectometry.project.Project._user_constraints`. +USER_CONSTRAINT_FLAG = '_easyreflectometry_user_constraint' + + +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. + """ + constrain(parameter, 'a', 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) + setattr(parameter, USER_CONSTRAINT_FLAG, True) + + +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() + if hasattr(parameter, USER_CONSTRAINT_FLAG): + delattr(parameter, USER_CONSTRAINT_FLAG) + + +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 — a live calculation to display or reuse + inside another :func:`constrain` expression. + + .. code-block:: python + + total = derived_parameter('total', 't1 + t2', t1=layer_1.thickness, t2=layer_2.thickness) + constrain(layer_3.thickness, 'T - t', T=total, t=layer_4.thickness) + + .. warning:: + + A standalone derived parameter belongs to no model, so it has no + structural path (:meth:`~easyreflectometry.Project.parameter_path` + returns ``None``): it cannot be referenced from an inequality + constraint, and a project holding a :func:`constrain` that depends + on one cannot be saved (``Project.as_dict`` raises). It is a + session-only convenience. For a derived value that must survive + save/load or appear in inequalities, use a parameter owned by the + model, such as :attr:`~easyreflectometry.model.Model.total_thickness`. + + Parameters + ---------- + name : 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)})' + constrain(parameter, expression, **dependency_map) diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index 37ba637e..91edcad4 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -2,7 +2,10 @@ # SPDX-License-Identifier: BSD-3-Clause +import contextlib +import functools import warnings +import weakref from typing import Any from typing import Callable @@ -13,6 +16,10 @@ from easyscience.fitting import Sampler from easyscience.fitting.multi_fitter import MultiFitter as EasyScienceMultiFitter +from easyreflectometry._bumps_constraints import NATIVE as _NATIVE_CONSTRAINTS +from easyreflectometry._bumps_constraints import NON_BUMPS_ERROR +from easyreflectometry._bumps_constraints import applied as _constraints_applied +from easyreflectometry._bumps_constraints import is_applied as _constraints_active from easyreflectometry.data import DataSet1D from easyreflectometry.data import PolarizedDataSet from easyreflectometry.model import Model @@ -21,6 +28,38 @@ _EPS = 1e-30 +class _ConstrainedEasyScienceMultiFitter(EasyScienceMultiFitter): + """EasyScience ``MultiFitter`` whose raw ``fit`` honours inequality constraints. + + ``fit`` is a read-only property on the base class (it builds a fresh + callable per access), so the interception lives in an override rather + than a monkey-patch. Calls that already carry an explicit + ``constraints_factory`` keyword, or run inside an active shim block (an + outer :meth:`MultiFitter._constraints` — possibly with an explicit + factory that must win), pass through untouched. + """ + + #: Weak reference to the owning :class:`MultiFitter` (weak to avoid a + #: reference cycle); ``None`` disables the interception. + _constraints_owner = None + + @property + def fit(self) -> Callable: + original = EasyScienceMultiFitter.fit.fget(self) + owner = self._constraints_owner() if self._constraints_owner is not None else None + if owner is None: + return original + + @functools.wraps(original) + def fit_with_constraints(*args, **kwargs): + if 'constraints_factory' in kwargs or _constraints_active(): + return original(*args, **kwargs) + with owner._constraints(None, fitter=self) as constraints_kwargs: + return original(*args, **kwargs, **constraints_kwargs) + + return fit_with_constraints + + def _validate_objective(objective: str) -> str: """Validate and resolve the objective string. @@ -269,7 +308,7 @@ def __init__(self, *args: Model, objective: str = 'hybrid'): self._fit_func = [_bind_fit_func(m.interface.fit_func, m.unique_name) for m in args] self._models = args - self.easy_science_multi_fitter = EasyScienceMultiFitter(args, self._fit_func) + self.easy_science_multi_fitter = self._build_easy_science_fitter(args, self._fit_func) self._fit_results: list[FitResults] | None = None self._classical_fit_metrics: list[dict] | None = None self._objective = _validate_objective(objective) @@ -278,12 +317,91 @@ 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 _build_easy_science_fitter(self, models, fit_funcs) -> EasyScienceMultiFitter: + """Build the EasyScience fitter, with its raw ``fit`` honouring constraints. + + :meth:`for_experiments` documents that the caller drives + ``easy_science_multi_fitter.fit(...)`` directly (e.g. a GUI worker + thread), which would bypass :meth:`_constraints` and silently fit an + unconstrained problem. The returned fitter routes that path through + the constraints machinery: :attr:`constraints_factory_provider` is + resolved at call time (and in the calling thread — the shim's context + variable is thread-local), and non-BUMPS engines are rejected rather + than silently dropping the constraints. + """ + fitter = _ConstrainedEasyScienceMultiFitter(models, fit_funcs) + fitter._constraints_owner = weakref.ref(self) + return fitter + + 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 + + @contextlib.contextmanager + def _constraints(self, explicit: Callable | None, fitter: EasyScienceMultiFitter | None = None): + """Yield the fit kwargs, with any inequality constraints active for the block. + + A core that takes ``constraints_factory`` itself gets it as a keyword; + otherwise the shim attaches the penalties to the BUMPS problem as it is + built and the kwargs stay empty. Either way inequality constraints are + rejected for engines that cannot enforce them, rather than dropped. + + Parameters + ---------- + explicit : Callable | None + Factory passed to the fit call, or None to use + :attr:`constraints_factory_provider`. + fitter : EasyScienceMultiFitter | None, optional + The fitter whose minimizer is about to run, when it is not this + one's — ``fit_polarized`` builds its own. By default, None. + """ + factory = self._resolve_constraints_factory(explicit) + if factory is not None: + minimizer = (fitter or self.easy_science_multi_fitter).minimizer + package = getattr(minimizer, 'package', None) + if package != 'bumps': + raise ValueError(NON_BUMPS_ERROR.format(package=package)) + if _NATIVE_CONSTRAINTS: + yield {'constraints_factory': factory} if factory is not None else {} + else: + with _constraints_applied(factory): + yield {} + + @staticmethod + def _keep_constraints_on_extend(sampler: Sampler, factory: Callable | None) -> None: + """Re-enter the constraints context around ``sampler.extend()``. + + Only needed when the shim is in use: a native core binds the factory in + ``Sampler.__init__`` and reuses it on extend. Without this a continued + chain would silently sample an unpenalised posterior. + """ + if factory is None or _NATIVE_CONSTRAINTS: + return + original = sampler.extend + + @functools.wraps(original) + def extend(*args, **kwargs): + with _constraints_applied(factory): + return original(*args, **kwargs) + + sampler.extend = extend @classmethod def for_experiments( cls, experiments: list[DataSet1D | PolarizedDataSet], objective: str = 'hybrid', + constraints_factory_provider: Callable[[], Callable | None] | None = None, ) -> 'MultiFitter': """Build a fitter for a mixed list of unpolarized and polarized experiments. @@ -299,6 +417,11 @@ def for_experiments( The resulting fitter is *not* run: the caller supplies the data arrays to ``easy_science_multi_fitter.fit(...)`` in the order given by :attr:`fit_datasets`, which lets a GUI drive it from a worker thread. + That call resolves :attr:`constraints_factory_provider` at call time, + so inequality constraints are applied on this path too — pass + ``constraints_factory_provider`` (e.g. + ``project.build_constraints_factory``) or set the attribute before + fitting; with none set, no inequality constraints are enforced. Note ---- @@ -317,6 +440,11 @@ def for_experiments( The loaded experiments, in the order they should be fitted. objective : str, optional Zero-variance handling strategy, see :meth:`__init__`. By default, 'hybrid'. + constraints_factory_provider : Callable[[], Callable | None] | None, optional + Zero-argument callable returning the ``constraints_factory`` for + the next fit, typically ``project.build_constraints_factory``; + stored as :attr:`constraints_factory_provider`. By default, None + (no inequality constraints are applied). Returns ------- @@ -357,12 +485,19 @@ def for_experiments( fit_funcs.append(_bind_fit_func(func, model.unique_name)) fitter._fit_func = fit_funcs - fitter.easy_science_multi_fitter = EasyScienceMultiFitter(models, fit_funcs) + fitter.easy_science_multi_fitter = fitter._build_easy_science_fitter(models, fit_funcs) fitter.fit_datasets = datasets fitter.fit_channels = channels + fitter.constraints_factory_provider = constraints_factory_provider 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 +509,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 +541,8 @@ 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) + with self._constraints(constraints_factory) as constraints_kwargs: + result = self.easy_science_multi_fitter.fit(x, y, weights=dy, **constraints_kwargs) self._fit_results = result self._classical_fit_metrics = [] new_data = data.copy() @@ -430,7 +570,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 +586,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 +607,8 @@ 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] + with self._constraints(constraints_factory) as constraints_kwargs: + result = self.easy_science_multi_fitter.fit(x=[x_out], y=[y_eff], weights=[weights], **constraints_kwargs)[0] self._fit_results = [result] model_curve = self._fit_func[0](x_vals) self._classical_fit_metrics = [ @@ -467,7 +616,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 @@ -488,6 +642,10 @@ def fit_polarized(self, data: PolarizedDataSet, 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 ------- @@ -545,7 +703,8 @@ 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) + with self._constraints(constraints_factory, fitter=polarized_fitter) as constraints_kwargs: + results = polarized_fitter.fit(x, y, weights=dy, **constraints_kwargs) # 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 +729,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 +747,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. @@ -650,23 +815,26 @@ def mcmc_sample( if initializer is not None: sampler_kwargs['init'] = initializer - sampler = Sampler( - self.easy_science_multi_fitter, - x=x, - y=y, - weights=dy, - ) - # Retained so the chain can be continued afterwards via ``self.sampler.extend()``. - self._sampler = sampler - results = sampler.sample( - samples=samples, - burn=burn, - thin=thin, - population=population, - sampler_kwargs=sampler_kwargs or None, - progress_callback=progress_callback, - abort_test=abort_test, - ) + # Resolved once and passed on as the explicit factory, so building it + # (which resolves every constraint's parameter paths) happens once. + factory = self._resolve_constraints_factory(constraints_factory) + with self._constraints(factory) as constraints_kwargs: + # On a native core the factory is bound to the sampler, so ``extend()`` + # keeps the same penalised posterior; otherwise the shim supplies it + # per run and `_keep_constraints_on_extend` covers the continuation. + sampler = Sampler(self.easy_science_multi_fitter, x=x, y=y, weights=dy, **constraints_kwargs) + self._keep_constraints_on_extend(sampler, factory) + # Retained so the chain can be continued afterwards via ``self.sampler.extend()``. + self._sampler = sampler + results = sampler.sample( + samples=samples, + burn=burn, + thin=thin, + population=population, + sampler_kwargs=sampler_kwargs or None, + progress_callback=progress_callback, + abort_test=abort_test, + ) return { 'draws': results.draws, 'param_names': results.param_names, diff --git a/src/easyreflectometry/inequality_constraints.py b/src/easyreflectometry/inequality_constraints.py new file mode 100644 index 00000000..8e900447 --- /dev/null +++ b/src/easyreflectometry/inequality_constraints.py @@ -0,0 +1,454 @@ +# 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 +import warnings +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 + # `paths` merges both sides, so one alias cannot mean two different + # parameters — the right side would silently win. + conflicting = sorted( + alias for alias, path in self.lhs_paths.items() if alias in self.rhs_paths and self.rhs_paths[alias] != path + ) + if conflicting: + raise ValueError( + f'Alias(es) {", ".join(repr(a) for a in conflicting)} map to different parameters on the ' + 'two sides of the inequality. Use distinct alias names per side.' + ) + + @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: + # A dependent parameter always carries its dependency expression in + # EasyScience; this is a defensive fallback for a foreign Parameter + # subclass. Freezing silently would hide a bug, so say so. + warnings.warn( + f"Dependent parameter '{parameter.name}' has no dependency expression; " + 'its current value is frozen as a constant in the inequality constraint.', + stacklevel=2, + ) + 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..0e96c0af 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -5,6 +5,8 @@ import json import logging import os +import warnings +import weakref from pathlib import Path from typing import Dict from typing import List @@ -14,6 +16,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 @@ -21,6 +24,8 @@ from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.calculators import PolarizationChannel from easyreflectometry.calculators.calculator_base import CalculatorBase +from easyreflectometry.constraints import USER_CONSTRAINT_FLAG +from easyreflectometry.constraints import constrain from easyreflectometry.data import DataSet1D from easyreflectometry.data import PolarizedDataSet from easyreflectometry.data import detect_polarization_channel @@ -28,6 +33,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 +85,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 +124,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 +356,258 @@ 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 _walk_parameters(self): + """Yield ``(structural path, parameter)`` for every parameter under the models. + + Each object is descended into once, so parent back-references cannot + recurse forever. + """ + visited: set[int] = set() + + def _walk(obj, tokens: List[str]): + if isinstance(obj, DescriptorNumberType): + yield '/'.join(tokens), obj + return + if id(obj) in visited: + return + visited.add(id(obj)) + for token, child in self._child_candidates(obj): + yield from _walk(child, tokens + [token]) + + yield from _walk(self._models, ['models']) + + 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. + """ + return next((path for path, candidate in self._walk_parameters() if candidate is parameter), None) + + 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. + + A name removes **every** spec carrying that name; use the index or + the spec object to remove a single one when names are shared. + """ + 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) + + # ----- equality constraints (parameter dependencies) ----- + + @staticmethod + def _is_user_constrained(parameter) -> bool: + """Whether `parameter` carries a constraint this project should persist. + + Both halves are needed: a constraint removed with the raw + ``make_independent()`` leaves the marker behind, and re-applying it on + load would resurrect what the user removed. + """ + return getattr(parameter, USER_CONSTRAINT_FLAG, False) and not parameter.independent + + def _user_constraints(self) -> List[dict]: + """Records describing the constraints created via :mod:`easyreflectometry.constraints`. + + Parameters are addressed by structural path rather than by EasyScience + serializer id: an id is minted lazily when a parameter first gains an + observer and deleted again when it loses its last one, so it is not a + durable handle. + + A parameter is recorded only when it is both marked *and* still + dependent. A constraint removed with the raw ``make_independent()`` + leaves the marker behind, and re-applying that on load would resurrect + something the user removed. Internal constraints carry no marker at all + — their owning class rebuilds them in its own ``from_dict``. + + Raises + ------ + ValueError + If a constrained parameter, or a live parameter it depends on, is + not reachable from the models, so no path can address it. + """ + # Which parameters are constrained is decided from `parameters`, which + # enumerates them without walking properties. The structural walk has to + # walk properties and leaves reference cycles behind (delaying collection + # of the project and its unique names), so it runs only when there is + # something to record, and only to supply the paths. Driving both from + # one list keeps a constraint from being dropped because the two + # enumerations disagree. + constrained = [parameter for parameter in self.parameters if self._is_user_constrained(parameter)] + if not constrained: + return [] + paths = {id(parameter): path for path, parameter in self._walk_parameters()} + return [self._constraint_record(parameter, paths) for parameter in constrained] + + @staticmethod + def _constraint_record(parameter, paths: dict) -> dict: + """One save record for `parameter`, addressing everything through `paths`.""" + + def _addressable(target, described_as: str) -> str: + path = paths.get(id(target)) + if path is None: + raise ValueError( + f"Cannot save the constraint on '{parameter.name}': {described_as} is not " + "reachable from the project's models. Constrain against a parameter that " + 'belongs to a model.' + ) + return path + + dependencies = {} + for alias, dependency in parameter._dependency_map.items(): + if isinstance(dependency, Parameter): + # Embedding a live parameter by value would silently turn a + # dependency into a frozen constant on load. + dependencies[alias] = {'path': _addressable(dependency, f"its dependency '{dependency.name}'")} + else: + # An object-less constant built for the expression (the explicit + # total of `constrain_to_sum`); nothing else serializes it, so it + # is embedded here. + dependencies[alias] = { + 'name': dependency.name, + 'value': float(dependency.value), + 'unit': str(dependency.unit), + } + return { + 'target': _addressable(parameter, 'the parameter itself'), + 'expression': parameter._clean_dependency_string, + 'dependencies': dependencies, + } + + def _restore_user_constraints(self, records: List[dict]) -> None: + """Re-apply the constraint records written by :meth:`_user_constraints`. + + Records are applied in the order they were written. A chain + (``a`` follows ``b`` follows ``c``) resolves whichever order it is + restored in, because re-constraining a parameter propagates the new + value to anything already following it. + """ + for record in records: + dependencies = {} + for alias, reference in record['dependencies'].items(): + if 'path' not in reference: + dependencies[alias] = DescriptorNumberType(**reference) + continue + try: + dependencies[alias] = self.resolve_parameter_path(reference['path']) + except KeyError as error: + raise KeyError( + f'Cannot restore the constraint on {record["target"]!r}: its dependency ' + f'{reference["path"]!r} does not exist in this project.' + ) from error + constrain(self.resolve_parameter_path(record['target']), record['expression'], **dependencies) + @property def calculator(self) -> str: """Calculator function.""" @@ -508,7 +785,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 +1652,11 @@ 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] + parameter_constraints = self._user_constraints() + if parameter_constraints: + project_dict['parameter_constraints'] = parameter_constraints return project_dict def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict): @@ -1468,8 +1753,48 @@ def from_dict(self, project_dict: dict): else: self._experiments = {} - # Resolve any pending parameter dependencies (constraints) after all objects are loaded + # Resolve any pending parameter dependencies parked by the core + # deserializer. Only cores that serialize nested dependencies produce + # them; on the others this is a no-op safety net and `parameter_constraints` + # below carries the user constraints instead. resolve_all_parameter_dependencies(self) + self._restore_user_constraints(project_dict.get('parameter_constraints', [])) + self._warn_on_unreadable_dependencies(project_dict.get('models')) + # 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', [])] + + @staticmethod + def _warn_on_unreadable_dependencies(models_dict) -> None: + """Warn about embedded dependencies this build cannot restore. + + A core that serializes dependencies inside each nested parameter writes + ``_dependency_string`` there. Cores without that feature drop the field + silently on load, so such a file would lose its equality constraints + with no signal at all. Detect it and say so; the constraints have to be + re-applied by hand. + + The field is written for internal dependencies too (material mixtures, + conformal roughness, ``Model.total_thickness``), and those are rebuilt + by their owning class regardless — so its presence does not prove + anything was actually lost. The wording is hedged accordingly. + """ + + def _contains_dependency(node) -> bool: + if isinstance(node, dict): + return '_dependency_string' in node or any(_contains_dependency(v) for v in node.values()) + if isinstance(node, (list, tuple)): + return any(_contains_dependency(item) for item in node) + return False + + if _contains_dependency(models_dict): + warnings.warn( + 'This project was saved by a build that stores parameter dependencies inside ' + 'each parameter, which this build cannot restore. Internal constraints are ' + 'rebuilt automatically, but any custom equality constraints have been dropped ' + 'and must be re-applied.', + stacklevel=2, + ) 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..36bd7ce4 100644 --- a/src/easyreflectometry/utils.py +++ b/src/easyreflectometry/utils.py @@ -80,15 +80,29 @@ 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: - """Count parameter user constraints.""" - return sum(1 for parameter in project.parameters if not parameter.independent) + """Count the constraints created via :mod:`easyreflectometry.constraints`. + + Counts only parameters that are both marked as user-constrained and still + dependent — the same test ``Project`` uses to decide what to persist. + Internal dependencies (``Model.total_thickness``, conformal assembly ties, + material mixtures) are not user constraints and are not counted. + """ + from easyreflectometry.constraints import USER_CONSTRAINT_FLAG + + return sum( + 1 for parameter in project.parameters if getattr(parameter, USER_CONSTRAINT_FLAG, False) and not parameter.independent + ) 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/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_bayesian.py b/tests/test_bayesian.py index 6d0c3148..38154d1b 100644 --- a/tests/test_bayesian.py +++ b/tests/test_bayesian.py @@ -191,10 +191,11 @@ def test_save_and_restore(self): # Use simple objects that support attribute assignment class MockParam: - def __init__(self, unique_name, raw_value, error): + def __init__(self, unique_name, raw_value, error, independent=True): self.unique_name = unique_name self.value = raw_value self.error = error + self.independent = independent param1 = MockParam('param_a', 1.5, 0.1) param2 = MockParam('param_b', 3.0, 0.2) @@ -221,6 +222,37 @@ def get_parameters(self): assert param2.value == 3.0 assert param2.error == 0.2 + def test_dependent_parameters_are_left_alone(self): + """Derived parameters cannot be written back, and do not need to be.""" + from easyreflectometry.analysis.bayesian import _restore_parameter_state + from easyreflectometry.analysis.bayesian import _save_parameter_state + + class MockParam: + def __init__(self, unique_name, independent): + self.unique_name = unique_name + self.value = 1.0 + self.error = 0.1 + self.independent = independent + + def __setattr__(self, name, value): + if name in ('value', 'error') and not self.__dict__.get('independent', True): + raise AttributeError(f'This is a dependent parameter, its {name} cannot be set directly.') + super().__setattr__(name, value) + + free = MockParam('free', True) + derived = MockParam('derived', False) + + class MockModel: + def get_parameters(self): + return [free, derived] + + model = MockModel() + state = _save_parameter_state(model) + + assert 'derived' not in state + _restore_parameter_state(model, state) # would raise if it wrote to `derived` + assert free.value == 1.0 + class TestApplyDraw: def test_apply_draw_updates_parameters(self): 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_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_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]) 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/test_utils.py b/tests/test_utils.py index 4d9c4828..782807b6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,8 +2,11 @@ # SPDX-License-Identifier: BSD-3-Clause from easyreflectometry import Project +from easyreflectometry.constraints import constrain +from easyreflectometry.constraints import unconstrain from easyreflectometry.utils import count_fixed_parameters from easyreflectometry.utils import count_free_parameters +from easyreflectometry.utils import count_parameter_user_constraints def test_count_free_parameters(): @@ -30,3 +33,20 @@ def test_count_fixed_parameters(): # Expect assert count == 13 + + +def test_count_parameter_user_constraints_counts_only_user_constraints(): + # When + project = Project() + project.default_model() + sample = project.models[0].sample + follower = sample[2].layers[0].thickness + + # Then / Expect: internal dependents (e.g. total_thickness) are not counted + assert count_parameter_user_constraints(project) == 0 + + constrain(follower, '2 * t', t=sample[1].layers[0].thickness) + assert count_parameter_user_constraints(project) == 1 + + unconstrain(follower) + assert count_parameter_user_constraints(project) == 0 diff --git a/tests/unit/test_bumps_constraints_shim.py b/tests/unit/test_bumps_constraints_shim.py new file mode 100644 index 00000000..f66177b2 --- /dev/null +++ b/tests/unit/test_bumps_constraints_shim.py @@ -0,0 +1,215 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +""" +Tests for the BUMPS inequality-constraints back-port +""" + +import numpy as np +import pytest +from easyscience import global_object +from easyscience.fitting import AvailableMinimizers +from easyscience.fitting.minimizers import minimizer_bumps +from easyscience.fitting.samplers import sampler_dream + +from easyreflectometry import _bumps_constraints +from easyreflectometry.data import DataSet1D +from easyreflectometry.inequality_constraints import InequalitySpec +from easyreflectometry.model import Model +from easyreflectometry.project import Project +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 _two_layer_project(): + air = Material(0.0, 0.0, 'Air') + film = Material(4.0, 0.0, 'Film') + substrate = Material(2.047, 0.0, 'Si') + sample = Sample( + Multilayer(Layer(air, 0.0, 0.0, 'Superphase')), + Multilayer(Layer(film, 40.0, 3.0, 'A')), + Multilayer(Layer(film, 40.0, 3.0, 'B')), + Multilayer(Layer(substrate, 0.0, 3.0, 'Subphase')), + ) + model = Model(sample=sample) + project = Project() + project.default_model() + project.models[0] = model + model.interface = project._calculator + return project, model + + +@pytest.fixture +def unpatched_modules(): + """`install()` mutates the core modules and has no undo; keep these tests independent. + + Unwraps a shim left behind by an earlier test so the assertions start from + the core's own builder, and puts that state back afterwards. + """ + originals = [] + for module in (minimizer_bumps, sampler_dream): + builder = module.build_curve_problem + while getattr(builder, '_easyreflectometry_shim', False): + builder = builder.__wrapped__ + originals.append((module, builder)) + module.build_curve_problem = builder + yield + for module, builder in originals: + module.build_curve_problem = builder + + +@pytest.mark.usefixtures('unpatched_modules') +class TestInstall: + def test_no_op_on_a_native_core(self, monkeypatch): + """A core that takes the keyword itself must not be patched.""" + monkeypatch.setattr(_bumps_constraints, 'NATIVE', True) + original = minimizer_bumps.build_curve_problem + _bumps_constraints.install() + assert minimizer_bumps.build_curve_problem is original + + def test_patches_both_consumer_namespaces_and_is_idempotent(self, monkeypatch): + """Both consumers bind the name at import, so each has to be patched.""" + monkeypatch.setattr(_bumps_constraints, 'NATIVE', False) + monkeypatch.setattr(minimizer_bumps, 'build_curve_problem', minimizer_bumps.build_curve_problem) + monkeypatch.setattr(sampler_dream, 'build_curve_problem', sampler_dream.build_curve_problem) + + _bumps_constraints.install() + patched = (minimizer_bumps.build_curve_problem, sampler_dream.build_curve_problem) + assert all(getattr(function, '_easyreflectometry_shim', False) for function in patched) + + _bumps_constraints.install() + assert (minimizer_bumps.build_curve_problem, sampler_dream.build_curve_problem) == patched + + +@pytest.mark.skipif(_bumps_constraints.NATIVE, reason='the core enforces the constraints itself') +class TestShimAgainstARealFit: + def test_the_patched_entry_point_is_the_one_a_fit_calls(self): + """A signature check would not catch patching the wrong namespace.""" + project, model = _two_layer_project() + project.minimizer = AvailableMinimizers.Bumps + layers = [layer for assembly in model.sample for layer in assembly.layers] + q = np.linspace(0.01, 0.3, 50) + reflectivity = model.interface.fit_func(q, model.unique_name) + for layer in layers: + for parameter in (layer.thickness, layer.roughness, layer.material.sld, layer.material.isld): + parameter.fixed = True + layers[1].thickness.fixed = False + + calls = [] + + def factory(bumps_parameters): + calls.append(dict(bumps_parameters)) + return [] + + dataset = DataSet1D(name='sim', x=q, y=reflectivity, ye=(0.05 * reflectivity) ** 2) + project.fitter.fit_single_data_set_1d(dataset, constraints_factory=factory) + + assert len(calls) == 1 + assert any(name.startswith('p') for name in calls[0]) + + def test_infeasible_start_point_warns(self): + """This is what `model_reset()` in the shim buys; without it there is no warning.""" + project, model = _two_layer_project() + project.minimizer = AvailableMinimizers.Bumps + layers = [layer for assembly in model.sample for layer in assembly.layers] + thickness_a, thickness_b = layers[1].thickness, layers[2].thickness + q = np.linspace(0.01, 0.3, 50) + reflectivity = model.interface.fit_func(q, model.unique_name) + for layer in layers: + for parameter in (layer.thickness, layer.roughness, layer.material.sld, layer.material.isld): + parameter.fixed = True + thickness_a.fixed = thickness_b.fixed = False + model.scale.fixed = model.background.fixed = True + thickness_a.value, thickness_b.value = 40.0, 60.0 # sum 100, outside the budget + + paths = (project.parameter_path(thickness_a), project.parameter_path(thickness_b)) + project.add_inequality_constraint(InequalitySpec('a + b', '<', '90', {'a': paths[0], 'b': paths[1]}, {}, name='budget')) + dataset = DataSet1D(name='sim', x=q, y=reflectivity, ye=(0.05 * reflectivity) ** 2) + + with pytest.warns(UserWarning, match=r'Unsatisfied constraints: \[budget fails\]'): + project.fitter.fit_single_data_set_1d(dataset) + + def test_tolerates_a_model_with_no_free_parameters(self): + """`curve.pars` is empty then; the shim must not be the thing that breaks.""" + project, model = _two_layer_project() + project.minimizer = AvailableMinimizers.Bumps + for parameter in project.parameters: + if parameter.independent: + parameter.fixed = True + + seen = [] + q = np.linspace(0.01, 0.3, 50) + reflectivity = model.interface.fit_func(q, model.unique_name) + dataset = DataSet1D(name='sim', x=q, y=reflectivity, ye=(0.05 * reflectivity) ** 2) + + with pytest.raises(Exception) as error: + project.fitter.fit_single_data_set_1d(dataset, constraints_factory=lambda pars: seen.append(dict(pars)) or []) + + assert seen == [{}] + # Whatever bumps does with an empty problem, it is not an AttributeError + # from the shim reaching into a Curve that has no parameters. + assert not isinstance(error.value, AttributeError) + + +@pytest.mark.skipif(_bumps_constraints.NATIVE, reason='the core binds the factory to the Sampler') +class TestExtendKeepsThePenalty: + def test_extend_re_enters_the_constraints_context(self): + """Without the wrapper a continued chain samples an unpenalised posterior.""" + active = [] + + class FakeSampler: + def extend(self, **kwargs): + active.append(_bumps_constraints._active.get()) + return 'extended' + + sampler = FakeSampler() + from easyreflectometry.fitting import MultiFitter + + def factory(bumps_parameters): + return [] + + MultiFitter._keep_constraints_on_extend(sampler, factory) + assert sampler.extend(additional_samples=10) == 'extended' + assert active == [factory] + # The context is released again afterwards. + assert _bumps_constraints._active.get() is None + + def test_no_wrapper_without_constraints(self): + from easyreflectometry.fitting import MultiFitter + + class FakeSampler: + def extend(self): + return None + + sampler = FakeSampler() + MultiFitter._keep_constraints_on_extend(sampler, None) + # Nothing shadowed the class's own method. + assert 'extend' not in sampler.__dict__ + + +class TestEngineRejection: + def test_non_bumps_engine_is_rejected_with_the_core_message(self): + """The message is also printed in the constraints tutorial; keep it verbatim.""" + project, model = _two_layer_project() + project.minimizer = AvailableMinimizers.LMFit + q = np.linspace(0.01, 0.3, 50) + reflectivity = model.interface.fit_func(q, model.unique_name) + dataset = DataSet1D(name='sim', x=q, y=reflectivity, ye=(0.05 * reflectivity) ** 2) + path = project.parameter_path([lay for a in model.sample for lay in a.layers][1].thickness) + project.add_inequality_constraint(InequalitySpec('a', '<', '90', {'a': path}, {})) + + with pytest.raises(ValueError) as error: + project.fitter.fit_single_data_set_1d(dataset) + + assert str(error.value) == ( + "Inequality constraints (constraints_factory) require the BUMPS engine; the selected minimizer uses 'lmfit'." + ) diff --git a/tests/unit/test_constraints.py b/tests/unit/test_constraints.py new file mode 100644 index 00000000..1663f3d5 --- /dev/null +++ b/tests/unit/test_constraints.py @@ -0,0 +1,305 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +""" +Tests for the user-facing constraint helpers +""" + +import json + +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 constrain_to_sum +from easyreflectometry import derived_parameter +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: + 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() + 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 = 60.0 # within the default [50, 200] thickness limits + assert follower.value == 120.0 + + def test_constrain_to_sum_with_numeric_total_survives(self): + """The object-less constant built for an explicit total is embedded by value.""" + source = Project() + source.default_model() + sample = source.models[0].sample + constrain_to_sum( + sample[2].layers[0].roughness, + [sample[1].layers[0].roughness, sample[2].layers[0].roughness], + total=10.0, + ) + project_dict = json.loads(json.dumps(source.as_dict())) + global_object.map._clear() + + project = Project() + project.from_dict(project_dict) + sample = project.models[0].sample + project.models[0].sample[1].layers[0].roughness.value = 4.0 + assert sample[2].layers[0].roughness.value == 6.0 + + def test_constraint_against_a_derived_parameter_survives(self): + """`Model.total_thickness` is reachable by path, so it can be a dependency.""" + source = Project() + source.default_model() + model = source.models[0] + constrain(model.sample[1].layers[0].roughness, 'total / 100', total=model.total_thickness) + project_dict = json.loads(json.dumps(source.as_dict())) + global_object.map._clear() + + project = Project() + project.from_dict(project_dict) + model = project.models[0] + roughness = model.sample[1].layers[0].roughness + assert roughness.independent is False + assert roughness.value == model.total_thickness.value / 100 + + def test_unconstrain_does_not_resurrect_on_reload(self): + source = Project() + source.default_model() + sample = source.models[0].sample + follower = sample[2].layers[0].thickness + constrain(follower, '2 * t', t=sample[1].layers[0].thickness) + unconstrain(follower) + + project_dict = json.loads(json.dumps(source.as_dict())) + assert 'parameter_constraints' not in project_dict + global_object.map._clear() + + project = Project() + project.from_dict(project_dict) + assert project.models[0].sample[2].layers[0].thickness.independent is True + + def test_raw_make_independent_does_not_resurrect_either(self): + """The marker alone is not enough: the parameter must still be dependent.""" + source = Project() + source.default_model() + sample = source.models[0].sample + follower = sample[2].layers[0].thickness + constrain(follower, '2 * t', t=sample[1].layers[0].thickness) + follower.make_independent() # bypasses `unconstrain`, so the marker survives + + assert 'parameter_constraints' not in source.as_dict() + + def test_unreachable_dependency_raises_rather_than_freezing(self): + """Embedding a live parameter by value would silently kill the dependency.""" + project = Project() + project.default_model() + detached = Parameter('detached', 5.0, unit='angstrom') + constrain(project.models[0].sample[1].layers[0].roughness, 'a', a=detached) + + with pytest.raises(ValueError, match='not reachable from'): + project.as_dict() + + def test_standalone_derived_parameter_is_session_only(self): + """A `derived_parameter` belongs to no model: it has no path, and a + constraint depending on it cannot be saved (documented limitation).""" + project = Project() + project.default_model() + sample = project.models[0].sample + total = derived_parameter('total', 'a + b', a=sample[1].layers[0].thickness, b=sample[2].layers[0].thickness) + + assert project.parameter_path(total) is None + constrain(sample[2].layers[0].roughness, 'T / 10', T=total) + with pytest.raises(ValueError, match='not reachable from'): + project.as_dict() + + def test_warns_when_dependencies_are_embedded_in_the_parameters(self): + """A file from a core that serializes dependencies in-place cannot be restored.""" + project = Project() + project.default_model() + project_dict = project.as_dict() + # Mimic the shape such a core writes for a dependent nested parameter. + project_dict['models']['data'][0]['scale']['_dependency_string'] = 'a' + global_object.map._clear() + + with pytest.warns(UserWarning, match='must be re-applied'): + Project().from_dict(project_dict) + + def test_chained_constraints_survive_and_still_follow(self): + """Records are restored in tree order, which need not be dependency order.""" + source = Project() + source.default_model() + sample = source.models[0].sample + root = sample[1].layers[0].roughness + middle = sample[2].layers[0].roughness + leaf = sample[2].layers[0].thickness + # leaf <- middle <- root, i.e. the chain runs against the tree order. + constrain(middle, '2 * r', r=root) + constrain(leaf, '10 * m', m=middle) + + project_dict = json.loads(json.dumps(source.as_dict())) + global_object.map._clear() + + project = Project() + project.from_dict(project_dict) + sample = project.models[0].sample + root = sample[1].layers[0].roughness + middle = sample[2].layers[0].roughness + leaf = sample[2].layers[0].thickness + + assert middle.independent is False + assert leaf.independent is False + root.value = 3.0 + assert middle.value == 6.0 + assert leaf.value == 60.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..94988cf1 --- /dev/null +++ b/tests/unit/test_inequality_constraints.py @@ -0,0 +1,317 @@ +# 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.fitting import MultiFitter +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' + + def test_rejects_alias_mapped_to_different_paths_on_the_two_sides(self): + # `paths` merges both sides; without the check the right side would silently win. + with pytest.raises(ValueError, match='different parameters'): + InequalitySpec('a', '<', 'a + b', {'a': 'p/x'}, {'a': 'p/y', 'b': 'p/z'}) + # The same alias for the same parameter on both sides is fine. + spec = InequalitySpec('a', '<', 'a + b', {'a': 'p/x'}, {'a': 'p/x', 'b': 'p/z'}) + assert spec.paths == {'a': 'p/x', 'b': 'p/z'} + + +# --------------------------------------------------------------------------- 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) + + def test_for_experiments_raw_fit_applies_project_inequalities(self): + """The documented GUI path — `for_experiments` then driving the raw + `easy_science_multi_fitter.fit(...)` — must apply the project's + inequality constraints (and refuse non-BUMPS engines) instead of + silently fitting an unconstrained problem.""" + project, model = _two_layer_project() + 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')) + dataset = DataSet1D(name='sim', x=q, y=r_true, ye=(0.05 * r_true) ** 2, model=model, auto_background=False) + + fitter = MultiFitter.for_experiments([dataset], constraints_factory_provider=project.build_constraints_factory) + fitter.easy_science_multi_fitter.switch_minimizer(AvailableMinimizers.Bumps) + weights = 1.0 / np.sqrt(np.asarray(dataset.ye)) + results = fitter.easy_science_multi_fitter.fit([np.asarray(dataset.x)], [np.asarray(dataset.y)], weights=[weights]) + + assert results[0].success + assert t_a.value + t_b.value <= 90.0 + 1e-3 # unconstrained optimum (100) is refused + + fitter.easy_science_multi_fitter.switch_minimizer(AvailableMinimizers.LMFit) + with pytest.raises(ValueError, match='require the BUMPS engine'): + fitter.easy_science_multi_fitter.fit([np.asarray(dataset.x)], [np.asarray(dataset.y)], weights=[weights])