Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 111 additions & 6 deletions docs/src/tutorials/fitting/simple_fitting.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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__}')"
]
},
{
Expand Down Expand Up @@ -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. "
]
},
Expand Down Expand Up @@ -464,7 +467,8 @@
"metadata": {},
"outputs": [],
"source": [
"analysed = fitter.fit(data)"
"analysed_refnx = fitter.fit(data)\n",
"analysed = analysed_refnx"
]
},
{
Expand Down Expand Up @@ -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"
},
Expand All @@ -538,7 +643,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.9"
"version": "3.11.9"
}
},
"nbformat": 4,
Expand Down
10 changes: 10 additions & 0 deletions src/easyreflectometry/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading