From 159f2d2047d1f44b3d59187a8f99b3b23e2e11fc Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 19 Mar 2026 11:24:53 +0100 Subject: [PATCH 1/2] assure refl1d actually runs from the GUI --- .../tutorials/fitting/simple_fitting.ipynb | 117 +++++++++++++++++- src/easyreflectometry/project.py | 10 ++ tests/test_project.py | 14 +++ 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/docs/src/tutorials/fitting/simple_fitting.ipynb b/docs/src/tutorials/fitting/simple_fitting.ipynb index 5dcd5eb8..ea761e75 100644 --- a/docs/src/tutorials/fitting/simple_fitting.ipynb +++ b/docs/src/tutorials/fitting/simple_fitting.ipynb @@ -30,7 +30,9 @@ "source": [ "%matplotlib inline\n", "\n", + "import matplotlib.pyplot as plt\n", "import pooch\n", + "import refl1d\n", "import refnx\n", "\n", "import easyreflectometry\n", @@ -63,7 +65,8 @@ "outputs": [], "source": [ "print(f'easyreflectometry: {easyreflectometry.__version__}')\n", - "print(f'refnx: {refnx.__version__}')" + "print(f'refnx: {refnx.__version__}')\n", + "print(f'refl1d: {refl1d.__version__}')" ] }, { @@ -395,7 +398,7 @@ "## Choosing our calculation engine\n", "\n", "The `easyreflectometry` package enables the calculation of the reflectometry profile using either [*refnx*](https://refnx.readthedocs.io/) or [*Refl1D*](https://refl1d.readthedocs.io/en/latest/).\n", - "For this tutorial, we will stick to the current default, which is *refnx*. \n", + "We will first run the fit with the current default, *refnx*, and then rebuild the same starting model with *Refl1D* to compare the results.\n", "The calculator must be created and associated with the model that we are to fit. " ] }, @@ -464,7 +467,8 @@ "metadata": {}, "outputs": [], "source": [ - "analysed = fitter.fit(data)" + "analysed_refnx = fitter.fit(data)\n", + "analysed = analysed_refnx" ] }, { @@ -513,18 +517,119 @@ "model" ] }, + { + "cell_type": "markdown", + "id": "2b2fe558", + "metadata": {}, + "source": [ + "## Repeating the optimisation with Refl1D\n", + "\n", + "To compare backends fairly, we rebuild the same initial model and then switch the calculator to `refl1d`.\n", + "This ensures that the second optimisation starts from the same parameter guesses rather than from the already-optimised `refnx` result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "341abc6f", + "metadata": {}, + "outputs": [], + "source": [ + "print(f'refl1d: {refl1d.__version__}')\n", + "\n", + "si_refl1d = Material(sld=2.07, isld=0, name='Si')\n", + "sio2_refl1d = Material(sld=3.47, isld=0, name='SiO2')\n", + "film_refl1d = Material(sld=2.0, isld=0, name='Film')\n", + "d2o_refl1d = Material(sld=6.36, isld=0, name='D2O')\n", + "\n", + "si_layer_refl1d = Layer(material=si_refl1d, thickness=0, roughness=0, name='Si layer')\n", + "sio2_layer_refl1d = Layer(material=sio2_refl1d, thickness=30, roughness=3, name='SiO2 layer')\n", + "film_layer_refl1d = Layer(material=film_refl1d, thickness=250, roughness=3, name='Film Layer')\n", + "subphase_refl1d = Layer(material=d2o_refl1d, thickness=0, roughness=3, name='D2O Subphase')\n", + "\n", + "superphase_refl1d = Multilayer([si_layer_refl1d, sio2_layer_refl1d], name='Si/SiO2 Superphase')\n", + "sample_refl1d = Sample(superphase_refl1d, Multilayer(film_layer_refl1d), Multilayer(subphase_refl1d), name='Film Structure')\n", + "\n", + "resolution_function_refl1d = PercentageFwhm(0.02)\n", + "model_refl1d = Model(\n", + " sample=sample_refl1d,\n", + " scale=1,\n", + " background=1e-6,\n", + " resolution_function=resolution_function_refl1d,\n", + " 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", + "\n", + "interface_refl1d = CalculatorFactory()\n", + "interface_refl1d.switch('refl1d')\n", + "model_refl1d.interface = interface_refl1d\n", + "\n", + "print(interface_refl1d.current_interface.name)" + ] + }, + { + "cell_type": "markdown", + "id": "2f764d79", + "metadata": {}, + "source": [ + "We can now fit the same dataset with `refl1d` and compare the fitted curve and reduced chi-squared with the earlier `refnx` result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70229cf9", + "metadata": {}, + "outputs": [], + "source": [ + "fitter_refl1d = MultiFitter(model_refl1d)\n", + "analysed_refl1d = fitter_refl1d.fit(data)\n", + "\n", + "print(f'refnx reduced chi: {analysed_refnx[\"reduced_chi\"]:.4f}')\n", + "print(f'refl1d reduced chi: {analysed_refl1d[\"reduced_chi\"]:.4f}')\n", + "\n", + "qz = data['coords']['Qz_0'].values\n", + "reflectivity = data['data']['R_0'].values\n", + "uncertainty = data['data']['R_0'].variances**0.5\n", + "\n", + "plt.figure(figsize=(8, 5))\n", + "plt.errorbar(qz, reflectivity, yerr=uncertainty, fmt='o', markersize=3, color='black', alpha=0.6, label='Data')\n", + "plt.plot(qz, analysed_refnx['R_0_model'].values, linewidth=2.5, label='refnx fit')\n", + "plt.plot(qz, analysed_refl1d['R_0_model'].values, linewidth=2.5, label='refl1d fit')\n", + "plt.yscale('log')\n", + "plt.xlabel(r'$Q_z$ (1/Å)')\n", + "plt.ylabel('Reflectivity')\n", + "plt.grid(True, which='both', alpha=0.25)\n", + "plt.legend()\n", + "plt.show()\n", + "\n", + "print('refnx model')\n", + "print(model)\n", + "print()\n", + "print('refl1d model')\n", + "print(model_refl1d)" + ] + }, { "cell_type": "markdown", "id": "df6db8c3-6515-478b-bd2e-aac967579231", "metadata": {}, "source": [ - "We note here that the results obtained are very similar to those from the [*refnx* tutorial](https://refnx.readthedocs.io/en/latest/getting_started.html#Fitting-a-neutron-reflectometry-dataset), which is hardly surprising given that we have used the *refnx* engine in this example." + "We note here that the results obtained with [*refnx*](https://refnx.readthedocs.io/en/latest/) and [*Refl1D*](https://refl1d.readthedocs.io/en/latest/) are very similar for this simple slab model, with only small backend-dependent differences in the fitted curve and optimised parameters." ] } ], "metadata": { "kernelspec": { - "display_name": "easyref", + "display_name": ".venv (3.11.9)", "language": "python", "name": "python3" }, @@ -538,7 +643,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 6f7096c1..67febcc0 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -220,7 +220,17 @@ def calculator(self) -> str: @calculator.setter def calculator(self, calculator: str) -> None: + if calculator == self._calculator.current_interface_name: + return + self._calculator.switch(calculator) + self._calculator.reset_storage() + + for model in self._models: + model.generate_bindings() + + self._fitter = None + self._fitter_model_index = None @property def minimizer(self) -> AvailableMinimizers: diff --git a/tests/test_project.py b/tests/test_project.py index b93df068..3876e4b8 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -268,6 +268,20 @@ def test_fitter_new_model_index(self): # Expect assert fitter_0 is not fitter_1 + def test_switch_calculator_rebuilds_model_bindings(self): + # When + project = Project() + project.default_model() + + # Then + project.calculator = 'refl1d' + reflectivity = project.model_data_for_model_at_index(0, np.array([0.01, 0.05, 0.1, 0.5])) + + # Expect + assert project.calculator == 'refl1d' + assert len(reflectivity.y) == 4 + assert np.all(np.isfinite(reflectivity.y)) + def test_experiments(self): # When project = Project() From 31105392f69409b7656e1b9b02a9a1090582ba2a Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 19 Mar 2026 11:31:11 +0100 Subject: [PATCH 2/2] don't fail on codecov upload --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 90967cd9..1d33313f 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -61,7 +61,7 @@ jobs: name: unit-tests-job flags: unittests files: ./coverage-unit.xml - fail_ci_if_error: true + fail_ci_if_error: false verbose: true token: ${{ secrets.CODECOV_TOKEN }} slug: EasyScience/EasyReflectometryLib