From 21c958d3b5bce2dd183fc0a961e5b0d6e8dd683b Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:39:08 +0200 Subject: [PATCH 01/11] 180 extract functionality from sample in easyreflectometryappproxiesmodel to lib (#183) * renaming from item to assembly * collections are together in folder * moving responsibilities from model to sample * rename experiment to model * test for sample * ruff * get classes from module * add Multilayer assembly if none * cleaning of sample * experiment to model and new sample structure * Update src/easyreflectometry/sample/collections/sample.py * update jupyter --- docs/src/tutorials/fitting/repeating.ipynb | 21 +- .../tutorials/fitting/simple_fitting.ipynb | 32 +- docs/src/tutorials/magnetism.ipynb | 20 +- .../tutorials/sample/material_solvated.ipynb | 42 +-- docs/src/tutorials/sample/monolayer.ipynb | 31 +- .../src/tutorials/sample/multi_contrast.ipynb | 45 +-- .../sample/resolution_functions.ipynb | 22 +- .../calculators/bornagain/calculator.py | 2 +- .../calculators/calculator_base.py | 2 +- .../calculators/refl1d/wrapper.py | 2 +- .../calculators/refnx/wrapper.py | 2 +- .../calculators/wrapper_base.py | 4 +- src/easyreflectometry/fitting.py | 2 +- .../{experiment => model}/__init__.py | 0 .../{experiment => model}/model.py | 46 +-- .../{experiment => model}/model_collection.py | 4 +- .../resolution_functions.py | 0 src/easyreflectometry/sample/__init__.py | 6 +- .../sample/assemblies/base_assembly.py | 2 +- .../sample/assemblies/gradient_layer.py | 2 +- .../sample/assemblies/multilayer.py | 7 +- .../sample/assemblies/repeating_multilayer.py | 4 +- .../sample/assemblies/surfactant_layer.py | 2 +- .../base_element_collection.py | 0 .../layer_collection.py | 4 +- .../material_collection.py | 8 +- .../sample/collections/sample.py | 166 ++++++++++ src/easyreflectometry/sample/sample.py | 77 ----- tests/calculators/refnx/test_refnx_wrapper.py | 4 +- tests/{experiment => model}/test_model.py | 80 ++--- .../test_model_collection.py | 4 +- .../test_resolution_functions.py | 8 +- tests/sample/assemblies/test_multilayer.py | 2 +- .../assemblies/test_repeating_multilayer.py | 2 +- .../test_layer_collection.py | 2 +- .../test_material_collection.py | 2 +- tests/sample/collections/test_sample.py | 305 ++++++++++++++++++ tests/sample/test_sample.py | 83 ----- tests/test_fitting.py | 23 +- tests/test_topmost_nesting.py | 10 +- 40 files changed, 687 insertions(+), 393 deletions(-) rename src/easyreflectometry/{experiment => model}/__init__.py (100%) rename src/easyreflectometry/{experiment => model}/model.py (82%) rename src/easyreflectometry/{experiment => model}/model_collection.py (92%) rename src/easyreflectometry/{experiment => model}/resolution_functions.py (100%) rename src/easyreflectometry/sample/{ => collections}/base_element_collection.py (100%) rename src/easyreflectometry/sample/{elements/layers => collections}/layer_collection.py (81%) rename src/easyreflectometry/sample/{elements/materials => collections}/material_collection.py (81%) create mode 100644 src/easyreflectometry/sample/collections/sample.py delete mode 100644 src/easyreflectometry/sample/sample.py rename tests/{experiment => model}/test_model.py (92%) rename tests/{experiment => model}/test_model_collection.py (95%) rename tests/{experiment => model}/test_resolution_functions.py (88%) rename tests/sample/{elements/layers => collections}/test_layer_collection.py (97%) rename tests/sample/{elements/materials => collections}/test_material_collection.py (95%) create mode 100644 tests/sample/collections/test_sample.py delete mode 100644 tests/sample/test_sample.py diff --git a/docs/src/tutorials/fitting/repeating.ipynb b/docs/src/tutorials/fitting/repeating.ipynb index 074fed86..f31f5d13 100644 --- a/docs/src/tutorials/fitting/repeating.ipynb +++ b/docs/src/tutorials/fitting/repeating.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "29d5d62d-af4a-416d-bbe2-1338d32b30f5", "metadata": {}, "outputs": [], @@ -45,8 +45,9 @@ "from easyreflectometry.sample import Sample\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import RepeatingMultilayer\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.sample import Multilayer\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.fitting import Fitter\n", "from easyreflectometry.plot import plot\n", @@ -87,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "7121c7e9", "metadata": {}, "outputs": [], @@ -141,7 +142,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "0f95d620-35b7-4b47-a3b4-9e33d5525b50", "metadata": {}, "outputs": [], @@ -154,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "9a0b37ed-8714-4614-b49f-1e86ac232ac1", "metadata": {}, "outputs": [], @@ -195,13 +196,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "fed8d60f-a4a7-40f1-8063-eb975bfa6115", "metadata": {}, "outputs": [], "source": [ "resolution_function = PercentageFhwm(0)\n", - "sample = Sample(superphase, rep_multilayer, subphase, name='Multilayer Structure')\n", + "sample = Sample(Multilayer(superphase), rep_multilayer, Multilayer(subphase), name='Multilayer Structure')\n", "model = Model(\n", " sample=sample,\n", " scale=1,\n", @@ -221,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "db1e3df9-d0fa-421a-a959-a8b2fe483310", "metadata": {}, "outputs": [], @@ -340,7 +341,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/docs/src/tutorials/fitting/simple_fitting.ipynb b/docs/src/tutorials/fitting/simple_fitting.ipynb index dab3008d..0b34c68d 100644 --- a/docs/src/tutorials/fitting/simple_fitting.ipynb +++ b/docs/src/tutorials/fitting/simple_fitting.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "f026d35c-6a4a-4e9d-889c-d23ea6ee7adc", "metadata": {}, "outputs": [], @@ -39,8 +39,8 @@ "from easyreflectometry.sample import Sample\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.fitting import Fitter\n", "from easyreflectometry.plot import plot" @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "7d851064-605c-4f80-a510-197bcdbff2ea", "metadata": {}, "outputs": [], @@ -157,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "1fd5d9ba-a912-40f1-96a9-8d8d85c35c18", "metadata": {}, "outputs": [], @@ -196,7 +196,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "bfd2d8a1-35fe-4a6c-aeee-93f1b4066b61", "metadata": {}, "outputs": [], @@ -235,7 +235,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "a6508395-d292-4338-9fbe-77e19b011ae6", "metadata": {}, "outputs": [], @@ -253,12 +253,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "2a0aee2a-e77e-4558-a8b0-ef2d1cffd4d0", "metadata": {}, "outputs": [], "source": [ - "sample = Sample(superphase, film_layer, subphase, name='Film Structure')" + "sample = Sample(superphase, Multilayer(film_layer), Multilayer(subphase), name='Film Structure')" ] }, { @@ -301,7 +301,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "8646c977-28b4-4cd4-adbd-fc263359ca1c", "metadata": {}, "outputs": [], @@ -347,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "a4d3eae5-ec8d-4a7f-91ef-fcce3cc5a5ed", "metadata": {}, "outputs": [], @@ -373,7 +373,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "7e2d7b81-7b49-4831-9508-75e0e234f15f", "metadata": {}, "outputs": [], @@ -400,7 +400,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "cdbe172a-1283-4cdc-8fb2-d3383b8d21b3", "metadata": {}, "outputs": [], @@ -439,7 +439,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "3648873e-16bb-449d-b90d-8b1bd4f05eb6", "metadata": {}, "outputs": [], @@ -458,7 +458,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "9762fc0b-c4c2-4f92-8560-079ea248dfca", "metadata": {}, "outputs": [], @@ -537,7 +537,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/docs/src/tutorials/magnetism.ipynb b/docs/src/tutorials/magnetism.ipynb index 28589ddc..b1bd0e60 100644 --- a/docs/src/tutorials/magnetism.ipynb +++ b/docs/src/tutorials/magnetism.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "29d5d62d-af4a-416d-bbe2-1338d32b30f5", "metadata": {}, "outputs": [], @@ -38,8 +38,8 @@ "import easyreflectometry\n", "\n", "from easyreflectometry.calculators import CalculatorFactory\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.sample import Layer\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", @@ -91,7 +91,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "0f95d620-35b7-4b47-a3b4-9e33d5525b50", "metadata": {}, "outputs": [], @@ -119,13 +119,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "2af8c30b", "metadata": {}, "outputs": [], "source": [ "two_layers = Multilayer([sld_4_layer, sld_8_layer], name='SLD 4/8 Layer')\n", - "sample = Sample(superphase, two_layers, subphase, name='Two Layer Sample')\n", + "sample = Sample(Multilayer(superphase), two_layers, Multilayer(subphase), name='Two Layer Sample')\n", "model = Model(\n", " sample=sample,\n", " scale=1,\n", @@ -144,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "b0259cd0", "metadata": {}, "outputs": [], @@ -175,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "f1500603-d85d-4e16-b697-e1bf16502991", "metadata": {}, "outputs": [], @@ -194,7 +194,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "18010202", "metadata": {}, "outputs": [], @@ -569,7 +569,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/docs/src/tutorials/sample/material_solvated.ipynb b/docs/src/tutorials/sample/material_solvated.ipynb index 39197d94..03d71c36 100644 --- a/docs/src/tutorials/sample/material_solvated.ipynb +++ b/docs/src/tutorials/sample/material_solvated.ipynb @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "88c86e82-88dd-4c2d-ad99-826f409ec7b7", "metadata": {}, "outputs": [], @@ -45,8 +45,8 @@ "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import MaterialSolvated\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.fitting import Fitter\n", "from easyreflectometry.plot import plot" @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "a95a39dd-d0eb-4029-9dc8-41e6e7918f66", "metadata": {}, "outputs": [], @@ -110,7 +110,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "60e5a3c5-58a8-429a-a446-a115f489af0f", "metadata": {}, "outputs": [], @@ -132,12 +132,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "f2b910db-530f-49c5-907a-67712ba939d2", "metadata": {}, "outputs": [], "source": [ - "solvated_film = MaterialSolvated(\n", + "solvated_film_material = MaterialSolvated(\n", " material=film,\n", " solvent=d2o,\n", " solvent_fraction=0.25,\n", @@ -165,7 +165,7 @@ "metadata": {}, "outputs": [], "source": [ - "solvated_film" + "solvated_film_material" ] }, { @@ -178,7 +178,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "f1ea2e41-f50e-4fbb-989f-aecc0e3e9860", "metadata": {}, "outputs": [], @@ -187,12 +187,12 @@ "sio2_layer = Layer(material=sio2, thickness=30, roughness=3, name='SiO2 layer')\n", "superphase = Multilayer([si_layer, sio2_layer], name='Si/SiO2 Superphase')\n", "\n", - "solvated_film_layer = Layer(material=solvated_film, thickness=250, roughness=3, name='Film Layer')\n", + "solvated_film = Layer(material=solvated_film_material, thickness=250, roughness=3, name='Film Layer')\n", "\n", "subphase = Layer(material=d2o, thickness=0, roughness=3, name='D2O Subphase')\n", "\n", "resolution_function = PercentageFhwm(0.02)\n", - "sample = Sample(superphase, solvated_film_layer, subphase, name='Film Structure')\n", + "sample = Sample(superphase, Multilayer(solvated_film), Multilayer(subphase), name='Film Structure')\n", "model = Model(\n", " sample=sample,\n", " scale=1,\n", @@ -216,7 +216,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "f99fba34-268d-40ee-a74d-982bb2805b11", "metadata": {}, "outputs": [], @@ -231,12 +231,12 @@ "metadata": {}, "outputs": [], "source": [ - "print(solvated_film_layer.material.sld)" + "print(solvated_film.material.sld)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "0e872a4d-f468-4e5a-8938-9cb50d16c460", "metadata": {}, "outputs": [], @@ -251,7 +251,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(solvated_film_layer.material.sld)" + "print(solvated_film.material.sld)" ] }, { @@ -264,17 +264,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "79c3dc7a-b4cd-4eea-b098-08c962d747e6", "metadata": {}, "outputs": [], "source": [ "# Thicknesses\n", "sio2_layer.thickness.bounds = (15, 50)\n", - "solvated_film_layer.thickness.bounds = (200, 300)\n", + "solvated_film.thickness.bounds = (200, 300)\n", "# Roughnesses\n", "sio2_layer.roughness.bounds = (1, 15)\n", - "solvated_film_layer.roughness.bounds = (1, 15)\n", + "solvated_film.roughness.bounds = (1, 15)\n", "subphase.roughness.bounds = (1, 15)\n", "# Scattering length density\n", "film.sld.bounds = (0.1, 3)\n", @@ -296,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "3e24b615-cb32-41f8-95e5-5998072c7868", "metadata": {}, "outputs": [], @@ -340,7 +340,7 @@ "metadata": {}, "outputs": [], "source": [ - "solvated_film" + "solvated_film_material" ] }, { @@ -368,7 +368,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/docs/src/tutorials/sample/monolayer.ipynb b/docs/src/tutorials/sample/monolayer.ipynb index 514673f3..533b3413 100644 --- a/docs/src/tutorials/sample/monolayer.ipynb +++ b/docs/src/tutorials/sample/monolayer.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "619bb767-475a-408b-b576-552f0bc4f2a7", "metadata": {}, "outputs": [], @@ -41,9 +41,10 @@ "from easyreflectometry.sample import SurfactantLayer\n", "from easyreflectometry.sample import LayerAreaPerMolecule\n", "from easyreflectometry.sample import Layer\n", + "from easyreflectometry.sample import Multilayer\n", "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.fitting import Fitter\n", "from easyreflectometry.plot import plot\n", "from easyscience.fitting import AvailableMinimizers\n" @@ -131,7 +132,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "80c8d71f-d309-4104-bae6-3941daa525d3", "metadata": {}, "outputs": [], @@ -163,7 +164,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "3ad9adef-8845-486d-8075-9ad6bb81ea6f", "metadata": {}, "outputs": [], @@ -182,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "a39a0eca-97d6-44d7-8796-a5e98d024788", "metadata": {}, "outputs": [], @@ -201,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "204144c3-a3e7-4ab1-9a6c-6aca8241f69e", "metadata": {}, "outputs": [], @@ -220,7 +221,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "e39cf91b-e049-4619-a5cd-4bdf8492252d", "metadata": {}, "outputs": [], @@ -281,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "c17ecc32-c578-4a22-a12c-da13af1e0347", "metadata": {}, "outputs": [], @@ -301,7 +302,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "f17762ca-33c5-48bb-88a2-bc2568bb18f7", "metadata": {}, "outputs": [], @@ -319,13 +320,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "216bfe40-a97c-4437-a2f9-8bc7966ae58d", "metadata": {}, "outputs": [], "source": [ "resolution_function = PercentageFhwm(5)\n", - "sample = Sample(air_layer, dspc, d2o_layer)\n", + "sample = Sample(Multilayer(air_layer), dspc, Multilayer(d2o_layer))\n", "model = Model(\n", " sample=sample,\n", " scale=1,\n", @@ -348,7 +349,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "d30eaa0f-be7f-4cbe-a7d6-11f43512f014", "metadata": {}, "outputs": [], @@ -370,7 +371,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "id": "bc61b31f-11bf-43e1-9fd9-d697ded79196", "metadata": {}, "outputs": [], @@ -487,7 +488,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/docs/src/tutorials/sample/multi_contrast.ipynb b/docs/src/tutorials/sample/multi_contrast.ipynb index fab42c39..d38770eb 100644 --- a/docs/src/tutorials/sample/multi_contrast.ipynb +++ b/docs/src/tutorials/sample/multi_contrast.ipynb @@ -39,10 +39,11 @@ "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import SurfactantLayer\n", "from easyreflectometry.sample import Layer\n", + "from easyreflectometry.sample import Multilayer\n", "from easyreflectometry.sample import LayerAreaPerMolecule\n", "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.fitting import Fitter\n", "from easyscience.fitting import AvailableMinimizers\n", @@ -65,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "694b4e5e-2d1a-402e-aa3f-a26cc82f7774", "metadata": {}, "outputs": [], @@ -124,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "7834b80e-1dc9-47b8-923a-dec58e3494be", "metadata": {}, "outputs": [], @@ -143,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "58855c68-c95b-40de-9fee-a662771c247b", "metadata": {}, "outputs": [], @@ -162,7 +163,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "08112c21-2027-47ba-845b-a3135439d862", "metadata": {}, "outputs": [], @@ -180,7 +181,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "e7caedc2-8305-4f3b-bad2-c042bacb363b", "metadata": {}, "outputs": [], @@ -200,7 +201,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "74e19578-03d4-4295-b6e2-a65fb2508c78", "metadata": {}, "outputs": [], @@ -223,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "c4aa6c00", "metadata": {}, "outputs": [], @@ -263,7 +264,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "16a970d8", "metadata": {}, "outputs": [], @@ -303,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "021272e0-7abb-4703-a4cd-92daed10ae50", "metadata": {}, "outputs": [], @@ -346,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "24808923-ba02-4a7d-9be3-8d717b08aa8e", "metadata": {}, "outputs": [], @@ -381,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "e31a4807-c6f4-4bfd-986e-749955aa7e49", "metadata": {}, "outputs": [], @@ -411,16 +412,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "6e92498a-581b-445d-94cd-dda6474f1984", "metadata": {}, "outputs": [], "source": [ "resolution_function = PercentageFhwm(5)\n", "\n", - "d13d2o_sample = Sample(air_layer, d13d2o, d2o_layer)\n", - "d70d2o_sample = Sample(air_layer, d70d2o, d2o_layer)\n", - "d83acmw_sample = Sample(air_layer, d83acmw, acmw_layer)\n", + "d13d2o_sample = Sample(Multilayer(air_layer), d13d2o, Multilayer(d2o_layer))\n", + "d70d2o_sample = Sample(Multilayer(air_layer), d70d2o, Multilayer(d2o_layer))\n", + "d83acmw_sample = Sample(Multilayer(air_layer), d83acmw, Multilayer(acmw_layer))\n", "d13d2o_model = Model(\n", " sample=d13d2o_sample,\n", " scale=0.1,\n", @@ -455,7 +456,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "cf35ee97-2d64-4a0f-9212-7cca8bb2dcfc", "metadata": {}, "outputs": [], @@ -486,7 +487,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "dc8e7f8f-6b4b-45ce-a38c-274dea683df4", "metadata": {}, "outputs": [], @@ -506,7 +507,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "ce890544-f3cf-4a74-b927-d19dc292e12c", "metadata": {}, "outputs": [], @@ -517,7 +518,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "0cb9d7b4-aa04-4c73-a700-4ff5af7e6f47", "metadata": {}, "outputs": [], @@ -570,7 +571,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/docs/src/tutorials/sample/resolution_functions.ipynb b/docs/src/tutorials/sample/resolution_functions.ipynb index 63f3d7c7..26037dfd 100644 --- a/docs/src/tutorials/sample/resolution_functions.ipynb +++ b/docs/src/tutorials/sample/resolution_functions.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "29d5d62d-af4a-416d-bbe2-1338d32b30f5", "metadata": {}, "outputs": [], @@ -43,9 +43,9 @@ "\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", - "from easyreflectometry.experiment import Model\n", - "from easyreflectometry.experiment import LinearSpline\n", - "from easyreflectometry.experiment import PercentageFhwm\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import LinearSpline\n", + "from easyreflectometry.model import PercentageFhwm\n", "from easyreflectometry.sample import Layer\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", @@ -89,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "609174e5-1371-412d-a29f-cb05bfe36df0", "metadata": {}, "outputs": [], @@ -157,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "0f95d620-35b7-4b47-a3b4-9e33d5525b50", "metadata": {}, "outputs": [], @@ -170,7 +170,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "9a0b37ed-8714-4614-b49f-1e86ac232ac1", "metadata": {}, "outputs": [], @@ -210,12 +210,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "2af8c30b", "metadata": {}, "outputs": [], "source": [ - "sample = Sample(superphase, two_layers, subphase, name='Two Layer Sample')\n", + "sample = Sample(Multilayer(superphase), two_layers, Multilayer(subphase), name='Two Layer Sample')\n", "model = Model(\n", " sample=sample,\n", " scale=1,\n", @@ -258,7 +258,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "880d10d7-b655-4ef1-b376-21b2e4394160", "metadata": {}, "outputs": [], @@ -394,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.5" } }, "nbformat": 4, diff --git a/src/easyreflectometry/calculators/bornagain/calculator.py b/src/easyreflectometry/calculators/bornagain/calculator.py index 3a17caef..205cf935 100644 --- a/src/easyreflectometry/calculators/bornagain/calculator.py +++ b/src/easyreflectometry/calculators/bornagain/calculator.py @@ -3,7 +3,7 @@ import numpy as np from easyscience.Objects.Inferface import ItemContainer -from easyreflectometry.experiment import Model +from easyreflectometry.model import Model from easyreflectometry.sample import Layer from easyreflectometry.sample import Material from easyreflectometry.sample import MaterialMixture diff --git a/src/easyreflectometry/calculators/calculator_base.py b/src/easyreflectometry/calculators/calculator_base.py index 122e3cbc..8811b7c3 100644 --- a/src/easyreflectometry/calculators/calculator_base.py +++ b/src/easyreflectometry/calculators/calculator_base.py @@ -7,7 +7,7 @@ from easyscience.Objects.core import ComponentSerializer from easyscience.Objects.Inferface import ItemContainer -from easyreflectometry.experiment import Model +from easyreflectometry.model import Model from easyreflectometry.sample import BaseAssembly from easyreflectometry.sample import Layer from easyreflectometry.sample import Material diff --git a/src/easyreflectometry/calculators/refl1d/wrapper.py b/src/easyreflectometry/calculators/refl1d/wrapper.py index 020801b7..9817dfb7 100644 --- a/src/easyreflectometry/calculators/refl1d/wrapper.py +++ b/src/easyreflectometry/calculators/refl1d/wrapper.py @@ -6,7 +6,7 @@ from refl1d import model from refl1d import names -from easyreflectometry.experiment.resolution_functions import PercentageFhwm +from easyreflectometry.model import PercentageFhwm from ..wrapper_base import WrapperBase diff --git a/src/easyreflectometry/calculators/refnx/wrapper.py b/src/easyreflectometry/calculators/refnx/wrapper.py index 3ab62a9b..0cfded10 100644 --- a/src/easyreflectometry/calculators/refnx/wrapper.py +++ b/src/easyreflectometry/calculators/refnx/wrapper.py @@ -5,7 +5,7 @@ import numpy as np from refnx import reflect -from easyreflectometry.experiment.resolution_functions import PercentageFhwm +from easyreflectometry.model import PercentageFhwm from ..wrapper_base import WrapperBase diff --git a/src/easyreflectometry/calculators/wrapper_base.py b/src/easyreflectometry/calculators/wrapper_base.py index 8f9440ee..56f90a16 100644 --- a/src/easyreflectometry/calculators/wrapper_base.py +++ b/src/easyreflectometry/calculators/wrapper_base.py @@ -2,8 +2,8 @@ import numpy as np -from easyreflectometry.experiment import PercentageFhwm -from easyreflectometry.experiment import ResolutionFunction +from easyreflectometry.model import PercentageFhwm +from easyreflectometry.model import ResolutionFunction class WrapperBase: diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index 92f7794a..89e4eb64 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -5,7 +5,7 @@ from easyscience.fitting import AvailableMinimizers from easyscience.fitting.multi_fitter import MultiFitter as easyFitter -from easyreflectometry.experiment import Model +from easyreflectometry.model import Model class Fitter: diff --git a/src/easyreflectometry/experiment/__init__.py b/src/easyreflectometry/model/__init__.py similarity index 100% rename from src/easyreflectometry/experiment/__init__.py rename to src/easyreflectometry/model/__init__.py diff --git a/src/easyreflectometry/experiment/model.py b/src/easyreflectometry/model/model.py similarity index 82% rename from src/easyreflectometry/experiment/model.py rename to src/easyreflectometry/model/model.py index e340fb82..98214ed6 100644 --- a/src/easyreflectometry/experiment/model.py +++ b/src/easyreflectometry/model/model.py @@ -14,8 +14,6 @@ from easyreflectometry.parameter_utils import get_as_parameter from easyreflectometry.parameter_utils import yaml_dump from easyreflectometry.sample import BaseAssembly -from easyreflectometry.sample import Layer -from easyreflectometry.sample import LayerCollection from easyreflectometry.sample import Sample from .resolution_functions import PercentageFhwm @@ -93,51 +91,37 @@ def __init__( # Must be set after resolution function self.interface = interface - def add_item(self, *assemblies: list[BaseAssembly]) -> None: + def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None: """Add a layer or item to the model sample. :param assemblies: Assemblies to add to model sample. """ - for arg in assemblies: - if issubclass(arg.__class__, BaseAssembly): - self.sample.append(arg) + for assembly in assemblies: + if issubclass(assembly.__class__, BaseAssembly): + self.sample.add_assembly(assembly) if self.interface is not None: - self.interface().add_item_to_model(arg.unique_name, self.unique_name) + self.interface().add_item_to_model(assembly.unique_name, self.unique_name) else: - raise ValueError(f'Object {arg} is not a valid type, must be a child of BaseAssembly.') + raise ValueError(f'Object {assembly} is not a valid type, must be a child of BaseAssembly.') - def duplicate_item(self, idx: int) -> None: + def duplicate_assembly(self, index: int) -> None: """Duplicate a given item or layer in a sample. :param idx: Index of the item or layer to duplicate """ - to_duplicate = self.sample[idx] - duplicate_layers = [] - for i in to_duplicate.layers: - duplicate_layers.append( - Layer( - material=i.material, - thickness=i.thickness.value, - roughness=i.roughness.value, - name=i.name + ' duplicate', - interface=i.interface, - ) - ) - duplicate = to_duplicate.__class__( - LayerCollection(*duplicate_layers, name=to_duplicate.layers.name + ' duplicate'), - name=to_duplicate.name + ' duplicate', - ) - self.add_item(duplicate) + self.sample.duplicate_assembly(index) + if self.interface is not None: + self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name) - def remove_item(self, idx: int) -> None: - """Remove an item from the model. + def remove_assembly(self, index: int) -> None: + """Remove an assembly from the model. :param idx: Index of the item to remove. """ - item_unique_name = self.sample[idx].unique_name - del self.sample[idx] + assembly_unique_name = self.sample[index].unique_name + self.sample.remove_assembly(index) if self.interface is not None: - self.interface().remove_item_from_model(item_unique_name, self.unique_name) + self.interface().remove_item_from_model(assembly_unique_name, self.unique_name) @property def resolution_function(self) -> ResolutionFunction: diff --git a/src/easyreflectometry/experiment/model_collection.py b/src/easyreflectometry/model/model_collection.py similarity index 92% rename from src/easyreflectometry/experiment/model_collection.py rename to src/easyreflectometry/model/model_collection.py index cdf610a4..5788afe0 100644 --- a/src/easyreflectometry/experiment/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -5,8 +5,8 @@ from typing import List from typing import Tuple -from easyreflectometry.sample.base_element_collection import SIZE_DEFAULT_COLLECTION -from easyreflectometry.sample.base_element_collection import BaseElementCollection +from easyreflectometry.sample.collections.base_element_collection import SIZE_DEFAULT_COLLECTION +from easyreflectometry.sample.collections.base_element_collection import BaseElementCollection from .model import Model diff --git a/src/easyreflectometry/experiment/resolution_functions.py b/src/easyreflectometry/model/resolution_functions.py similarity index 100% rename from src/easyreflectometry/experiment/resolution_functions.py rename to src/easyreflectometry/model/resolution_functions.py diff --git a/src/easyreflectometry/sample/__init__.py b/src/easyreflectometry/sample/__init__.py index 903fffad..7bb988dd 100644 --- a/src/easyreflectometry/sample/__init__.py +++ b/src/easyreflectometry/sample/__init__.py @@ -3,15 +3,15 @@ from .assemblies.multilayer import Multilayer from .assemblies.repeating_multilayer import RepeatingMultilayer from .assemblies.surfactant_layer import SurfactantLayer +from .collections.layer_collection import LayerCollection +from .collections.material_collection import MaterialCollection +from .collections.sample import Sample from .elements.layers.layer import Layer from .elements.layers.layer_area_per_molecule import LayerAreaPerMolecule -from .elements.layers.layer_collection import LayerCollection from .elements.materials.material import Material -from .elements.materials.material_collection import MaterialCollection from .elements.materials.material_density import MaterialDensity from .elements.materials.material_mixture import MaterialMixture from .elements.materials.material_solvated import MaterialSolvated -from .sample import Sample __all__ = ( BaseAssembly, diff --git a/src/easyreflectometry/sample/assemblies/base_assembly.py b/src/easyreflectometry/sample/assemblies/base_assembly.py index 1c0f2807..7d0af4b8 100644 --- a/src/easyreflectometry/sample/assemblies/base_assembly.py +++ b/src/easyreflectometry/sample/assemblies/base_assembly.py @@ -4,8 +4,8 @@ from easyscience.Constraints import ObjConstraint from ..base_core import BaseCore +from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer -from ..elements.layers.layer_collection import LayerCollection class BaseAssembly(BaseCore): diff --git a/src/easyreflectometry/sample/assemblies/gradient_layer.py b/src/easyreflectometry/sample/assemblies/gradient_layer.py index 4f5a29bb..8be219a9 100644 --- a/src/easyreflectometry/sample/assemblies/gradient_layer.py +++ b/src/easyreflectometry/sample/assemblies/gradient_layer.py @@ -3,8 +3,8 @@ from easyscience import global_object from numpy import arange +from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer -from ..elements.layers.layer_collection import LayerCollection from ..elements.materials.material import Material from .base_assembly import BaseAssembly diff --git a/src/easyreflectometry/sample/assemblies/multilayer.py b/src/easyreflectometry/sample/assemblies/multilayer.py index d57d78b3..320ba1df 100644 --- a/src/easyreflectometry/sample/assemblies/multilayer.py +++ b/src/easyreflectometry/sample/assemblies/multilayer.py @@ -3,10 +3,10 @@ from typing import Optional from typing import Union -from easyreflectometry.sample.base_element_collection import SIZE_DEFAULT_COLLECTION +from easyreflectometry.sample.collections.base_element_collection import SIZE_DEFAULT_COLLECTION +from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer -from ..elements.layers.layer_collection import LayerCollection from .base_assembly import BaseAssembly @@ -103,7 +103,4 @@ def from_dict(cls, data: dict) -> Multilayer: :return: Multilayer """ multilayer = super().from_dict(data) - # Remove the default materials - for i in range(SIZE_DEFAULT_COLLECTION): - del multilayer.layers[0] return multilayer diff --git a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py index ba26fef6..5aa9d697 100644 --- a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py +++ b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py @@ -5,10 +5,10 @@ from easyscience.Objects.new_variable import Parameter from easyreflectometry.parameter_utils import get_as_parameter -from easyreflectometry.sample.base_element_collection import SIZE_DEFAULT_COLLECTION +from easyreflectometry.sample.collections.base_element_collection import SIZE_DEFAULT_COLLECTION +from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer -from ..elements.layers.layer_collection import LayerCollection from .multilayer import Multilayer DEFAULTS = { diff --git a/src/easyreflectometry/sample/assemblies/surfactant_layer.py b/src/easyreflectometry/sample/assemblies/surfactant_layer.py index 7dfcda5b..e0db9c59 100644 --- a/src/easyreflectometry/sample/assemblies/surfactant_layer.py +++ b/src/easyreflectometry/sample/assemblies/surfactant_layer.py @@ -5,8 +5,8 @@ from easyscience.Constraints import ObjConstraint from easyscience.Objects.new_variable import Parameter +from ..collections.layer_collection import LayerCollection from ..elements.layers.layer_area_per_molecule import LayerAreaPerMolecule -from ..elements.layers.layer_collection import LayerCollection from ..elements.materials.material import Material from .base_assembly import BaseAssembly diff --git a/src/easyreflectometry/sample/base_element_collection.py b/src/easyreflectometry/sample/collections/base_element_collection.py similarity index 100% rename from src/easyreflectometry/sample/base_element_collection.py rename to src/easyreflectometry/sample/collections/base_element_collection.py diff --git a/src/easyreflectometry/sample/elements/layers/layer_collection.py b/src/easyreflectometry/sample/collections/layer_collection.py similarity index 81% rename from src/easyreflectometry/sample/elements/layers/layer_collection.py rename to src/easyreflectometry/sample/collections/layer_collection.py index 69c481fb..a72b7871 100644 --- a/src/easyreflectometry/sample/elements/layers/layer_collection.py +++ b/src/easyreflectometry/sample/collections/layer_collection.py @@ -2,8 +2,8 @@ from typing import Optional -from ...base_element_collection import BaseElementCollection -from .layer import Layer +from ..elements.layers.layer import Layer +from .base_element_collection import BaseElementCollection class LayerCollection(BaseElementCollection): diff --git a/src/easyreflectometry/sample/elements/materials/material_collection.py b/src/easyreflectometry/sample/collections/material_collection.py similarity index 81% rename from src/easyreflectometry/sample/elements/materials/material_collection.py rename to src/easyreflectometry/sample/collections/material_collection.py index 7901327b..07833090 100644 --- a/src/easyreflectometry/sample/elements/materials/material_collection.py +++ b/src/easyreflectometry/sample/collections/material_collection.py @@ -2,10 +2,10 @@ from typing import Tuple from typing import Union -from ...base_element_collection import SIZE_DEFAULT_COLLECTION -from ...base_element_collection import BaseElementCollection -from .material import Material -from .material_mixture import MaterialMixture +from ..elements.materials.material import Material +from ..elements.materials.material_mixture import MaterialMixture +from .base_element_collection import SIZE_DEFAULT_COLLECTION +from .base_element_collection import BaseElementCollection class MaterialCollection(BaseElementCollection): diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py new file mode 100644 index 00000000..d5505ce9 --- /dev/null +++ b/src/easyreflectometry/sample/collections/sample.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +__author__ = 'github.com/arm61' + +from typing import List +from typing import Optional + +from easyscience.Objects.Groups import BaseCollection + +from easyreflectometry.parameter_utils import yaml_dump + +from ..assemblies.base_assembly import BaseAssembly +from ..assemblies.multilayer import Multilayer +from ..assemblies.repeating_multilayer import RepeatingMultilayer +from ..assemblies.surfactant_layer import SurfactantLayer +from ..elements.layers.layer import Layer + +NR_DEFAULT_ASSEMBLIES = 2 + + +class Sample(BaseCollection): + """A sample is a collection of assemblies that represent the structure for which experimental measurements exist.""" + + def __init__( + self, + *list_assemblies: Optional[List[BaseAssembly]], + name: str = 'EasySample', + interface=None, + populate_if_none: bool = True, + **kwargs, + ): + """Constructor. + + :param args: The assemblies in the sample. + :param name: Name of the sample, defaults to 'EasySample'. + :param interface: Calculator interface, defaults to `None`. + """ + if not list_assemblies: + if populate_if_none: + list_assemblies = [Multilayer(interface=interface) for _ in range(NR_DEFAULT_ASSEMBLIES)] + else: + list_assemblies = [] + # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict + # Else collisions might occur in global_object.map + self.populate_if_none = False + + for assembly in list_assemblies: + if not issubclass(type(assembly), BaseAssembly): + raise ValueError('The elements must be an Assembly.') + super().__init__(name, *list_assemblies, **kwargs) + self.interface = interface + + def add_assembly(self, assembly: Optional[BaseAssembly] = None): + """Add an assembly to the sample. + + :param assembly: Assembly to add. + """ + if assembly is None: + assembly = Multilayer(name='New EasyMultilayer', interface=self.interface) + self._enable_changes_to_outermost_layers() + self.append(assembly) + self._disable_changes_to_outermost_layers() + + def duplicate_assembly(self, index: int): + """Add an assembly to the sample. + + :param assembly: Assembly to add. + """ + self._enable_changes_to_outermost_layers() + to_be_duplicated = self[index] + if isinstance(to_be_duplicated, Multilayer): + duplicate = Multilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + elif isinstance(to_be_duplicated, RepeatingMultilayer): + duplicate = RepeatingMultilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + elif isinstance(to_be_duplicated, SurfactantLayer): + duplicate = SurfactantLayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + duplicate.name = duplicate.name + ' duplicate' + self.append(duplicate) + self._disable_changes_to_outermost_layers() + + def move_assembly_up(self, index: int): + """Move the assembly at the given index up in the sample. + + :param index: Index of the assembly to move up. + """ + if index == 0: + return + self._enable_changes_to_outermost_layers() + self.insert(index - 1, self.pop(index)) + self._disable_changes_to_outermost_layers() + + def move_assembly_down(self, index: int): + """Move the assembly at the given index down in the sample. + + :param index: Index of the assembly to move down. + """ + if index == len(self) - 1: + return + self._enable_changes_to_outermost_layers() + self.insert(index + 1, self.pop(index)) + self._disable_changes_to_outermost_layers() + + def remove_assembly(self, index: int): + """Remove the assembly at the given index from the sample. + + :param index: Index of the assembly to remove. + """ + self._enable_changes_to_outermost_layers() + self.pop(index) + self._disable_changes_to_outermost_layers() + + @property + def superphase(self) -> Layer: + """The superphase of the sample.""" + return self[0].front_layer + + @property + def subphase(self) -> Layer: + """The subphase of the sample.""" + # This assembly only got one layer + if self[-1].back_layer is None: + return self[-1].front_layer + else: + return self[-1].back_layer + + def _enable_changes_to_outermost_layers(self): + """Allowed to change the outermost layers of the sample. + Superphase can change thickness and roughness. + Subphase can change thickness. + """ + self.superphase.thickness.enabled = True + self.superphase.roughness.enabled = True + self.subphase.thickness.enabled = True + + def _disable_changes_to_outermost_layers(self): + """No allowed to change the outermost layers of the sample. + Superphase can change thickness and roughness. + Subphase can change thickness. + """ + self.superphase.thickness.enabled = False + self.superphase.roughness.enabled = False + self.subphase.thickness.enabled = False + + # Representation + @property + def _dict_repr(self) -> dict: + """A simplified dict representation.""" + return {self.name: [i._dict_repr for i in self]} + + def __repr__(self) -> str: + """String representation of the sample.""" + return yaml_dump(self._dict_repr) + + def as_dict(self, skip: list = None) -> dict: + """Produces a cleaned dict using a custom as_dict method to skip necessary things. + The resulting dict matches the parameters in __init__ + + :param skip: List of keys to skip, defaults to `None`. + """ + if skip is None: + skip = [] + this_dict = super().as_dict(skip=skip) + for i, assembly in enumerate(self.data): + this_dict['data'][i] = assembly.as_dict(skip=skip) + this_dict['populate_if_none'] = self.populate_if_none + return this_dict diff --git a/src/easyreflectometry/sample/sample.py b/src/easyreflectometry/sample/sample.py deleted file mode 100644 index a613b7d1..00000000 --- a/src/easyreflectometry/sample/sample.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -__author__ = 'github.com/arm61' - -from typing import Union - -from easyscience.Objects.Groups import BaseCollection - -from easyreflectometry.parameter_utils import yaml_dump - -from .assemblies.base_assembly import BaseAssembly -from .assemblies.multilayer import Multilayer -from .elements.layers.layer import Layer - -NR_DEFAULT_LAYERS = 2 - - -class Sample(BaseCollection): - """Collection of assemblies that represent the sample for which experimental measurements exist.""" - - def __init__( - self, - *list_layer_like: list[Union[Layer, BaseAssembly]], - name: str = 'EasySample', - interface=None, - populate_if_none: bool = True, - **kwargs, - ): - """Constructor. - - :param args: The assemblies in the sample. - :param name: Name of the sample, defaults to 'EasySample'. - :param interface: Calculator interface, defaults to `None`. - """ - new_items = [] - if not list_layer_like: - if populate_if_none: - list_layer_like = [Multilayer(interface=interface) for _ in range(NR_DEFAULT_LAYERS)] - else: - list_layer_like = [] - # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict - # Else collisions might occur in global_object.map - self.populate_if_none = False - - for layer_like in list_layer_like: - if issubclass(type(layer_like), Layer): - new_items.append(Multilayer(layer_like, name=layer_like.name)) - elif issubclass(type(layer_like), BaseAssembly): - new_items.append(layer_like) - else: - raise ValueError('The items must be either a Layer or an Assembly.') - super().__init__(name, *new_items, **kwargs) - self.interface = interface - - # Representation - @property - def _dict_repr(self) -> dict: - """A simplified dict representation.""" - return {self.name: [i._dict_repr for i in self]} - - def __repr__(self) -> str: - """String representation of the sample.""" - return yaml_dump(self._dict_repr) - - def as_dict(self, skip: list = None) -> dict: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. - """ - if skip is None: - skip = [] - this_dict = super().as_dict(skip=skip) - for i, layer in enumerate(self.data): - this_dict['data'][i] = layer.as_dict(skip=skip) - this_dict['populate_if_none'] = self.populate_if_none - return this_dict diff --git a/tests/calculators/refnx/test_refnx_wrapper.py b/tests/calculators/refnx/test_refnx_wrapper.py index de2a0027..093bfb2c 100644 --- a/tests/calculators/refnx/test_refnx_wrapper.py +++ b/tests/calculators/refnx/test_refnx_wrapper.py @@ -16,8 +16,8 @@ from refnx import reflect from easyreflectometry.calculators.refnx.wrapper import RefnxWrapper -from easyreflectometry.experiment import LinearSpline -from easyreflectometry.experiment import PercentageFhwm +from easyreflectometry.model import LinearSpline +from easyreflectometry.model import PercentageFhwm class TestRefnx(unittest.TestCase): diff --git a/tests/experiment/test_model.py b/tests/model/test_model.py similarity index 92% rename from tests/experiment/test_model.py rename to tests/model/test_model.py index 8ec73309..f0f3c496 100644 --- a/tests/experiment/test_model.py +++ b/tests/model/test_model.py @@ -15,9 +15,9 @@ from numpy.testing import assert_equal from easyreflectometry.calculators import CalculatorFactory -from easyreflectometry.experiment import LinearSpline -from easyreflectometry.experiment import Model -from easyreflectometry.experiment import PercentageFhwm +from easyreflectometry.model import LinearSpline +from easyreflectometry.model import Model +from easyreflectometry.model import PercentageFhwm from easyreflectometry.sample import Layer from easyreflectometry.sample import LayerCollection from easyreflectometry.sample import Material @@ -84,7 +84,7 @@ def test_from_pars(self): assert mod._resolution_function.smearing([1]) == 2.0 assert mod._resolution_function.smearing([100]) == 2.0 - def test_add_item(self): + def test_add_assemblies(self): m1 = Material(6.908, -0.278, 'Boron') m2 = Material(0.487, 0.000, 'Potassium') l1 = Layer(m1, 5.0, 2.0, 'thinBoron') @@ -99,24 +99,24 @@ def test_add_item(self): resolution_function = PercentageFhwm(2.0) mod = Model(d, 2, 1e-5, resolution_function, 'newModel') assert_equal(len(mod.sample), 1) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.sample), 2) assert_equal(mod.sample[1].name, 'oneLayerItem2') assert_equal(issubclass(mod.sample[1].__class__, RepeatingMultilayer), True) - mod.add_item(surfactant) + mod.add_assemblies(surfactant) assert_equal(len(mod.sample), 3) - mod.add_item(multilayer) + mod.add_assemblies(multilayer) assert_equal(len(mod.sample), 4) - def test_add_item_exception(self): + def test_add_assemblies_exception(self): # When mod = Model() # Then Expect with pytest.raises(ValueError): - mod.add_item('not an assembly') + mod.add_assemblies('not an assembly') - def test_add_item_with_interface_refnx(self): + def test_add_assemblies_with_interface_refnx(self): interface = CalculatorFactory() m1 = Material(6.908, -0.278, 'Boron') m2 = Material(0.487, 0.000, 'Potassium') @@ -131,11 +131,11 @@ def test_add_item_with_interface_refnx(self): mod = Model(d, 2, 1e-5, resolution_function, 'newModel', interface=interface) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.interface()._wrapper.storage['item']), 2) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - def test_add_item_with_interface_refl1d(self): + def test_add_assemblies_with_interface_refl1d(self): interface = CalculatorFactory() interface.switch('refl1d') m1 = Material(6.908, -0.278, 'Boron') @@ -151,11 +151,11 @@ def test_add_item_with_interface_refl1d(self): mod = Model(d, 2, 1e-5, resolution_function, 'newModel', interface=interface) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.interface()._wrapper.storage['item']), 2) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - # def test_add_item_with_interface_bornagain(self): + # def test_add_assemblies_with_interface_bornagain(self): # interface = CalculatorFactory() # interface.switch('BornAgain') # m1 = Material.from_pars(6.908, 0.278, 'Boron') @@ -170,11 +170,11 @@ def test_add_item_with_interface_refl1d(self): # mod = Model(d, 2, 1e-5, 2.0, 'newModel', interface=interface) # assert_equal(len(mod.interface()._wrapper.storage['item']), 1) # assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - # mod.add_item(o2) + # mod.add_assemblies(o2) # assert_equal(len(mod.interface()._wrapper.storage['item']), 2) # assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - def test_duplicate_item(self): + def test_duplicate_assembly(self): m1 = Material(6.908, -0.278, 'Boron') m2 = Material(0.487, 0.000, 'Potassium') l1 = Layer(m1, 5.0, 2.0, 'thinBoron') @@ -187,14 +187,14 @@ def test_duplicate_item(self): resolution_function = PercentageFhwm(2.0) mod = Model(d, 2, 1e-5, resolution_function, 'newModel') assert_equal(len(mod.sample), 1) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.sample), 2) - mod.duplicate_item(1) + mod.duplicate_assembly(1) assert_equal(len(mod.sample), 3) assert_equal(mod.sample[2].name, 'oneLayerItem2 duplicate') assert_equal(issubclass(mod.sample[2].__class__, RepeatingMultilayer), True) - def test_duplicate_item_with_interface_refnx(self): + def test_duplicate_assembly_with_interface_refnx(self): interface = CalculatorFactory() m1 = Material(6.908, -0.278, 'Boron') m2 = Material(0.487, 0.000, 'Potassium') @@ -208,12 +208,12 @@ def test_duplicate_item_with_interface_refnx(self): resolution_function = PercentageFhwm(2.0) mod = Model(d, 2, 1e-5, resolution_function, 'newModel', interface=interface) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.interface()._wrapper.storage['item']), 2) - mod.duplicate_item(1) + mod.duplicate_assembly(1) assert_equal(len(mod.interface()._wrapper.storage['item']), 3) - def test_duplicate_item_with_interface_refl1d(self): + def test_duplicate_assembly_with_interface_refl1d(self): interface = CalculatorFactory() interface.switch('refl1d') m1 = Material(6.908, -0.278, 'Boron') @@ -228,9 +228,9 @@ def test_duplicate_item_with_interface_refl1d(self): resolution_function = PercentageFhwm(2.0) mod = Model(d, 2, 1e-5, resolution_function, 'newModel', interface=interface) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.interface()._wrapper.storage['item']), 2) - mod.duplicate_item(1) + mod.duplicate_assembly(1) assert_equal(len(mod.interface()._wrapper.storage['item']), 3) # def test_duplicate_item_with_interface_bornagain(self): @@ -246,13 +246,13 @@ def test_duplicate_item_with_interface_refl1d(self): # o2 = RepeatingMultilayer.from_pars(ls2, 1.0, 'oneLayerItem2') # d = Sample.from_pars(o1, name='myModel') # mod = Model(d, 2, 1e-5, 2.0, 'newModel', interface=interface) - # assert_equal(len(mod.interface()._wrapper.storage['item']), 1) - # mod.add_item(o2) + # assert_equal(len(mod.interface()._wrapper.storage['assembly']), 1) + # mod.add_assemblies(o2) # assert_equal(len(mod.interface()._wrapper.storage['item']), 2) - # mod.duplicate_item(1) + # mod.duplicate_assembly(1) # assert_equal(len(mod.interface()._wrapper.storage['item']), 3) - def test_remove_item(self): + def test_remove_assembly(self): m1 = Material(6.908, -0.278, 'Boron') m2 = Material(0.487, 0.000, 'Potassium') l1 = Layer(m1, 5.0, 2.0, 'thinBoron') @@ -265,12 +265,12 @@ def test_remove_item(self): resolution_function = PercentageFhwm(2.0) mod = Model(d, 2, 1e-5, resolution_function, 'newModel') assert_equal(len(mod.sample), 1) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.sample), 2) - mod.remove_item(0) + mod.remove_assembly(0) assert_equal(len(mod.sample), 1) - def test_remove_item_with_interface_refnx(self): + def test_remove_assembly_with_interface_refnx(self): interface = CalculatorFactory() m1 = Material(6.908, -0.278, 'Boron') m2 = Material(0.487, 0.000, 'Potassium') @@ -285,14 +285,14 @@ def test_remove_item_with_interface_refnx(self): mod = Model(d, 2, 1e-5, resolution_function, 'newModel', interface=interface) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.interface()._wrapper.storage['item']), 2) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - mod.remove_item(0) + mod.remove_assembly(0) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - def test_remove_item_with_interface_refl1d(self): + def test_remove_assembly_with_interface_refl1d(self): interface = CalculatorFactory() interface.switch('refl1d') m1 = Material(6.908, -0.278, 'Boron') @@ -308,14 +308,14 @@ def test_remove_item_with_interface_refl1d(self): mod = Model(d, 2, 1e-5, resolution_function, 'newModel', interface=interface) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - mod.add_item(o2) + mod.add_assemblies(o2) assert_equal(len(mod.interface()._wrapper.storage['item']), 2) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - mod.remove_item(0) + mod.remove_assembly(0) assert_equal(len(mod.interface()._wrapper.storage['item']), 1) assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - # def test_remove_item_with_interface_bornagain(self): + # def test_remove_assembly_with_interface_bornagain(self): # interface = CalculatorFactory() # interface.switch('BornAgain') # m1 = Material.from_pars(6.908, 0.278, 'Boron') @@ -330,10 +330,10 @@ def test_remove_item_with_interface_refl1d(self): # mod = Model(d, 2, 1e-5, 2.0, 'newModel', interface=interface) # assert_equal(len(mod.interface()._wrapper.storage['item']), 1) # assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - # mod.add_item(o2) + # mod.add_assemblies(o2) # assert_equal(len(mod.interface()._wrapper.storage['item']), 2) # assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) - # mod.remove_item(0) + # mod.remove_assembly(0) # assert_equal(len(mod.interface()._wrapper.storage['item']), 1) # assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) @@ -401,7 +401,7 @@ def test_dict_round_trip(interface): model = Model(interface=interface) model.resolution_function = resolution_function for additional_layer in [SurfactantLayer(), Multilayer(), RepeatingMultilayer()]: - model.add_item(additional_layer) + model.add_assemblies(additional_layer) src_dict = model.as_dict() global_object.map._clear() diff --git a/tests/experiment/test_model_collection.py b/tests/model/test_model_collection.py similarity index 95% rename from tests/experiment/test_model_collection.py rename to tests/model/test_model_collection.py index 02f0ec4b..8614efb8 100644 --- a/tests/experiment/test_model_collection.py +++ b/tests/model/test_model_collection.py @@ -1,7 +1,7 @@ from easyscience import global_object -from easyreflectometry.experiment.model import Model -from easyreflectometry.experiment.model_collection import ModelCollection +from easyreflectometry.model.model import Model +from easyreflectometry.model.model_collection import ModelCollection class TestModelCollection: diff --git a/tests/experiment/test_resolution_functions.py b/tests/model/test_resolution_functions.py similarity index 88% rename from tests/experiment/test_resolution_functions.py rename to tests/model/test_resolution_functions.py index ab8eb614..ff5b1816 100644 --- a/tests/experiment/test_resolution_functions.py +++ b/tests/model/test_resolution_functions.py @@ -2,10 +2,10 @@ import numpy as np -from easyreflectometry.experiment.resolution_functions import DEFAULT_RESOLUTION_FWHM_PERCENTAGE -from easyreflectometry.experiment.resolution_functions import LinearSpline -from easyreflectometry.experiment.resolution_functions import PercentageFhwm -from easyreflectometry.experiment.resolution_functions import ResolutionFunction +from easyreflectometry.model.resolution_functions import DEFAULT_RESOLUTION_FWHM_PERCENTAGE +from easyreflectometry.model.resolution_functions import LinearSpline +from easyreflectometry.model.resolution_functions import PercentageFhwm +from easyreflectometry.model.resolution_functions import ResolutionFunction class TestPercentageFhwm(unittest.TestCase): diff --git a/tests/sample/assemblies/test_multilayer.py b/tests/sample/assemblies/test_multilayer.py index c1ee3fd0..878c4c34 100644 --- a/tests/sample/assemblies/test_multilayer.py +++ b/tests/sample/assemblies/test_multilayer.py @@ -13,8 +13,8 @@ from easyreflectometry.calculators.factory import CalculatorFactory from easyreflectometry.sample.assemblies.multilayer import Multilayer +from easyreflectometry.sample.collections.layer_collection import LayerCollection from easyreflectometry.sample.elements.layers.layer import Layer -from easyreflectometry.sample.elements.layers.layer_collection import LayerCollection from easyreflectometry.sample.elements.materials.material import Material diff --git a/tests/sample/assemblies/test_repeating_multilayer.py b/tests/sample/assemblies/test_repeating_multilayer.py index ce585992..e3c173d2 100644 --- a/tests/sample/assemblies/test_repeating_multilayer.py +++ b/tests/sample/assemblies/test_repeating_multilayer.py @@ -14,8 +14,8 @@ from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.sample.assemblies.repeating_multilayer import RepeatingMultilayer +from easyreflectometry.sample.collections.layer_collection import LayerCollection from easyreflectometry.sample.elements.layers.layer import Layer -from easyreflectometry.sample.elements.layers.layer_collection import LayerCollection from easyreflectometry.sample.elements.materials.material import Material diff --git a/tests/sample/elements/layers/test_layer_collection.py b/tests/sample/collections/test_layer_collection.py similarity index 97% rename from tests/sample/elements/layers/test_layer_collection.py rename to tests/sample/collections/test_layer_collection.py index c2c0bec2..2e17d3ab 100644 --- a/tests/sample/elements/layers/test_layer_collection.py +++ b/tests/sample/collections/test_layer_collection.py @@ -11,8 +11,8 @@ from numpy.testing import assert_equal from easyreflectometry.sample.assemblies.repeating_multilayer import RepeatingMultilayer +from easyreflectometry.sample.collections.layer_collection import LayerCollection from easyreflectometry.sample.elements.layers.layer import Layer -from easyreflectometry.sample.elements.layers.layer_collection import LayerCollection from easyreflectometry.sample.elements.materials.material import Material diff --git a/tests/sample/elements/materials/test_material_collection.py b/tests/sample/collections/test_material_collection.py similarity index 95% rename from tests/sample/elements/materials/test_material_collection.py rename to tests/sample/collections/test_material_collection.py index cdfc47b0..37c652b8 100644 --- a/tests/sample/elements/materials/test_material_collection.py +++ b/tests/sample/collections/test_material_collection.py @@ -9,8 +9,8 @@ from easyscience import global_object +from easyreflectometry.sample.collections.material_collection import MaterialCollection from easyreflectometry.sample.elements.materials.material import Material -from easyreflectometry.sample.elements.materials.material_collection import MaterialCollection class TestMaterialCollection(unittest.TestCase): diff --git a/tests/sample/collections/test_sample.py b/tests/sample/collections/test_sample.py new file mode 100644 index 00000000..92e5c1b8 --- /dev/null +++ b/tests/sample/collections/test_sample.py @@ -0,0 +1,305 @@ +""" +Tests for Sample class. +""" + +__author__ = 'github.com/arm61' +__version__ = '0.0.1' + +from unittest.mock import MagicMock + +import pytest +from easyscience import global_object +from numpy.testing import assert_equal + +from easyreflectometry.sample import Layer +from easyreflectometry.sample import LayerCollection +from easyreflectometry.sample import Material +from easyreflectometry.sample import Multilayer +from easyreflectometry.sample import RepeatingMultilayer +from easyreflectometry.sample import Sample +from easyreflectometry.sample import SurfactantLayer + + +class TestSample: + def test_default(self): + # When Then + p = Sample() + + # Expect + assert_equal(p.name, 'EasySample') + assert_equal(p.interface, None) + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + + def test_add_assembly(self): + # When + p = Sample() + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + surfactant = SurfactantLayer() + + # Then + p.add_assembly(surfactant) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + assert_equal(p[2].name, 'EasySurfactantLayer') + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + # Problems with parameterized tests START + def test_duplicate_assembly_multilayer(self): + # When + assembly_to_duplicate = Multilayer() + p = Sample() + p.add_assembly(assembly_to_duplicate) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.duplicate_assembly(2) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + assert_equal(p[2].name, assembly_to_duplicate.name) + assert_equal(p[3].name, assembly_to_duplicate.name + ' duplicate') + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + def test_duplicate_assembly_repeating_multilayer(self): + # When + assembly_to_duplicate = RepeatingMultilayer() + p = Sample() + p.add_assembly(assembly_to_duplicate) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.duplicate_assembly(2) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + assert_equal(p[2].name, assembly_to_duplicate.name) + assert_equal(p[3].name, assembly_to_duplicate.name + ' duplicate') + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + def test_duplicate_assembly_surfactant(self): + # When + assembly_to_duplicate = SurfactantLayer() + p = Sample() + p.add_assembly(assembly_to_duplicate) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.duplicate_assembly(2) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + assert_equal(p[2].name, assembly_to_duplicate.name) + assert_equal(p[3].name, assembly_to_duplicate.name + ' duplicate') + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + # Problems with parameterized tests END + + def test_move_assembly_up(self): + # When + p = Sample() + surfactant = SurfactantLayer() + p.add_assembly(surfactant) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.move_assembly_up(2) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, surfactant.name) + assert_equal(p[2].name, 'EasyMultilayer') + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + def test_move_assembly_up_index_0(self): + # When + p = Sample() + surfactant = SurfactantLayer() + p.add_assembly(surfactant) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.move_assembly_up(0) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + assert_equal(p[2].name, surfactant.name) + p._enable_changes_to_outermost_layers.assert_not_called() + p._disable_changes_to_outermost_layers.assert_not_called() + + def test_move_assembly_down(self): + # When + p = Sample() + surfactant = SurfactantLayer() + p.add_assembly(surfactant) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.move_assembly_down(1) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, surfactant.name) + assert_equal(p[2].name, 'EasyMultilayer') + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + def test_move_assembly_down_index_2(self): + # When + p = Sample() + surfactant = SurfactantLayer() + p.add_assembly(surfactant) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.move_assembly_down(2) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, 'EasyMultilayer') + assert_equal(p[2].name, surfactant.name) + p._enable_changes_to_outermost_layers.assert_not_called() + p._disable_changes_to_outermost_layers.assert_not_called() + + def test_remove_assembly(self): + # When + p = Sample() + surfactant = SurfactantLayer() + p.add_assembly(surfactant) + p._enable_changes_to_outermost_layers = MagicMock() + p._disable_changes_to_outermost_layers = MagicMock() + + # Then + p.remove_assembly(1) + + # Expect + assert_equal(p[0].name, 'EasyMultilayer') + assert_equal(p[1].name, surfactant.name) + p._enable_changes_to_outermost_layers.assert_called_once_with() + p._disable_changes_to_outermost_layers.assert_called_once_with() + + def test_subphase(self): + # When + p = Sample() + layer = Multilayer(Layer(name='new layer')) + p.add_assembly(layer) + + # Then + layer = p.subphase + + # Expect + assert_equal(layer.name, 'new layer') + + def test_superphase(self): + # When + p = Sample() + layer = Multilayer(Layer(name='new layer')) + p.add_assembly(layer) + p.move_assembly_up(2) + p.move_assembly_up(1) + + # Then + layer = p.superphase + + # Expect + assert_equal(layer.name, 'new layer') + + def test_enable_changes_to_outermost_layers(self): + # When + p = Sample() + p.superphase.thickness.enabled = False + p.superphase.roughness.enabled = False + p.subphase.thickness.enabled = False + + # Then + p._enable_changes_to_outermost_layers() + + # Expect + assert_equal(p.superphase.thickness.enabled, True) + assert_equal(p.superphase.roughness.enabled, True) + assert_equal(p.subphase.thickness.enabled, True) + + def test_disable_changes_to_outermost_layers(self): + # When + p = Sample() + p.superphase.thickness.enabled = True + p.superphase.roughness.enabled = True + p.subphase.thickness.enabled = True + + # Then + p._disable_changes_to_outermost_layers() + + # Expect + assert_equal(p.superphase.thickness.enabled, False) + assert_equal(p.superphase.roughness.enabled, False) + assert_equal(p.subphase.thickness.enabled, False) + + def test_from_pars(self): + # When + m1 = Material(6.908, -0.278, 'Boron') + m2 = Material(0.487, 0.000, 'Potassium') + l1 = Layer(m1, 5.0, 2.0, 'thinBoron') + l2 = Layer(m2, 50.0, 1.0, 'thickPotassium') + ls1 = LayerCollection(l1, l2, name='twoLayer1') + ls2 = LayerCollection(l2, l1, name='twoLayer2') + o1 = RepeatingMultilayer(ls1, 2.0, 'twoLayerItem1') + o2 = RepeatingMultilayer(ls2, 1.0, 'oneLayerItem2') + + # Then + d = Sample(o1, o2, name='myModel') + + # Expect + assert_equal(d.name, 'myModel') + assert_equal(d.interface, None) + assert_equal(d[0].name, 'twoLayerItem1') + assert_equal(d[1].name, 'oneLayerItem2') + + def test_from_pars_error(self): + m1 = Material(6.908, -0.278, 'Boron') + + with pytest.raises(ValueError): + _ = Sample(m1, name='myModel') + + def test_repr(self): + p = Sample() + assert ( + p.__repr__() + == 'EasySample:\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + ) + + def test_dict_round_trip(self): + # When + p = Sample() + surfactant = SurfactantLayer() + p.append(surfactant) + multilayer = Multilayer() + p.append(multilayer) + repeating = RepeatingMultilayer() + p.append(repeating) + p_dict = p.as_dict() + global_object.map._clear() + + # Then + q = Sample.from_dict(p_dict) + + # Expect + assert sorted(p.as_data_dict()) == sorted(q.as_data_dict()) diff --git a/tests/sample/test_sample.py b/tests/sample/test_sample.py deleted file mode 100644 index 0a916173..00000000 --- a/tests/sample/test_sample.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Tests for Sample class. -""" - -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - -import unittest - -from easyscience import global_object -from numpy.testing import assert_equal - -from easyreflectometry.sample import Layer -from easyreflectometry.sample import LayerCollection -from easyreflectometry.sample import Material -from easyreflectometry.sample import Multilayer -from easyreflectometry.sample import RepeatingMultilayer -from easyreflectometry.sample import Sample -from easyreflectometry.sample import SurfactantLayer - - -class TestSample(unittest.TestCase): - def test_default(self): - p = Sample() - assert_equal(p.name, 'EasySample') - assert_equal(p.interface, None) - assert_equal(p[0].name, 'EasyMultilayer') - assert_equal(p[1].name, 'EasyMultilayer') - - def test_from_pars(self): - m1 = Material(6.908, -0.278, 'Boron') - m2 = Material(0.487, 0.000, 'Potassium') - l1 = Layer(m1, 5.0, 2.0, 'thinBoron') - l2 = Layer(m2, 50.0, 1.0, 'thickPotassium') - ls1 = LayerCollection(l1, l2, name='twoLayer1') - ls2 = LayerCollection(l2, l1, name='twoLayer2') - o1 = RepeatingMultilayer(ls1, 2.0, 'twoLayerItem1') - o2 = RepeatingMultilayer(ls2, 1.0, 'oneLayerItem2') - d = Sample(o1, o2, name='myModel') - assert_equal(d.name, 'myModel') - assert_equal(d.interface, None) - assert_equal(d[0].name, 'twoLayerItem1') - assert_equal(d[1].name, 'oneLayerItem2') - - def test_from_pars_layers(self): - m1 = Material(6.908, -0.278, 'Boron') - m2 = Material(0.487, 0.000, 'Potassium') - l1 = Layer(m1, 5.0, 2.0, 'thinBoron') - l2 = Layer(m2, 50.0, 1.0, 'thickPotassium') - d = Sample(l1, l2, name='myModel') - assert_equal(d.name, 'myModel') - assert_equal(d.interface, None) - assert_equal(d[0].name, 'thinBoron') - assert_equal(d[1].name, 'thickPotassium') - - def test_from_pars_error(self): - m1 = Material(6.908, -0.278, 'Boron') - with self.assertRaises(ValueError): - _ = Sample(m1, name='myModel') - - def test_repr(self): - p = Sample() - assert ( - p.__repr__() - == 'EasySample:\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 - ) - - def test_dict_round_trip(self): - # When - p = Sample() - surfactant = SurfactantLayer() - p.append(surfactant) - multilayer = Multilayer() - p.append(multilayer) - repeating = RepeatingMultilayer() - p.append(repeating) - p_dict = p.as_dict() - global_object.map._clear() - - # Then - q = Sample.from_dict(p_dict) - - assert sorted(p.as_data_dict()) == sorted(q.as_data_dict()) diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 42948feb..727c90a1 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -8,11 +8,12 @@ import easyreflectometry from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.data import load -from easyreflectometry.experiment import Model -from easyreflectometry.experiment import PercentageFhwm from easyreflectometry.fitting import Fitter +from easyreflectometry.model import Model +from easyreflectometry.model import PercentageFhwm from easyreflectometry.sample import Layer from easyreflectometry.sample import Material +from easyreflectometry.sample import Multilayer from easyreflectometry.sample import Sample PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests', '_static') @@ -26,10 +27,10 @@ def test_fitting(minimizer): sio2 = Material(3.47, 0, 'SiO2') film = Material(2.0, 0, 'Film') d2o = Material(6.36, 0, 'D2O') - si_layer = Layer(si, 0, 0, 'Si layer') - sio2_layer = Layer(sio2, 30, 3, 'SiO2 layer') - film_layer = Layer(film, 250, 3, 'Film Layer') - superphase = Layer(d2o, 0, 3, 'D2O Subphase') + si_layer = Multilayer(Layer(si, 0, 0, 'Si layer')) + sio2_layer = Multilayer(Layer(sio2, 30, 3, 'SiO2 layer')) + film_layer = Multilayer(Layer(film, 250, 3, 'Film Layer')) + superphase = Multilayer(Layer(d2o, 0, 3, 'D2O Subphase')) sample = Sample( si_layer, sio2_layer, @@ -40,12 +41,12 @@ def test_fitting(minimizer): resolution_function = PercentageFhwm(0.02) model = Model(sample, 1, 1e-6, resolution_function, 'Film Model') # Thicknesses - sio2_layer.thickness.bounds = (15, 50) - film_layer.thickness.bounds = (200, 300) + sio2_layer.layers[0].thickness.bounds = (15, 50) + film_layer.layers[0].thickness.bounds = (200, 300) # Roughnesses - sio2_layer.roughness.bounds = (1, 15) - film_layer.roughness.bounds = (1, 15) - superphase.roughness.bounds = (1, 15) + sio2_layer.layers[0].roughness.bounds = (1, 15) + film_layer.layers[0].roughness.bounds = (1, 15) + superphase.layers[0].roughness.bounds = (1, 15) # Scattering length density film.sld.bounds = (0.1, 3) # Background diff --git a/tests/test_topmost_nesting.py b/tests/test_topmost_nesting.py index 585dc8da..a270b72d 100644 --- a/tests/test_topmost_nesting.py +++ b/tests/test_topmost_nesting.py @@ -8,8 +8,8 @@ from numpy.testing import assert_almost_equal from easyreflectometry.calculators import CalculatorFactory -from easyreflectometry.experiment import LinearSpline -from easyreflectometry.experiment import Model +from easyreflectometry.model import LinearSpline +from easyreflectometry.model import Model from easyreflectometry.sample import Multilayer from easyreflectometry.sample import RepeatingMultilayer from easyreflectometry.sample import SurfactantLayer @@ -20,8 +20,7 @@ def test_dict_skip_unique_name(): resolution_function = LinearSpline([0, 10], [0, 10]) model = Model(interface=CalculatorFactory()) model.resolution_function = resolution_function - for additional_layer in [SurfactantLayer(), Multilayer(), RepeatingMultilayer()]: - model.add_item(additional_layer) + model.add_assemblies(SurfactantLayer(), Multilayer(), RepeatingMultilayer()) # Then dict_no_unique_name = model.as_dict(skip=['unique_name']) @@ -35,8 +34,7 @@ def test_copy(): resolution_function = LinearSpline([0, 10], [0, 10]) model = Model(interface=CalculatorFactory()) model.resolution_function = resolution_function - for additional_layer in [SurfactantLayer(), Multilayer(), RepeatingMultilayer()]: - model.add_item(additional_layer) + model.add_assemblies(SurfactantLayer(), Multilayer(), RepeatingMultilayer()) # Then model_copy = copy(model) From 2c4bc500497f9be48fed295640ab409bf2e19f8c Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:22:07 +0200 Subject: [PATCH 02/11] data folder (#187) --- src/easyreflectometry/data/__init__.py | 7 + src/easyreflectometry/data/data_store.py | 129 ++++++++++++++++++ .../{data.py => data/measurement.py} | 0 src/easyreflectometry/measurement/data.py | 11 -- tests/test_data.py | 8 +- tests/test_fitting.py | 2 +- 6 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 src/easyreflectometry/data/__init__.py create mode 100644 src/easyreflectometry/data/data_store.py rename src/easyreflectometry/{data.py => data/measurement.py} (100%) delete mode 100644 src/easyreflectometry/measurement/data.py diff --git a/src/easyreflectometry/data/__init__.py b/src/easyreflectometry/data/__init__.py new file mode 100644 index 00000000..3676d427 --- /dev/null +++ b/src/easyreflectometry/data/__init__.py @@ -0,0 +1,7 @@ +from .data_store import ProjectData +from .measurement import load + +__all__ = [ + load, + ProjectData, +] diff --git a/src/easyreflectometry/data/data_store.py b/src/easyreflectometry/data/data_store.py new file mode 100644 index 00000000..65965b8b --- /dev/null +++ b/src/easyreflectometry/data/data_store.py @@ -0,0 +1,129 @@ +__author__ = 'github.com/wardsimon' + +from collections.abc import Sequence +from typing import Optional +from typing import TypeVar +from typing import Union + +import numpy as np +from easyscience.Objects.core import ComponentSerializer +from easyscience.Utils.io.dict import DictSerializer + +from easyreflectometry.model import Model + +T = TypeVar('T') + + +class ProjectData(ComponentSerializer): + def __init__(self, name='DataStore', exp_data=None, sim_data=None): + self.name = name + if exp_data is None: + exp_data = DataStore(name='Exp Datastore') + if sim_data is None: + sim_data = DataStore(name='Sim Datastore') + self.exp_data = exp_data + self.sim_data = sim_data + + +class DataStore(Sequence, ComponentSerializer): + def __init__(self, *args, name='DataStore'): + self.name = name + self.items = list(args) + self.show_legend = False + + def __getitem__(self, i: int) -> T: + return self.items.__getitem__(i) + + def __len__(self) -> int: + return len(self.items) + + def __setitem__(self, key, value): + self.items[key] = value + + def __delitem__(self, key): + del self.items[key] + + def append(self, *args): + self.items.append(*args) + + def as_dict(self, skip: list = []) -> dict: + this_dict = super(DataStore, self).as_dict(self, skip=skip) + this_dict['items'] = [item.as_dict() for item in self.items if hasattr(item, 'as_dict')] + + @classmethod + def from_dict(cls, d): + items = d['items'] + del d['items'] + obj = cls.from_dict(d) + decoder = DictSerializer() + obj.items = [decoder.decode(item) for item in items] + return obj + + @property + def experiments(self): + return [self[idx] for idx in range(len(self)) if self[idx].is_experiment] + + @property + def simulations(self): + return [self[idx] for idx in range(len(self)) if self[idx].is_simulation] + + +class DataSet1D(ComponentSerializer): + def __init__( + self, + name: str = 'Series', + x: Optional[Union[np.ndarray, list]] = None, + y: Optional[Union[np.ndarray, list]] = None, + ye: Optional[Union[np.ndarray, list]] = None, + xe: Optional[Union[np.ndarray, list]] = None, + model: Optional[Model] = None, + x_label: str = 'x', + y_label: str = 'y', + ): + self._model = model + self._model.background = np.min(y) + + if x is None: + x = np.array([]) + if y is None: + y = np.array([]) + if ye is None: + ye = np.zeros_like(x) + if xe is None: + xe = np.zeros_like(x) + + self.name = name + if not isinstance(x, np.ndarray): + x = np.array(x) + if not isinstance(y, np.ndarray): + y = np.array(y) + + self.x = x + self.y = y + self.ye = ye + self.xe = xe + + self.x_label = x_label + self.y_label = y_label + + self._color = None + + @property + def model(self) -> Model: + return self._model + + @model.setter + def model(self, new_model: Model) -> None: + self._model = new_model + self._model.background = np.min(self.y) + + @property + def is_experiment(self) -> bool: + return self._model is not None + + @property + def is_simulation(self) -> bool: + return self._model is None + + def __repr__(self) -> str: + return "1D DataStore of '{:s}' Vs '{:s}' with {} data points".format(self.x_label, self.y_label, len(self.x)) diff --git a/src/easyreflectometry/data.py b/src/easyreflectometry/data/measurement.py similarity index 100% rename from src/easyreflectometry/data.py rename to src/easyreflectometry/data/measurement.py diff --git a/src/easyreflectometry/measurement/data.py b/src/easyreflectometry/measurement/data.py deleted file mode 100644 index f45bffc0..00000000 --- a/src/easyreflectometry/measurement/data.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'github.com/arm61' - -from typing import TextIO -from typing import Union - -import scipp as sc -from orsopy.fileio import orso - - -def load(fname: Union[TextIO, str]) -> sc.DataGroup: - return orso.load_orso(fname) diff --git a/tests/test_data.py b/tests/test_data.py index ded2107e..57418adf 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -10,11 +10,11 @@ from orsopy.fileio import load_orso import easyreflectometry -from easyreflectometry.data import _load_orso -from easyreflectometry.data import _load_txt -from easyreflectometry.data import load +from easyreflectometry.data.measurement import _load_orso +from easyreflectometry.data.measurement import _load_txt +from easyreflectometry.data.measurement import load -PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests' , '_static') +PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests', '_static') class TestData(unittest.TestCase): diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 727c90a1..09a367e4 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -7,7 +7,7 @@ import easyreflectometry from easyreflectometry.calculators import CalculatorFactory -from easyreflectometry.data import load +from easyreflectometry.data.measurement import load from easyreflectometry.fitting import Fitter from easyreflectometry.model import Model from easyreflectometry.model import PercentageFhwm From 9a0b0cb979cac48c5ef2ff4f8301a1f78e79c3c7 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:40:05 +0200 Subject: [PATCH 03/11] 184 extract functionality from easyreflectometryapp proxies material to lib (#185) * renaming from item to assembly * collections are together in folder * moving responsibilities from model to sample * rename experiment to model * test for sample * ruff * get classes from module * functionality for material collection * added default collection * add material adjusted * model will create assembly if called with none * Collections are now with just a single element * code cleaning * default collections * base collection introduced * no need for base_element_collection * introduced default collection * names in base collection * tests --- pyproject.toml | 2 +- src/easyreflectometry/model/model.py | 21 ++-- .../model/model_collection.py | 17 ++- .../sample/assemblies/multilayer.py | 6 +- .../sample/assemblies/repeating_multilayer.py | 3 +- ...ement_collection.py => base_collection.py} | 29 +++-- .../sample/collections/layer_collection.py | 7 +- .../sample/collections/material_collection.py | 63 ++++++++-- .../sample/collections/sample.py | 35 ++---- tests/model/test_model.py | 4 +- tests/model/test_model_collection.py | 3 +- tests/sample/assemblies/test_multilayer.py | 4 +- .../assemblies/test_repeating_multilayer.py | 4 +- .../collections/test_base_collection.py | 81 ++++++++++++ .../collections/test_material_collection.py | 119 ++++++++++++++++-- tests/sample/collections/test_sample.py | 2 +- tests/test_fitting.py | 26 ++-- 17 files changed, 314 insertions(+), 112 deletions(-) rename src/easyreflectometry/sample/collections/{base_element_collection.py => base_collection.py} (75%) create mode 100644 tests/sample/collections/test_base_collection.py diff --git a/pyproject.toml b/pyproject.toml index 6699841c..e8de39c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] requires-python = ">=3.9,<3.13" dependencies = [ - 'easyscience>=1.1.0', + 'easyscience @ git+https://github.com/EasyScience/EasyScience.git@develop', "scipp>=23.12.0", "refnx>=0.1.15", "refl1d>=0.8.14", diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index 98214ed6..c0247387 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -92,17 +92,22 @@ def __init__( self.interface = interface def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None: - """Add a layer or item to the model sample. + """Add assemblies to the model sample. :param assemblies: Assemblies to add to model sample. """ - for assembly in assemblies: - if issubclass(assembly.__class__, BaseAssembly): - self.sample.add_assembly(assembly) - if self.interface is not None: - self.interface().add_item_to_model(assembly.unique_name, self.unique_name) - else: - raise ValueError(f'Object {assembly} is not a valid type, must be a child of BaseAssembly.') + if not assemblies: + self.sample.add_assembly() + if self.interface is not None: + self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name) + else: + for assembly in assemblies: + if issubclass(assembly.__class__, BaseAssembly): + self.sample.add_assembly(assembly) + if self.interface is not None: + self.interface().add_item_to_model(self.sample[-1].unique_name, self.unique_name) + else: + raise ValueError(f'Object {assembly} is not a valid type, must be a child of BaseAssembly.') def duplicate_assembly(self, index: int) -> None: """Duplicate a given item or layer in a sample. diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index 5788afe0..7ebbe355 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -5,16 +5,14 @@ from typing import List from typing import Tuple -from easyreflectometry.sample.collections.base_element_collection import SIZE_DEFAULT_COLLECTION -from easyreflectometry.sample.collections.base_element_collection import BaseElementCollection +from easyreflectometry.sample.collections.base_collection import BaseCollection from .model import Model +DEFAULT_COLLECTION = [Model()] -class ModelCollection(BaseElementCollection): - # Added in super().__init__ - models: list[Model] +class ModelCollection(BaseCollection): def __init__( self, *models: Tuple[Model], @@ -25,7 +23,7 @@ def __init__( ): if not models: if populate_if_none: - models = [Model(interface=interface) for _ in range(SIZE_DEFAULT_COLLECTION)] + models = self._make_default_collection(DEFAULT_COLLECTION, interface) else: models = [] # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict @@ -33,7 +31,6 @@ def __init__( self.populate_if_none = False super().__init__(name, interface, *models, **kwargs) - self.interface = interface def add_model(self, new_model: Model): """ @@ -43,13 +40,13 @@ def add_model(self, new_model: Model): """ self.append(new_model) - def remove_model(self, idx: int): + def remove_model(self, index: int): """ Remove an model from the models. - :param idx: Index of the model to remove + :param index: Index of the model to remove """ - del self[idx] + self.pop(index) def as_dict(self, skip: List[str] | None = None) -> dict: this_dict = super().as_dict(skip=skip) diff --git a/src/easyreflectometry/sample/assemblies/multilayer.py b/src/easyreflectometry/sample/assemblies/multilayer.py index 320ba1df..9f3307b6 100644 --- a/src/easyreflectometry/sample/assemblies/multilayer.py +++ b/src/easyreflectometry/sample/assemblies/multilayer.py @@ -3,8 +3,6 @@ from typing import Optional from typing import Union -from easyreflectometry.sample.collections.base_element_collection import SIZE_DEFAULT_COLLECTION - from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer from .base_assembly import BaseAssembly @@ -38,7 +36,7 @@ def __init__( """ if layers is None: if populate_if_none: - layers = LayerCollection([Layer(interface=interface) for _ in range(SIZE_DEFAULT_COLLECTION)]) + layers = LayerCollection([Layer(interface=interface)]) else: layers = LayerCollection() elif isinstance(layers, Layer): @@ -90,8 +88,6 @@ def remove_layer(self, idx: int) -> None: @property def _dict_repr(self) -> dict: """A simplified dict representation.""" - if len(self.layers) == 1: - return self.front_layer._dict_repr return {self.name: self.layers._dict_repr} @classmethod diff --git a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py index 5aa9d697..800603f1 100644 --- a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py +++ b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py @@ -5,7 +5,6 @@ from easyscience.Objects.new_variable import Parameter from easyreflectometry.parameter_utils import get_as_parameter -from easyreflectometry.sample.collections.base_element_collection import SIZE_DEFAULT_COLLECTION from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer @@ -57,7 +56,7 @@ def __init__( if layers is None: if populate_if_none: - layers = LayerCollection([Layer(interface=interface) for _ in range(SIZE_DEFAULT_COLLECTION)]) + layers = LayerCollection([Layer(interface=interface)]) else: layers = LayerCollection() elif isinstance(layers, Layer): diff --git a/src/easyreflectometry/sample/collections/base_element_collection.py b/src/easyreflectometry/sample/collections/base_collection.py similarity index 75% rename from src/easyreflectometry/sample/collections/base_element_collection.py rename to src/easyreflectometry/sample/collections/base_collection.py index ab839920..6a3842b5 100644 --- a/src/easyreflectometry/sample/collections/base_element_collection.py +++ b/src/easyreflectometry/sample/collections/base_collection.py @@ -1,14 +1,13 @@ +from copy import deepcopy from typing import List from typing import Optional -from easyscience.Objects.Groups import BaseCollection +from easyscience.Objects.Groups import BaseCollection as EasyBaseCollection from easyreflectometry.parameter_utils import yaml_dump -SIZE_DEFAULT_COLLECTION = 2 - -class BaseElementCollection(BaseCollection): +class BaseCollection(EasyBaseCollection): def __init__( self, name: str, @@ -19,13 +18,6 @@ def __init__( super().__init__(name, *args, **kwargs) self.interface = interface - @property - def names(self) -> list: - """ - :returns: list of names for the elements in the collection. - """ - return [i.name for i in self] - def __repr__(self) -> str: """ String representation of the collection. @@ -34,6 +26,13 @@ def __repr__(self) -> str: """ return yaml_dump(self._dict_repr) + @property + def names(self) -> list: + """ + :returns: list of names for the elements in the collection. + """ + return [i.name for i in self] + @property def _dict_repr(self) -> dict: """ @@ -43,12 +42,20 @@ def _dict_repr(self) -> dict: """ return {self.name: [i._dict_repr for i in self]} + def _make_default_collection(self, default_collection: List, interface) -> List: + elements = deepcopy(default_collection) + for element in elements: + element.interface = interface + return elements + def as_dict(self, skip: Optional[List[str]] = None) -> dict: """ Create a dictionary representation of the collection. :return: A dictionary representation of the collection """ + if skip is None: + skip = [] this_dict = super().as_dict(skip=skip) this_dict['data'] = [] for collection_element in self: diff --git a/src/easyreflectometry/sample/collections/layer_collection.py b/src/easyreflectometry/sample/collections/layer_collection.py index a72b7871..cbed822d 100644 --- a/src/easyreflectometry/sample/collections/layer_collection.py +++ b/src/easyreflectometry/sample/collections/layer_collection.py @@ -3,13 +3,10 @@ from typing import Optional from ..elements.layers.layer import Layer -from .base_element_collection import BaseElementCollection +from .base_collection import BaseCollection -class LayerCollection(BaseElementCollection): - # Added in super().__init__ - layers: list[Layer] - +class LayerCollection(BaseCollection): def __init__( self, *layers: Optional[list[Layer]], diff --git a/src/easyreflectometry/sample/collections/material_collection.py b/src/easyreflectometry/sample/collections/material_collection.py index 07833090..ac5b89b6 100644 --- a/src/easyreflectometry/sample/collections/material_collection.py +++ b/src/easyreflectometry/sample/collections/material_collection.py @@ -1,20 +1,21 @@ __author__ = 'github.com/arm61' +from typing import Optional from typing import Tuple -from typing import Union from ..elements.materials.material import Material -from ..elements.materials.material_mixture import MaterialMixture -from .base_element_collection import SIZE_DEFAULT_COLLECTION -from .base_element_collection import BaseElementCollection +from .base_collection import BaseCollection +DEFAULT_COLLECTION = ( + Material(sld=0.0, isld=0.0, name='Air'), + Material(sld=6.335, isld=0.0, name='D2O'), + Material(sld=2.074, isld=0.0, name='Si'), +) -class MaterialCollection(BaseElementCollection): - # Added in super().__init__ - matertials: list[Union[Material, MaterialMixture]] +class MaterialCollection(BaseCollection): def __init__( self, - *materials: Tuple[Union[Material, MaterialMixture]], + *materials: Tuple[Material], name: str = 'EasyMaterials', interface=None, populate_if_none: bool = True, @@ -22,7 +23,7 @@ def __init__( ): if not materials: # Empty tuple if no materials are provided if populate_if_none: - materials = [Material(interface=interface) for _ in range(SIZE_DEFAULT_COLLECTION)] + materials = self._make_default_collection(DEFAULT_COLLECTION, interface) else: materials = [] # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict @@ -35,3 +36,47 @@ def __init__( *materials, **kwargs, ) + + def add_material(self, material: Optional[Material] = None): + """Add a material to the collection. + + :param material: Material to add. + """ + if material is None: + material = Material(sld=2.074, isld=0.000, name='Si new', interface=self.interface) + self.append(material) + + def duplicate_material(self, index: int): + """Duplicate a material in the collection. + + :param material: Assembly to add. + """ + to_be_duplicated = self[index] + duplicate = Material.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + duplicate.name = duplicate.name + ' duplicate' + self.append(duplicate) + + def move_material_up(self, index: int): + """Move the material at the given index up in the collection. + + :param index: Index of the material to move up. + """ + if index == 0: + return + self.insert(index - 1, self.pop(index)) + + def move_material_down(self, index: int): + """Move the material at the given index down in the collection. + + :param index: Index of the material to move down. + """ + if index == len(self) - 1: + return + self.insert(index + 1, self.pop(index)) + + def remove_material(self, index: int): + """Remove the material at the given index from the collection. + + :param index: Index of the material to remove. + """ + self.pop(index) diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py index d5505ce9..02525173 100644 --- a/src/easyreflectometry/sample/collections/sample.py +++ b/src/easyreflectometry/sample/collections/sample.py @@ -5,17 +5,14 @@ from typing import List from typing import Optional -from easyscience.Objects.Groups import BaseCollection - -from easyreflectometry.parameter_utils import yaml_dump - from ..assemblies.base_assembly import BaseAssembly from ..assemblies.multilayer import Multilayer from ..assemblies.repeating_multilayer import RepeatingMultilayer from ..assemblies.surfactant_layer import SurfactantLayer from ..elements.layers.layer import Layer +from .base_collection import BaseCollection -NR_DEFAULT_ASSEMBLIES = 2 +DEFAULT_COLLECTION = [Multilayer(), Multilayer()] class Sample(BaseCollection): @@ -23,7 +20,7 @@ class Sample(BaseCollection): def __init__( self, - *list_assemblies: Optional[List[BaseAssembly]], + *assemblies: Optional[List[BaseAssembly]], name: str = 'EasySample', interface=None, populate_if_none: bool = True, @@ -35,20 +32,19 @@ def __init__( :param name: Name of the sample, defaults to 'EasySample'. :param interface: Calculator interface, defaults to `None`. """ - if not list_assemblies: + if not assemblies: if populate_if_none: - list_assemblies = [Multilayer(interface=interface) for _ in range(NR_DEFAULT_ASSEMBLIES)] + assemblies = self._make_default_collection(DEFAULT_COLLECTION, interface) else: - list_assemblies = [] + assemblies = [] # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict # Else collisions might occur in global_object.map self.populate_if_none = False - for assembly in list_assemblies: + for assembly in assemblies: if not issubclass(type(assembly), BaseAssembly): raise ValueError('The elements must be an Assembly.') - super().__init__(name, *list_assemblies, **kwargs) - self.interface = interface + super().__init__(name, interface, *assemblies, **kwargs) def add_assembly(self, assembly: Optional[BaseAssembly] = None): """Add an assembly to the sample. @@ -142,25 +138,12 @@ def _disable_changes_to_outermost_layers(self): self.subphase.thickness.enabled = False # Representation - @property - def _dict_repr(self) -> dict: - """A simplified dict representation.""" - return {self.name: [i._dict_repr for i in self]} - - def __repr__(self) -> str: - """String representation of the sample.""" - return yaml_dump(self._dict_repr) - - def as_dict(self, skip: list = None) -> dict: + def as_dict(self, skip: Optional[List[str]] = None) -> dict: """Produces a cleaned dict using a custom as_dict method to skip necessary things. The resulting dict matches the parameters in __init__ :param skip: List of keys to skip, defaults to `None`. """ - if skip is None: - skip = [] this_dict = super().as_dict(skip=skip) - for i, assembly in enumerate(self.data): - this_dict['data'][i] = assembly.as_dict(skip=skip) this_dict['populate_if_none'] = self.populate_if_none return this_dict diff --git a/tests/model/test_model.py b/tests/model/test_model.py index f0f3c496..32a8f62c 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -378,7 +378,7 @@ def test_repr(self): assert ( model.__repr__() - == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: 5.0 %\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: 5.0 %\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 ) def test_repr_resolution_function(self): @@ -387,7 +387,7 @@ def test_repr_resolution_function(self): model.resolution_function = resolution_function assert ( model.__repr__() - == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: function of Q\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: function of Q\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 ) diff --git a/tests/model/test_model_collection.py b/tests/model/test_model_collection.py index 8614efb8..44daa806 100644 --- a/tests/model/test_model_collection.py +++ b/tests/model/test_model_collection.py @@ -12,9 +12,8 @@ def test_default(self): # Expect assert collection.name == 'EasyModels' assert collection.interface is None - assert len(collection) == 2 + assert len(collection) == 1 assert collection[0].name == 'EasyModel' - assert collection[1].name == 'EasyModel' def test_from_pars(self): # When diff --git a/tests/sample/assemblies/test_multilayer.py b/tests/sample/assemblies/test_multilayer.py index 878c4c34..8631fa21 100644 --- a/tests/sample/assemblies/test_multilayer.py +++ b/tests/sample/assemblies/test_multilayer.py @@ -24,7 +24,7 @@ def test_default(self): assert_equal(p.name, 'EasyMultilayer') assert_equal(p._type, 'Multi-layer') assert_equal(p.interface, None) - assert_equal(len(p.layers), 2) + assert_equal(len(p.layers), 1) assert_equal(p.layers.name, 'EasyLayerCollection') def test_default_empty(self): @@ -158,7 +158,7 @@ def test_repr(self): p = Multilayer() assert ( p.__repr__() - == 'EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + == 'EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 ) def test_dict_round_trip(self): diff --git a/tests/sample/assemblies/test_repeating_multilayer.py b/tests/sample/assemblies/test_repeating_multilayer.py index e3c173d2..bd8c3b47 100644 --- a/tests/sample/assemblies/test_repeating_multilayer.py +++ b/tests/sample/assemblies/test_repeating_multilayer.py @@ -25,7 +25,7 @@ def test_default(self): assert_equal(p.name, 'EasyRepeatingMultilayer') assert_equal(p._type, 'Repeating Multi-layer') assert_equal(p.interface, None) - assert_equal(len(p.layers), 2) + assert_equal(len(p.layers), 1) assert_equal(p.repetitions.display_name, 'repetitions') assert_equal(str(p.repetitions.unit), 'dimensionless') assert_equal(p.repetitions.value, 1.0) @@ -186,7 +186,7 @@ def test_repr(self): p = RepeatingMultilayer(populate_if_none=True) assert ( p.__repr__() - == 'EasyRepeatingMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n repetitions: 1.0\n' # noqa: E501 + == 'EasyRepeatingMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n repetitions: 1.0\n' # noqa: E501 ) def test_dict_round_trip(self): diff --git a/tests/sample/collections/test_base_collection.py b/tests/sample/collections/test_base_collection.py new file mode 100644 index 00000000..6b3624d7 --- /dev/null +++ b/tests/sample/collections/test_base_collection.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock + +from easyreflectometry.sample.collections.base_collection import BaseCollection +from easyreflectometry.sample.elements.layers.layer import Layer + + +class TestBaseCollection: + def test_constructor(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + mock_interface = MagicMock() + + # Then + p = BaseCollection('name', mock_interface, elem_1, elem_2) + + # Expect + p._interface = mock_interface + len(p) == 2 + + def test_names(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + mock_interface = MagicMock() + + # Then + p = BaseCollection('name', mock_interface, elem_1, elem_2) + + # Expect + assert p.names == ['layer_1', 'layer_2'] + + def test_dict_repr(self): + # When + elem = Layer(name='layer') + mock_interface = MagicMock() + + # Then + p = BaseCollection('name', mock_interface, elem) + + # Expect + assert p._dict_repr == { + 'name': [ + { + 'layer': { + 'material': {'EasyMaterial': {'isld': '0.000e-6 1/Å^2', 'sld': '4.186e-6 1/Å^2'}}, + 'roughness': '3.300 Å', + 'thickness': '10.000 Å', + } + } + ] + } + + def test_as_dict(self): + # When + elem = Layer(name='layer') + mock_interface = MagicMock() + + # Then + p = BaseCollection('name', mock_interface, elem) + + # Expect + assert p.as_dict()['name'] == 'name' + assert len(p.as_dict()['data']) == 1 + assert p.as_dict()['data'][0]['name'] == 'layer' + + def test_make_default_collection(self): + # When + elem_1 = Layer(name='layer_1') + mock_interface_1 = MagicMock() + elem_2 = Layer(name='layer_2') + mock_interface_2 = MagicMock() + p = BaseCollection('name', mock_interface_1, elem_1) + + # Then + default_collection = p._make_default_collection([elem_2], mock_interface_2) + + # Expect + assert default_collection[0] != elem_2 + assert default_collection[0].name == 'layer_2' + assert default_collection[0].interface == mock_interface_2 diff --git a/tests/sample/collections/test_material_collection.py b/tests/sample/collections/test_material_collection.py index 37c652b8..ca85e3ee 100644 --- a/tests/sample/collections/test_material_collection.py +++ b/tests/sample/collections/test_material_collection.py @@ -2,25 +2,21 @@ Tests for LayerCollection class. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - -import unittest - from easyscience import global_object from easyreflectometry.sample.collections.material_collection import MaterialCollection from easyreflectometry.sample.elements.materials.material import Material -class TestMaterialCollection(unittest.TestCase): +class TestMaterialCollection: def test_default(self): p = MaterialCollection() assert p.name == 'EasyMaterials' assert p.interface is None - assert len(p) == 2 - assert p[0].name == 'EasyMaterial' - assert p[1].name == 'EasyMaterial' + assert len(p) == 3 + assert p[0].name == 'Air' + assert p[1].name == 'D2O' + assert p[2].name == 'Si' def test_from_pars(self): m = Material(6.908, -0.278, 'Boron') @@ -42,8 +38,9 @@ def test_dict_repr(self): p = MaterialCollection() assert p._dict_repr == { 'EasyMaterials': [ - {'EasyMaterial': {'isld': '0.000e-6 1/Å^2', 'sld': '4.186e-6 1/Å^2'}}, - {'EasyMaterial': {'isld': '0.000e-6 1/Å^2', 'sld': '4.186e-6 1/Å^2'}}, + {'Air': {'isld': '0.000e-6 1/Å^2', 'sld': '0.000e-6 1/Å^2'}}, + {'D2O': {'isld': '0.000e-6 1/Å^2', 'sld': '6.335e-6 1/Å^2'}}, + {'Si': {'isld': '0.000e-6 1/Å^2', 'sld': '2.074e-6 1/Å^2'}}, ] } @@ -52,7 +49,7 @@ def test_repr(self): p.__repr__() assert ( p.__repr__() - == 'EasyMaterials:\n- EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n- EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n' # noqa: E501 + == 'EasyMaterials:\n- Air:\n sld: 0.000e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n- D2O:\n sld: 6.335e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n- Si:\n sld: 2.074e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n' # noqa: E501 ) def test_dict_round_trip(self): @@ -61,7 +58,7 @@ def test_dict_round_trip(self): k = Material(0.487, 0.000, 'Potassium') p = MaterialCollection() p.insert(0, m) - p.append(k) + p.add_material(k) p_dict = p.as_dict() global_object.map._clear() @@ -70,3 +67,99 @@ def test_dict_round_trip(self): # Expect assert sorted(p.as_data_dict()) == sorted(q.as_data_dict()) + + def test_add_material(self): + # Given + p = MaterialCollection() + m = Material(6.908, -0.278, 'Boron') + + # When + p.add_material(m) + + # Then + assert p[3] == m + + def test_duplicate_material(self): + # Given + p = MaterialCollection() + m = Material(6.908, -0.278, 'Boron') + p.add_material(m) + + # When + p.duplicate_material(3) + + # Then + assert p[4].name == 'Boron duplicate' + + def test_move_material_up(self): + # Given + p = MaterialCollection() + k = Material(0.487, 0.000, 'Bottom') + p.add_material(k) + + # When + p.move_material_up(3) + + # Then + assert p[2].name == 'Bottom' + assert p[3].name == 'Si' + + def test_move_material_up_to_top_and_further(self): + # Given + p = MaterialCollection() + m = Material(0.487, 0.000, 'Bottom') + p.add_material(m) + + # When + p.move_material_up(3) + p.move_material_up(2) + p.move_material_up(1) + p.move_material_up(0) + + # Then + assert p[0].name == 'Bottom' + assert p[3].name == 'Si' + + def test_move_material_down(self): + # Given + p = MaterialCollection() + m = Material(0.487, 0.000, 'Bottom') + p.add_material(m) + + # When + p.move_material_down(2) + + # Then + assert p[2].name == 'Bottom' + assert p[3].name == 'Si' + + def test_move_material_down_to_bottom_and_further(self): + # Given + p = MaterialCollection() + k = Material(0.487, 0.000, 'Middle') + m = Material(0.487, 0.000, 'Bottom') + p.add_material(k) + p.add_material(m) + + # When + p.move_material_down(3) + p.move_material_down(4) + + # Then + assert p[0].name == 'Air' + assert p[3].name == 'Bottom' + assert p[4].name == 'Middle' + + def test_remove_material(self): + # Given + p = MaterialCollection() + m = Material(0.487, 0.000, 'Bottom') + p.add_material(m) + + # When + p.remove_material(1) + + # Then + assert len(p) == 3 + assert p[0].name == 'Air' + assert p[2].name == 'Bottom' diff --git a/tests/sample/collections/test_sample.py b/tests/sample/collections/test_sample.py index 92e5c1b8..8ff1a5d7 100644 --- a/tests/sample/collections/test_sample.py +++ b/tests/sample/collections/test_sample.py @@ -283,7 +283,7 @@ def test_repr(self): p = Sample() assert ( p.__repr__() - == 'EasySample:\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + == 'EasySample:\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n- EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 ) def test_dict_round_trip(self): diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 09a367e4..159e8d8a 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -27,26 +27,26 @@ def test_fitting(minimizer): sio2 = Material(3.47, 0, 'SiO2') film = Material(2.0, 0, 'Film') d2o = Material(6.36, 0, 'D2O') - si_layer = Multilayer(Layer(si, 0, 0, 'Si layer')) - sio2_layer = Multilayer(Layer(sio2, 30, 3, 'SiO2 layer')) - film_layer = Multilayer(Layer(film, 250, 3, 'Film Layer')) - superphase = Multilayer(Layer(d2o, 0, 3, 'D2O Subphase')) + si_layer = Layer(si, 0, 0, 'Si layer') + sio2_layer = Layer(sio2, 30, 3, 'SiO2 layer') + film_layer = Layer(film, 250, 3, 'Film Layer') + superphase = Layer(d2o, 0, 3, 'D2O Subphase') sample = Sample( - si_layer, - sio2_layer, - film_layer, - superphase, + Multilayer(si_layer), + Multilayer(sio2_layer), + Multilayer(film_layer), + Multilayer(superphase), name='Film Structure', ) resolution_function = PercentageFhwm(0.02) model = Model(sample, 1, 1e-6, resolution_function, 'Film Model') # Thicknesses - sio2_layer.layers[0].thickness.bounds = (15, 50) - film_layer.layers[0].thickness.bounds = (200, 300) + sio2_layer.thickness.bounds = (15, 50) + film_layer.thickness.bounds = (200, 300) # Roughnesses - sio2_layer.layers[0].roughness.bounds = (1, 15) - film_layer.layers[0].roughness.bounds = (1, 15) - superphase.layers[0].roughness.bounds = (1, 15) + sio2_layer.roughness.bounds = (1, 15) + film_layer.roughness.bounds = (1, 15) + superphase.roughness.bounds = (1, 15) # Scattering length density film.sld.bounds = (0.1, 3) # Background From a0b0df67667c75164821320c88e6ff74954dc8d2 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:49:38 +0200 Subject: [PATCH 04/11] handle empty sample (#188) * handle empty sample * code cleaning --- src/easyreflectometry/sample/collections/sample.py | 14 ++++++++------ tests/model/test_model.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py index 02525173..a0eaf918 100644 --- a/src/easyreflectometry/sample/collections/sample.py +++ b/src/easyreflectometry/sample/collections/sample.py @@ -124,18 +124,20 @@ def _enable_changes_to_outermost_layers(self): Superphase can change thickness and roughness. Subphase can change thickness. """ - self.superphase.thickness.enabled = True - self.superphase.roughness.enabled = True - self.subphase.thickness.enabled = True + if len(self) != 0: + self.superphase.thickness.enabled = True + self.superphase.roughness.enabled = True + self.subphase.thickness.enabled = True def _disable_changes_to_outermost_layers(self): """No allowed to change the outermost layers of the sample. Superphase can change thickness and roughness. Subphase can change thickness. """ - self.superphase.thickness.enabled = False - self.superphase.roughness.enabled = False - self.subphase.thickness.enabled = False + if len(self) != 0: + self.superphase.thickness.enabled = False + self.superphase.roughness.enabled = False + self.subphase.thickness.enabled = False # Representation def as_dict(self, skip: Optional[List[str]] = None) -> dict: diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 32a8f62c..97fd02a6 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -337,6 +337,17 @@ def test_remove_assembly_with_interface_refl1d(self): # assert_equal(len(mod.interface()._wrapper.storage['item']), 1) # assert_equal(len(mod.interface()._wrapper.storage['layer']), 2) + def test_remove_all_assemblies(self): + # when + mod = Model() + + # Then + mod.remove_assembly(0) + mod.remove_assembly(0) + + # Expect + assert_equal(len(mod.sample), 0) + def test_resolution_function(self): mock_resolution_function = MagicMock() interface = CalculatorFactory() From 14d172f8bd83d9759b6626daecca935d8a36ed7e Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:14:29 +0200 Subject: [PATCH 05/11] Extract functionality for creating and manipulating a project should be moved from the app to the lib (#189) * first extraction steps * use data container * most parts are wokring * still some work needs to be done on experiments data * added reset * added save * added load * added functionality * more flexible * bug fix * default project * add materials * test for default * current_path to projeckt_path * project * tests update * reset functionality * reset * project * streamlining * code cleaning * code cleaning * from project_path to path * fix path * pr response * added test for creation of empty collections * pyproject --- src/easyreflectometry/__init__.py | 7 + src/easyreflectometry/model/model.py | 2 + .../model/model_collection.py | 7 +- src/easyreflectometry/project.py | 261 +++++++++++++ .../sample/assemblies/gradient_layer.py | 2 + .../sample/assemblies/multilayer.py | 3 +- .../sample/assemblies/surfactant_layer.py | 2 + .../sample/collections/base_collection.py | 18 +- .../sample/collections/layer_collection.py | 4 +- .../sample/collections/material_collection.py | 20 +- .../sample/collections/sample.py | 16 +- tests/model/test_model_collection.py | 6 + .../collections/test_base_collection.py | 16 - .../collections/test_material_collection.py | 6 + tests/sample/collections/test_sample.py | 6 + .../elements/materials/test_material.py | 5 +- tests/test_project.py | 355 ++++++++++++++++++ 17 files changed, 689 insertions(+), 47 deletions(-) create mode 100644 src/easyreflectometry/project.py create mode 100644 tests/test_project.py diff --git a/src/easyreflectometry/__init__.py b/src/easyreflectometry/__init__.py index 3ec8ef40..6de988ac 100644 --- a/src/easyreflectometry/__init__.py +++ b/src/easyreflectometry/__init__.py @@ -1,6 +1,13 @@ from importlib import metadata +from .project import Project + try: __version__ = metadata.version(__package__ or __name__) except metadata.PackageNotFoundError: __version__ = '0.0.0' + +__all__ = [ + Project, + __version__, +] diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index c0247387..426706c1 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -60,6 +60,7 @@ def __init__( background: Union[Parameter, Number, None] = None, resolution_function: Union[ResolutionFunction, None] = None, name: str = 'EasyModel', + unique_name: Optional[str] = None, interface=None, ): """Constructor. @@ -83,6 +84,7 @@ def __init__( super().__init__( name=name, + unique_name=unique_name, sample=sample, scale=scale, background=background, diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index 7ebbe355..79c50a37 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -9,7 +9,10 @@ from .model import Model -DEFAULT_COLLECTION = [Model()] + +# Needs to be a function, elements are added to the global_object.map +def DEFAULT_ELEMENTS(interface): + return (Model(interface),) class ModelCollection(BaseCollection): @@ -23,7 +26,7 @@ def __init__( ): if not models: if populate_if_none: - models = self._make_default_collection(DEFAULT_COLLECTION, interface) + models = DEFAULT_ELEMENTS(interface) else: models = [] # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py new file mode 100644 index 00000000..669ba2cc --- /dev/null +++ b/src/easyreflectometry/project.py @@ -0,0 +1,261 @@ +import datetime +import json +import os +from pathlib import Path +from typing import List +from typing import Optional +from typing import Union + +from easyscience import global_object +from easyscience.fitting import AvailableMinimizers + +from easyreflectometry.data.data_store import DataSet1D +from easyreflectometry.model import Model +from easyreflectometry.model import ModelCollection +from easyreflectometry.sample import Layer +from easyreflectometry.sample import MaterialCollection +from easyreflectometry.sample import Multilayer +from easyreflectometry.sample import Sample +from easyreflectometry.sample.collections.base_collection import BaseCollection + + +class Project: + def __init__(self): + self._info = self._default_info() + self._path = Path(os.path.expanduser('~')) + self._models = ModelCollection(populate_if_none=False, unique_name='project_models') + self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') + self._calculator = None + self._minimizer: AvailableMinimizers = None + self._experiments: List[DataSet1D] = None + self._colors = None + self._report = None + + # Project flags + self._project_created = False + self._project_with_experiments = False + + def reset(self): + del self._models + del self._materials + global_object.map._clear() + + self._models = ModelCollection(populate_if_none=False, unique_name='project_models') + self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') + + self._info = self._default_info() + self._path = Path(os.path.expanduser('~')) + self._calculator = None + self._minimizer = None + self._experiments = None + self._colors = None + self._report = None + + # Project flags + self._project_created = False + self._project_with_experiments = False + + @property + def path(self): + return self._path + + @path.setter + def path(self, path: Union[Path, str]): + self._path = Path(path) + + @property + def models(self) -> ModelCollection: + return self._models + + @models.setter + def models(self, models: ModelCollection) -> None: + self._replace_collection(models, self._models) + self._materials.extend(self._get_materials_in_models()) + + @property + def minimizer(self) -> AvailableMinimizers: + return self._minimizer + + @minimizer.setter + def minimizer(self, minimizer: AvailableMinimizers) -> None: + self._minimizer = minimizer + + @property + def experiments(self) -> List[DataSet1D]: + return self._experiments + + @experiments.setter + def experiments(self, experiments: List[DataSet1D]) -> None: + self._experiments = experiments + + @property + def path_json(self): + return self._path / 'project.json' + + def default_model(self): + self._replace_collection(MaterialCollection(), self._materials) + + layers = [ + Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer'), + Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='Multi-layer'), + Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer'), + ] + items = [ + Multilayer(layers[0], name='Superphase'), + Multilayer(layers[1], name='Multi-layer'), + Multilayer(layers[2], name='Subphase'), + ] + sample = Sample(*items) + sample[0].layers[0].thickness.enabled = False + sample[0].layers[0].roughness.enabled = False + sample[-1].layers[-1].thickness.enabled = False + self._replace_collection([Model(sample=sample)], self._models) + + def add_material(self, material: MaterialCollection) -> None: + if material in self._materials: + print(f'WARNING: Material {material} is already in material collection') + else: + self._materials.append(material) + + def remove_material(self, index: int) -> None: + if self._materials[index] in self._get_materials_in_models(): + print(f'ERROR: Material {self._materials[index]} is used in models') + else: + self._materials.pop(index) + + def _default_info(self): + return dict( + name='Example Project', + short_description='reflectometry, 1D', + samples='None', + experiments='None', + modified=datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), + ) + + def create_project_dir(self): + if not os.path.exists(self._path): + os.makedirs(self._path) + os.makedirs(self._path / 'experiments') + else: + print(f'ERROR: Directory {self._path} already exists') + + def save_project_json(self, overwrite=False): + if self.path_json.exists() and not overwrite: + print(f'File already exists {self.path_json}. Overwriting...') + self.path_json.unlink() + try: + project_json = json.dumps(self.as_dict(include_materials_not_in_model=True), indent=4) + self.path_json.parent.mkdir(exist_ok=True, parents=True) + with open(self.path_json, 'w') as file: + file.write(project_json) + except Exception as exception: + print(exception) + + def load_project_json(self, path: Optional[Union[Path, str]] = None): + path = Path(path) + if path is None: + path = self.path_json + if path.exists(): + with open(path, 'r') as file: + project_dict = json.load(file) + self.reset() + self.from_dict(project_dict) + self._path = path.parent + else: + print(f'ERROR: File {path} does not exist') + + def as_dict(self, include_materials_not_in_model=False): + project_dict = {} + project_dict['info'] = self._info + project_dict['project_with_experiments'] = self._project_with_experiments + project_dict['project_created'] = self._project_created + if self._models is not None: + project_dict['models'] = self._models.as_dict(skip=['interface']) + if include_materials_not_in_model: + self._as_dict_add_materials_not_in_model_dict(project_dict) + if self._project_with_experiments: + self._as_dict_add_experiments(project_dict) + if self._minimizer is not None: + project_dict['minimizer'] = self._minimizer.name + if self._calculator is not None: + project_dict['calculator'] = [self._calculator.current_interface_name] + if self._colors is not None: + project_dict['colors'] = self._colors + return project_dict + + def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict): + materials_not_in_model = [] + for material in self._materials: + if material not in self._get_materials_in_models(): + materials_not_in_model.append(material) + if len(materials_not_in_model) > 0: + project_dict['materials_not_in_model'] = MaterialCollection(materials_not_in_model).as_dict(skip=['interface']) + + def _as_dict_add_experiments(self, project_dict: dict): + project_dict['experiments'] = [] + project_dict['experiments_models'] = [] + project_dict['experiments_names'] = [] + for experiment in self._experiments: + if self._experiments[0].xe is not None: + project_dict['experiments'].append([experiment.x, experiment.y, experiment.ye, experiment.xe]) + else: + project_dict['experiments'].append([experiment.x, experiment.y, experiment.ye]) + project_dict['experiments_models'].append(experiment.model.name) + project_dict['experiments_names'].append(experiment.name) + + def from_dict(self, project_dict: dict): + keys = list(project_dict.keys()) + self._info = project_dict['info'] + self._project_with_experiments = project_dict['project_with_experiments'] + if 'models' in keys: + self._models = None + self._models = ModelCollection.from_dict(project_dict['models']) + + self._replace_collection(self._get_materials_in_models(), self._materials) + + if 'materials_not_in_model' in keys: + self._materials.extend(MaterialCollection.from_dict(project_dict['materials_not_in_model'])) + + if 'minimizer' in keys: + self._minimizer = AvailableMinimizers[project_dict['minimizer']] + else: + self._minimizer = None + if 'experiments' in keys: + self._experiments = self._from_dict_extract_experiments(project_dict) + else: + self._experiments = None + if 'calculator' in keys: + self._calculator = project_dict['calculator'] + else: + self._calculator = None + + def _from_dict_extract_experiments(self, project_dict: dict): + self._experiments: List[DataSet1D] = [] + + for i in range(len(project_dict['experiments'])): + self._experiments.append( + DataSet1D( + name=project_dict['experiments_names'][i], + x=project_dict['experiments'][i][0], + y=project_dict['experiments'][i][1], + ye=project_dict['experiments'][i][2], + xe=project_dict['experiments'][i][3], + model=self._models[project_dict['experiments_models'][i]], + ) + ) + + def _get_materials_in_models(self) -> MaterialCollection: + materials_in_model = MaterialCollection(populate_if_none=False) + for model in self._models: + for assembly in model.sample: + for layer in assembly.layers: + materials_in_model.append(layer.material) + return materials_in_model + + def _replace_collection(self, src_collection: BaseCollection, dst_collection: BaseCollection) -> None: + # Clear the destination collection + for i in range(len(dst_collection)): + dst_collection.pop(0) + + for element in src_collection: + dst_collection.append(element) diff --git a/src/easyreflectometry/sample/assemblies/gradient_layer.py b/src/easyreflectometry/sample/assemblies/gradient_layer.py index 8be219a9..38771e80 100644 --- a/src/easyreflectometry/sample/assemblies/gradient_layer.py +++ b/src/easyreflectometry/sample/assemblies/gradient_layer.py @@ -23,6 +23,7 @@ def __init__( roughness: Optional[float] = 0.2, discretisation_elements: int = 10, name: str = 'EasyGradienLayer', + unique_name: Optional[str] = None, interface=None, ): """Constructor. @@ -58,6 +59,7 @@ def __init__( super().__init__( layers=gradient_layers, name=name, + unique_name=unique_name, interface=interface, type='Gradient-layer', ) diff --git a/src/easyreflectometry/sample/assemblies/multilayer.py b/src/easyreflectometry/sample/assemblies/multilayer.py index 9f3307b6..360db10a 100644 --- a/src/easyreflectometry/sample/assemblies/multilayer.py +++ b/src/easyreflectometry/sample/assemblies/multilayer.py @@ -23,6 +23,7 @@ def __init__( self, layers: Union[Layer, list[Layer], LayerCollection, None] = None, name: str = 'EasyMultilayer', + unique_name: Optional[str] = None, interface=None, type: str = 'Multi-layer', populate_if_none: Optional[bool] = True, @@ -47,7 +48,7 @@ def __init__( # Else collisions might occur in global_object.map self.populate_if_none = False - super().__init__(name, layers=layers, type=type, interface=interface) + super().__init__(name, unique_name=unique_name, layers=layers, type=type, interface=interface) def add_layer(self, *layers: tuple[Layer]) -> None: """Add a layer to the multi layer. diff --git a/src/easyreflectometry/sample/assemblies/surfactant_layer.py b/src/easyreflectometry/sample/assemblies/surfactant_layer.py index e0db9c59..9c7d4144 100644 --- a/src/easyreflectometry/sample/assemblies/surfactant_layer.py +++ b/src/easyreflectometry/sample/assemblies/surfactant_layer.py @@ -30,6 +30,7 @@ def __init__( tail_layer: Optional[LayerAreaPerMolecule] = None, head_layer: Optional[LayerAreaPerMolecule] = None, name: str = 'EasySurfactantLayer', + unique_name: Optional[str] = None, constrain_area_per_molecule: bool = False, conformal_roughness: bool = False, interface=None, @@ -68,6 +69,7 @@ def __init__( surfactant = LayerCollection(tail_layer, head_layer, name=name) super().__init__( name=name, + unique_name=unique_name, type='Surfactant Layer', layers=surfactant, interface=interface, diff --git a/src/easyreflectometry/sample/collections/base_collection.py b/src/easyreflectometry/sample/collections/base_collection.py index 6a3842b5..1c618226 100644 --- a/src/easyreflectometry/sample/collections/base_collection.py +++ b/src/easyreflectometry/sample/collections/base_collection.py @@ -1,4 +1,3 @@ -from copy import deepcopy from typing import List from typing import Optional @@ -13,11 +12,16 @@ def __init__( name: str, interface, *args, + unique_name: Optional[str] = None, **kwargs, ): - super().__init__(name, *args, **kwargs) + super().__init__(name, unique_name=unique_name, *args, **kwargs) self.interface = interface + # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict + # Else collisions might occur in global_object.map + self.populate_if_none = False + def __repr__(self) -> str: """ String representation of the collection. @@ -42,12 +46,6 @@ def _dict_repr(self) -> dict: """ return {self.name: [i._dict_repr for i in self]} - def _make_default_collection(self, default_collection: List, interface) -> List: - elements = deepcopy(default_collection) - for element in elements: - element.interface = interface - return elements - def as_dict(self, skip: Optional[List[str]] = None) -> dict: """ Create a dictionary representation of the collection. @@ -60,4 +58,8 @@ def as_dict(self, skip: Optional[List[str]] = None) -> dict: this_dict['data'] = [] for collection_element in self: this_dict['data'].append(collection_element.as_dict(skip=skip)) + this_dict['populate_if_none'] = self.populate_if_none return this_dict + + def __deepcopy__(self, memo): + return self.from_dict(self.as_dict(skip=['unique_name'])) diff --git a/src/easyreflectometry/sample/collections/layer_collection.py b/src/easyreflectometry/sample/collections/layer_collection.py index cbed822d..2ddfabcc 100644 --- a/src/easyreflectometry/sample/collections/layer_collection.py +++ b/src/easyreflectometry/sample/collections/layer_collection.py @@ -12,9 +12,11 @@ def __init__( *layers: Optional[list[Layer]], name: str = 'EasyLayerCollection', interface=None, + unique_name: Optional[str] = None, + populate_if_none: bool = True, # Needed to match as_dict signature from BaseCollection **kwargs, ): if not layers: layers = [] - super().__init__(name, interface, *layers, **kwargs) + super().__init__(name, interface, unique_name=unique_name, *layers, **kwargs) diff --git a/src/easyreflectometry/sample/collections/material_collection.py b/src/easyreflectometry/sample/collections/material_collection.py index ac5b89b6..3e1a2690 100644 --- a/src/easyreflectometry/sample/collections/material_collection.py +++ b/src/easyreflectometry/sample/collections/material_collection.py @@ -5,11 +5,14 @@ from ..elements.materials.material import Material from .base_collection import BaseCollection -DEFAULT_COLLECTION = ( - Material(sld=0.0, isld=0.0, name='Air'), - Material(sld=6.335, isld=0.0, name='D2O'), - Material(sld=2.074, isld=0.0, name='Si'), -) + +# Needs to be a function, elements are added to the global_object.map +def DEFAULT_ELEMENTS(interface): + return ( + Material(sld=0.0, isld=0.0, name='Air', interface=interface), + Material(sld=6.335, isld=0.0, name='D2O', interface=interface), + Material(sld=2.074, isld=0.0, name='Si', interface=interface), + ) class MaterialCollection(BaseCollection): @@ -18,21 +21,20 @@ def __init__( *materials: Tuple[Material], name: str = 'EasyMaterials', interface=None, + unique_name: Optional[str] = None, populate_if_none: bool = True, **kwargs, ): if not materials: # Empty tuple if no materials are provided if populate_if_none: - materials = self._make_default_collection(DEFAULT_COLLECTION, interface) + materials = DEFAULT_ELEMENTS(interface) else: materials = [] - # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict - # Else collisions might occur in global_object.map - self.populate_if_none = False super().__init__( name, interface, + unique_name=unique_name, *materials, **kwargs, ) diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py index a0eaf918..ec9df191 100644 --- a/src/easyreflectometry/sample/collections/sample.py +++ b/src/easyreflectometry/sample/collections/sample.py @@ -12,7 +12,13 @@ from ..elements.layers.layer import Layer from .base_collection import BaseCollection -DEFAULT_COLLECTION = [Multilayer(), Multilayer()] + +# Needs to be a function, elements are added to the global_object.map +def DEFAULT_ELEMENTS(interface): + return ( + Multilayer(interface=interface), + Multilayer(interface=interface), + ) class Sample(BaseCollection): @@ -23,6 +29,7 @@ def __init__( *assemblies: Optional[List[BaseAssembly]], name: str = 'EasySample', interface=None, + unique_name: Optional[str] = None, populate_if_none: bool = True, **kwargs, ): @@ -34,17 +41,14 @@ def __init__( """ if not assemblies: if populate_if_none: - assemblies = self._make_default_collection(DEFAULT_COLLECTION, interface) + assemblies = DEFAULT_ELEMENTS(interface) else: assemblies = [] - # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict - # Else collisions might occur in global_object.map - self.populate_if_none = False for assembly in assemblies: if not issubclass(type(assembly), BaseAssembly): raise ValueError('The elements must be an Assembly.') - super().__init__(name, interface, *assemblies, **kwargs) + super().__init__(name, interface, unique_name=unique_name, *assemblies, **kwargs) def add_assembly(self, assembly: Optional[BaseAssembly] = None): """Add an assembly to the sample. diff --git a/tests/model/test_model_collection.py b/tests/model/test_model_collection.py index 44daa806..ebaa418a 100644 --- a/tests/model/test_model_collection.py +++ b/tests/model/test_model_collection.py @@ -15,6 +15,12 @@ def test_default(self): assert len(collection) == 1 assert collection[0].name == 'EasyModel' + def test_dont_populate(self): + p = ModelCollection(populate_if_none=False) + assert p.name == 'EasyModels' + assert p.interface is None + assert len(p) == 0 + def test_from_pars(self): # When model_1 = Model(name='Model1') diff --git a/tests/sample/collections/test_base_collection.py b/tests/sample/collections/test_base_collection.py index 6b3624d7..78279009 100644 --- a/tests/sample/collections/test_base_collection.py +++ b/tests/sample/collections/test_base_collection.py @@ -63,19 +63,3 @@ def test_as_dict(self): assert p.as_dict()['name'] == 'name' assert len(p.as_dict()['data']) == 1 assert p.as_dict()['data'][0]['name'] == 'layer' - - def test_make_default_collection(self): - # When - elem_1 = Layer(name='layer_1') - mock_interface_1 = MagicMock() - elem_2 = Layer(name='layer_2') - mock_interface_2 = MagicMock() - p = BaseCollection('name', mock_interface_1, elem_1) - - # Then - default_collection = p._make_default_collection([elem_2], mock_interface_2) - - # Expect - assert default_collection[0] != elem_2 - assert default_collection[0].name == 'layer_2' - assert default_collection[0].interface == mock_interface_2 diff --git a/tests/sample/collections/test_material_collection.py b/tests/sample/collections/test_material_collection.py index ca85e3ee..e25074fe 100644 --- a/tests/sample/collections/test_material_collection.py +++ b/tests/sample/collections/test_material_collection.py @@ -18,6 +18,12 @@ def test_default(self): assert p[1].name == 'D2O' assert p[2].name == 'Si' + def test_dont_populate(self): + p = MaterialCollection(populate_if_none=False) + assert p.name == 'EasyMaterials' + assert p.interface is None + assert len(p) == 0 + def test_from_pars(self): m = Material(6.908, -0.278, 'Boron') k = Material(0.487, 0.000, 'Potassium') diff --git a/tests/sample/collections/test_sample.py b/tests/sample/collections/test_sample.py index 8ff1a5d7..da4d2180 100644 --- a/tests/sample/collections/test_sample.py +++ b/tests/sample/collections/test_sample.py @@ -31,6 +31,12 @@ def test_default(self): assert_equal(p[0].name, 'EasyMultilayer') assert_equal(p[1].name, 'EasyMultilayer') + def test_dont_populate(self): + p = Sample(populate_if_none=False) + assert p.name == 'EasySample' + assert p.interface is None + assert len(p) == 0 + def test_add_assembly(self): # When p = Sample() diff --git a/tests/sample/elements/materials/test_material.py b/tests/sample/elements/materials/test_material.py index 0d836037..7c7b495e 100644 --- a/tests/sample/elements/materials/test_material.py +++ b/tests/sample/elements/materials/test_material.py @@ -5,9 +5,6 @@ __author__ = 'github.com/arm61' __version__ = '0.0.1' - -import unittest - import numpy as np from easyscience import global_object @@ -16,7 +13,7 @@ from easyreflectometry.sample.elements.materials.material import Material -class TestMaterial(unittest.TestCase): +class TestMaterial: def test_no_arguments(self): p = Material() assert p.name == 'EasyMaterial' diff --git a/tests/test_project.py b/tests/test_project.py new file mode 100644 index 00000000..718c5747 --- /dev/null +++ b/tests/test_project.py @@ -0,0 +1,355 @@ +import datetime +import os +from pathlib import Path + +from easyscience import global_object +from easyscience.fitting import AvailableMinimizers + +from easyreflectometry.model import Model +from easyreflectometry.model import ModelCollection +from easyreflectometry.project import Project +from easyreflectometry.sample import Material +from easyreflectometry.sample import MaterialCollection + + +class TestProject: + def test_constructor(self): + # When Then + project = Project() + + # Expect + assert project._info == { + 'name': 'Example Project', + 'short_description': 'reflectometry, 1D', + 'samples': 'None', + 'experiments': 'None', + 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), + } + assert project._path == Path(os.path.expanduser('~')) + assert len(project._materials) == 0 + assert len(project._models) == 0 + assert project._calculator is None + assert project._minimizer is None + assert project._experiments is None + assert project._report is None + assert project._project_created is False + assert project._project_with_experiments is False + + def test_reset(self): + # When + project = Project() + project._info['name'] = 'Test Project' + project._materials.append(Material()) + project._models.append(Model()) + project._calculator = 'calculator' + project._minimizer = 'minimizer' + project._experiments = 'experiments' + project._report = 'report' + project._project_created = True + project._project_with_experiments = True + project._path = 'project_path' + + # Then + project.reset() + + # Expect + assert project._info == { + 'name': 'Example Project', + 'short_description': 'reflectometry, 1D', + 'samples': 'None', + 'experiments': 'None', + 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), + } + assert project._models.unique_name == 'project_models' + assert len(project._models) == 0 + assert project._materials.unique_name == 'project_materials' + assert len(project._materials) == 0 + + assert project._path == Path(os.path.expanduser('~')) + assert project._calculator is None + assert project._minimizer is None + assert project._experiments is None + assert project._report is None + assert project._project_created is False + assert project._project_with_experiments is False + assert global_object.map.vertices() == ['project_models', 'project_materials'] + + def test_models(self): + # When + project = Project() + models = ModelCollection(Model()) + material = Material() + project._materials.append(material) + + # Then + project.models = models + + # Expect + project_models_dict = project.models.as_dict(skip=['interface']) + models_dict = models.as_dict(skip=['interface']) + models_dict['unique_name'] = 'project_models' + assert project_models_dict == models_dict + + assert len(project._materials) == 3 + assert project._materials[0] == material + assert project._materials[1] == models[0].sample[0].layers[0].material + assert project._materials[2] == models[0].sample[1].layers[0].material + + def test_default_model(self): + # When + global_object.map._clear() + project = Project() + + # Then + project.default_model() + + # Expect + assert len(project._models) == 1 + assert project._models[0].unique_name == 'Model_0' + assert len(project._models.data[0].sample) == 3 + assert len(project._materials) == 3 + + def test_minimizer(self): + # When + project = Project() + + # Then + project.minimizer = 'minimizer' + + # Expect + assert project.minimizer == 'minimizer' + + def test_experiments(self): + # When + project = Project() + + # Then + project.experiments = 'experiments' + + # Expect + assert project.experiments == 'experiments' + + def test_path_json(self, tmp_path): + # When + project = Project() + project.path = tmp_path + + # Then Expect + assert project.path_json == Path(tmp_path) / 'project.json' + + def test_add_material(self): + # When + project = Project() + material = Material() + + # Then + project.add_material(material) + + # Expect + assert len(project._materials) == 1 + assert project._materials[0] == material + + def test_remove_material(self): + # When + project = Project() + material = Material() + project.add_material(material) + + # Then + project.remove_material(0) + + # Expect + assert len(project._materials) == 0 + + def test_remove_material_in_model(self): + # When + project = Project() + model = Model() + models = ModelCollection(model) + project.models = models + + # Then + project.remove_material(0) + + # Expect + assert len(project._materials) == 2 + + def test_default_info(self): + # When + project = Project() + + # Then + info = project._default_info() + + # Expect + assert info == { + 'name': 'Example Project', + 'short_description': 'reflectometry, 1D', + 'samples': 'None', + 'experiments': 'None', + 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), + } + + def test_as_dict(self): + # When + project = Project() + + # Then + project_dict = project.as_dict() + + # Expect + keys = list(project_dict.keys()) + keys.sort() + assert keys == [ + 'info', + 'models', + 'project_created', + 'project_with_experiments', + ] + assert project_dict['info'] == { + 'name': 'Example Project', + 'short_description': 'reflectometry, 1D', + 'samples': 'None', + 'experiments': 'None', + 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), + } + assert project_dict['models']['data'] == [] + assert project_dict['project_created'] is False + assert project_dict['project_with_experiments'] is False + + def test_as_dict_models(self): + # When + project = Project() + models = ModelCollection(Model()) + project.models = models + + # Then + project_dict = project.as_dict() + + # Expect + models_dict = models.as_dict(skip=['interface']) + models_dict['unique_name'] = 'project_models' + assert project_dict['models'] == models_dict + + def test_as_dict_materials_not_in_model(self): + # When + project = Project() + models = ModelCollection(Model()) + project.models = models + material = Material() + project.add_material(material) + + # Then + project_dict = project.as_dict(include_materials_not_in_model=True) + + # Expect + assert project_dict['materials_not_in_model']['data'][0] == material.as_dict(skip=['interface']) + + def test_as_dict_minimizer(self): + # When + project = Project() + minimizer = AvailableMinimizers.LMFit + project.minimizer = minimizer + + # Then + project_dict = project.as_dict() + + # Expect + assert project_dict['minimizer'] == 'LMFit' + + def test_replace_collection(self): + # When + project = Project() + material = Material() + project._materials.append(material) + new_material = Material() + + # Then + project._replace_collection(MaterialCollection(new_material), project._materials) + + # Expect + assert project._materials[0] == new_material + assert project._materials.unique_name == 'project_materials' + + def test_get_materials_in_models(self): + # When + models = ModelCollection(Model()) + project = Project() + project.models = models + material = Material(6.908, -0.278, 'Boron') + project.add_material(material) + + # Then + materials = project._get_materials_in_models() + + # Expect + assert len(materials) == 2 + assert materials[0] == models[0].sample[0].layers[0].material + assert materials[1] == models[0].sample[1].layers[0].material + + def test_dict_round_trip(self): + # When + global_object.map._clear() + project = Project() + models = ModelCollection(Model()) + project.models = models + material = Material(6.908, -0.278, 'Boron') + project.add_material(material) + minimizer = AvailableMinimizers.LMFit + project.minimizer = minimizer + project_dict = project.as_dict(include_materials_not_in_model=True) + project_materials_dict = project._materials.as_dict() + + del material + global_object.map._clear() + + # Then + new_project = Project() + new_project.from_dict(project_dict) + new_project_dict = new_project.as_dict(include_materials_not_in_model=True) + new_project_materials_dict = new_project._materials.as_dict() + + # Expect + keys = list(project_dict.keys()) + for key in keys: + assert project_dict[key] == new_project_dict[key] + assert project_materials_dict == new_project_materials_dict + + def test_save_project(self, tmp_path): + # When + global_object.map._clear() + project = Project() + project.path = tmp_path + project._models.append(Model()) + project._info['name'] = 'Test Project' + project.save_project_json(overwrite=True) + + # Then + project_path = project.path_json + + # Expect + assert project_path.exists() + + def test_load_project(self, tmp_path): + # When + global_object.map._clear() + project = Project() + project.path = tmp_path + project._models.append(Model()) + project._info['name'] = 'Test Project' + project.save_project_json() + project_dict = project.as_dict() + + global_object.map._clear() + new_project = Project() + + # Then + new_project.load_project_json(tmp_path / 'project.json') + # Do it twice to ensure that potential global objects don't collide + new_project.load_project_json(tmp_path / 'project.json') + + # Expect + assert len(new_project._models) == 1 + assert new_project._info['name'] == 'Test Project' + assert new_project.as_dict() == project_dict + assert new_project.path == tmp_path From 0601013d666ea443dac484285b18338a4dffc967 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Thu, 3 Oct 2024 06:42:24 +0200 Subject: [PATCH 06/11] Project adjustments (#190) * minor adjustments * fix to create * slight renaming * set created when project is loaded * pr response * ruff * change test comparing files * test file changes --- src/easyreflectometry/project.py | 58 ++++++++------- tests/test_project.py | 119 +++++++++++++++++++++++-------- 2 files changed, 123 insertions(+), 54 deletions(-) diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 669ba2cc..018057cc 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -22,7 +22,7 @@ class Project: def __init__(self): self._info = self._default_info() - self._path = Path(os.path.expanduser('~')) + self._path_project_parent = Path(os.path.expanduser('~')) self._models = ModelCollection(populate_if_none=False, unique_name='project_models') self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') self._calculator = None @@ -32,8 +32,8 @@ def __init__(self): self._report = None # Project flags - self._project_created = False - self._project_with_experiments = False + self._created = False + self._with_experiments = False def reset(self): del self._models @@ -44,7 +44,7 @@ def reset(self): self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') self._info = self._default_info() - self._path = Path(os.path.expanduser('~')) + self._path_project_parent = Path(os.path.expanduser('~')) self._calculator = None self._minimizer = None self._experiments = None @@ -52,16 +52,19 @@ def reset(self): self._report = None # Project flags - self._project_created = False - self._project_with_experiments = False + self._created = False + self._with_experiments = False + + @property + def created(self) -> bool: + return self._created @property def path(self): - return self._path + return self._path_project_parent / self._info['name'] - @path.setter - def path(self, path: Union[Path, str]): - self._path = Path(path) + def set_path_project_parent(self, path: Union[Path, str]): + self._path_project_parent = Path(path) @property def models(self) -> ModelCollection: @@ -90,7 +93,7 @@ def experiments(self, experiments: List[DataSet1D]) -> None: @property def path_json(self): - return self._path / 'project.json' + return self.path / 'project.json' def default_model(self): self._replace_collection(MaterialCollection(), self._materials) @@ -132,26 +135,28 @@ def _default_info(self): modified=datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), ) - def create_project_dir(self): - if not os.path.exists(self._path): - os.makedirs(self._path) - os.makedirs(self._path / 'experiments') + def create(self): + if not os.path.exists(self.path): + os.makedirs(self.path) + os.makedirs(self.path / 'experiments') + self._created = True + self._timestamp_modification() else: - print(f'ERROR: Directory {self._path} already exists') + print(f'ERROR: Directory {self.path} already exists') - def save_project_json(self, overwrite=False): - if self.path_json.exists() and not overwrite: + def save_as_json(self, overwrite=False): + if self.path_json.exists() and overwrite: print(f'File already exists {self.path_json}. Overwriting...') self.path_json.unlink() try: project_json = json.dumps(self.as_dict(include_materials_not_in_model=True), indent=4) self.path_json.parent.mkdir(exist_ok=True, parents=True) - with open(self.path_json, 'w') as file: + with open(self.path_json, mode='x') as file: file.write(project_json) except Exception as exception: print(exception) - def load_project_json(self, path: Optional[Union[Path, str]] = None): + def load_from_json(self, path: Optional[Union[Path, str]] = None): path = Path(path) if path is None: path = self.path_json @@ -160,20 +165,20 @@ def load_project_json(self, path: Optional[Union[Path, str]] = None): project_dict = json.load(file) self.reset() self.from_dict(project_dict) - self._path = path.parent + self._path_project_parent = path.parents[1] + self._created = True else: print(f'ERROR: File {path} does not exist') def as_dict(self, include_materials_not_in_model=False): project_dict = {} project_dict['info'] = self._info - project_dict['project_with_experiments'] = self._project_with_experiments - project_dict['project_created'] = self._project_created + project_dict['with_experiments'] = self._with_experiments if self._models is not None: project_dict['models'] = self._models.as_dict(skip=['interface']) if include_materials_not_in_model: self._as_dict_add_materials_not_in_model_dict(project_dict) - if self._project_with_experiments: + if self._with_experiments: self._as_dict_add_experiments(project_dict) if self._minimizer is not None: project_dict['minimizer'] = self._minimizer.name @@ -206,7 +211,7 @@ def _as_dict_add_experiments(self, project_dict: dict): def from_dict(self, project_dict: dict): keys = list(project_dict.keys()) self._info = project_dict['info'] - self._project_with_experiments = project_dict['project_with_experiments'] + self._with_experiments = project_dict['with_experiments'] if 'models' in keys: self._models = None self._models = ModelCollection.from_dict(project_dict['models']) @@ -259,3 +264,6 @@ def _replace_collection(self, src_collection: BaseCollection, dst_collection: Ba for element in src_collection: dst_collection.append(element) + + def _timestamp_modification(self): + self._info['modified'] = datetime.datetime.now().strftime('%d.%m.%Y %H:%M') diff --git a/tests/test_project.py b/tests/test_project.py index 718c5747..25691682 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -25,15 +25,15 @@ def test_constructor(self): 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } - assert project._path == Path(os.path.expanduser('~')) + assert project._path_project_parent == Path(os.path.expanduser('~')) assert len(project._materials) == 0 assert len(project._models) == 0 assert project._calculator is None assert project._minimizer is None assert project._experiments is None assert project._report is None - assert project._project_created is False - assert project._project_with_experiments is False + assert project._created is False + assert project._with_experiments is False def test_reset(self): # When @@ -45,9 +45,9 @@ def test_reset(self): project._minimizer = 'minimizer' project._experiments = 'experiments' project._report = 'report' - project._project_created = True - project._project_with_experiments = True - project._path = 'project_path' + project._created = True + project._with_experiments = True + project._path_project_parent = 'project_path' # Then project.reset() @@ -65,13 +65,13 @@ def test_reset(self): assert project._materials.unique_name == 'project_materials' assert len(project._materials) == 0 - assert project._path == Path(os.path.expanduser('~')) + assert project._path_project_parent == Path(os.path.expanduser('~')) assert project._calculator is None assert project._minimizer is None assert project._experiments is None assert project._report is None - assert project._project_created is False - assert project._project_with_experiments is False + assert project._created is False + assert project._with_experiments is False assert global_object.map.vertices() == ['project_models', 'project_materials'] def test_models(self): @@ -132,10 +132,10 @@ def test_experiments(self): def test_path_json(self, tmp_path): # When project = Project() - project.path = tmp_path + project.set_path_project_parent(tmp_path) # Then Expect - assert project.path_json == Path(tmp_path) / 'project.json' + assert project.path_json == Path(tmp_path) / 'Example Project' / 'project.json' def test_add_material(self): # When @@ -203,8 +203,7 @@ def test_as_dict(self): assert keys == [ 'info', 'models', - 'project_created', - 'project_with_experiments', + 'with_experiments', ] assert project_dict['info'] == { 'name': 'Example Project', @@ -214,8 +213,7 @@ def test_as_dict(self): 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } assert project_dict['models']['data'] == [] - assert project_dict['project_created'] is False - assert project_dict['project_with_experiments'] is False + assert project_dict['with_experiments'] is False def test_as_dict_models(self): # When @@ -315,41 +313,104 @@ def test_dict_round_trip(self): assert project_dict[key] == new_project_dict[key] assert project_materials_dict == new_project_materials_dict - def test_save_project(self, tmp_path): + def test_save_as_json(self, tmp_path): # When global_object.map._clear() project = Project() - project.path = tmp_path + project.set_path_project_parent(tmp_path) project._models.append(Model()) project._info['name'] = 'Test Project' - project.save_project_json(overwrite=True) # Then - project_path = project.path_json + project.save_as_json() + project.path_json # Expect - assert project_path.exists() + assert project.path_json.exists() - def test_load_project(self, tmp_path): + def test_save_as_json_overwrite(self, tmp_path): # When global_object.map._clear() project = Project() - project.path = tmp_path + project.set_path_project_parent(tmp_path) + project.save_as_json() + file_info = project.path_json.stat() + + # Then + project._info['short_description'] = 'short_description' project._models.append(Model()) - project._info['name'] = 'Test Project' - project.save_project_json() + project.save_as_json(overwrite=True) + + # Expect + assert str(file_info) != str(project.path_json.stat()) + + def test_save_as_json_dont_overwrite(self, tmp_path): + # When + global_object.map._clear() + project = Project() + project.set_path_project_parent(tmp_path) + project.save_as_json() + file_info = project.path_json.stat() + + # Then + project._info['short_description'] = 'short_description' + project._models.append(Model()) + project.save_as_json() + + # Expect + assert str(file_info) == str(project.path_json.stat()) + + def test_load_from_json(self, tmp_path): + # When + global_object.map._clear() + project = Project() + project.set_path_project_parent(tmp_path) + project._models.append(Model()) + project._info['name'] = 'name' + project._info['short_description'] = 'short_description' + project._info['samples'] = 'samples' + project._info['experiments'] = 'experiments' + + project.save_as_json() project_dict = project.as_dict() global_object.map._clear() new_project = Project() # Then - new_project.load_project_json(tmp_path / 'project.json') + new_project.load_from_json(tmp_path / 'name' / 'project.json') # Do it twice to ensure that potential global objects don't collide - new_project.load_project_json(tmp_path / 'project.json') + new_project.load_from_json(tmp_path / 'name' / 'project.json') # Expect assert len(new_project._models) == 1 - assert new_project._info['name'] == 'Test Project' - assert new_project.as_dict() == project_dict - assert new_project.path == tmp_path + assert new_project._info['name'] == 'name' + assert new_project._info['short_description'] == 'short_description' + assert new_project._info['samples'] == 'samples' + assert new_project._info['experiments'] == 'experiments' + assert project_dict == new_project.as_dict() + assert new_project._path_project_parent == tmp_path + assert new_project.created is True + + def test_create(self, tmp_path): + # When + project = Project() + project.set_path_project_parent(tmp_path) + project._info['modified'] = 'modified' + project._info['name'] = 'Test Project' + + # Then + project.create() + + # Expect + assert project.path == tmp_path / 'Test Project' + assert project.path.exists() + assert (project.path / 'experiments').exists() + assert project.created is True + assert project._info == { + 'name': 'Test Project', + 'short_description': 'reflectometry, 1D', + 'samples': 'None', + 'experiments': 'None', + 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), + } From dc2ea110ea54befda92687d080fa1d042efd1c55 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:45:28 +0200 Subject: [PATCH 07/11] 191 adjust model and modelcollection to hold functionality from app (#192) * from materials collection to base collection * added color to Model * renaming up and down to be consistent with other code * adjusted collections * placeholder for calculated data * ready to expose data * datastore and tests * added functionality * code cleaning * explicit clean global obejct for surfactant tests * unique name in surfactant and layer area per molecule * code clean * code cleaning * code cleaning * pr response * pr response and tests * pr response --- src/easyreflectometry/data/__init__.py | 2 + src/easyreflectometry/data/data_store.py | 9 +- src/easyreflectometry/model/model.py | 3 + .../model/model_collection.py | 27 ++--- src/easyreflectometry/project.py | 35 +++++- .../sample/assemblies/surfactant_layer.py | 32 +++++- .../sample/collections/base_collection.py | 26 +++++ .../sample/collections/layer_collection.py | 22 ++++ .../sample/collections/material_collection.py | 28 +---- .../sample/collections/sample.py | 17 ++- .../layers/layer_area_per_molecule.py | 17 ++- .../sample/elements/materials/material.py | 8 +- tests/data/test_data_store.py | 44 ++++++++ tests/model/test_model.py | 4 +- tests/model/test_model_collection.py | 2 +- .../collections/test_base_collection.py | 101 ++++++++++++++++++ .../collections/test_layer_collection.py | 24 +++++ .../collections/test_material_collection.py | 88 ++------------- tests/sample/collections/test_sample.py | 28 ++--- 19 files changed, 362 insertions(+), 155 deletions(-) create mode 100644 tests/data/test_data_store.py diff --git a/src/easyreflectometry/data/__init__.py b/src/easyreflectometry/data/__init__.py index 3676d427..fdecf1dd 100644 --- a/src/easyreflectometry/data/__init__.py +++ b/src/easyreflectometry/data/__init__.py @@ -1,7 +1,9 @@ +from .data_store import DataSet1D from .data_store import ProjectData from .measurement import load __all__ = [ load, ProjectData, + DataSet1D, ] diff --git a/src/easyreflectometry/data/data_store.py b/src/easyreflectometry/data/data_store.py index 65965b8b..aec66e57 100644 --- a/src/easyreflectometry/data/data_store.py +++ b/src/easyreflectometry/data/data_store.py @@ -81,7 +81,8 @@ def __init__( y_label: str = 'y', ): self._model = model - self._model.background = np.min(y) + if y is not None and model is not None: + self._model.background = np.min(y) if x is None: x = np.array([]) @@ -92,6 +93,9 @@ def __init__( if xe is None: xe = np.zeros_like(x) + if len(x) != len(y): + raise ValueError('x and y must be the same length') + self.name = name if not isinstance(x, np.ndarray): x = np.array(x) @@ -125,5 +129,8 @@ def is_experiment(self) -> bool: def is_simulation(self) -> bool: return self._model is None + def data_points(self) -> int: + return zip(self.x, self.y) + def __repr__(self) -> str: return "1D DataStore of '{:s}' Vs '{:s}' with {} data points".format(self.x_label, self.y_label, len(self.x)) diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index 426706c1..2d24e88e 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -60,6 +60,7 @@ def __init__( background: Union[Parameter, Number, None] = None, resolution_function: Union[ResolutionFunction, None] = None, name: str = 'EasyModel', + color: str = 'black', unique_name: Optional[str] = None, interface=None, ): @@ -81,6 +82,7 @@ def __init__( scale = get_as_parameter('scale', scale, DEFAULTS) background = get_as_parameter('background', background, DEFAULTS) + self.color = color super().__init__( name=name, @@ -173,6 +175,7 @@ def _dict_repr(self) -> dict[str, dict[str, str]]: 'scale': float(self.scale.value), 'background': float(self.background.value), 'resolution': resolution, + 'color': self.color, 'sample': self.sample._dict_repr, } } diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index 79c50a37..041d14b8 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -1,8 +1,7 @@ from __future__ import annotations -__author__ = 'github.com/arm61' - from typing import List +from typing import Optional from typing import Tuple from easyreflectometry.sample.collections.base_collection import BaseCollection @@ -35,21 +34,24 @@ def __init__( super().__init__(name, interface, *models, **kwargs) - def add_model(self, new_model: Model): - """ - Add a model to the models. + def add_model(self, model: Optional[Model] = None): + """Add a model to the collection. - :param new_model: New model to be added. + :param model: Model to add. """ - self.append(new_model) + if model is None: + model = Model(name='Model new', interface=self.interface) + self.append(model) - def remove_model(self, index: int): - """ - Remove an model from the models. + def duplicate_model(self, index: int): + """Duplicate a model in the collection. - :param index: Index of the model to remove + :param index: Model to duplicate. """ - self.pop(index) + to_be_duplicated = self[index] + duplicate = Model.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + duplicate.name = duplicate.name + ' duplicate' + self.append(duplicate) def as_dict(self, skip: List[str] | None = None) -> dict: this_dict = super().as_dict(skip=skip) @@ -62,7 +64,6 @@ def from_dict(cls, this_dict: dict) -> ModelCollection: Create an instance of a collection from a dictionary. :param data: The dictionary for the collection - :return: An instance of the collection """ collection_dict = this_dict.copy() # We neeed to call from_dict on the base class to get the models diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 018057cc..8b80988d 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -6,10 +6,11 @@ from typing import Optional from typing import Union +import numpy as np from easyscience import global_object from easyscience.fitting import AvailableMinimizers -from easyreflectometry.data.data_store import DataSet1D +from easyreflectometry.data import DataSet1D from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection from easyreflectometry.sample import Layer @@ -18,6 +19,29 @@ from easyreflectometry.sample import Sample from easyreflectometry.sample.collections.base_collection import BaseCollection +MODELS_SAMPLE_DATA = [ + DataSet1D( + name='Sample Data 0', + x=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + y=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + ) +] +MODELS_MODEL_DATA = [ + DataSet1D( + name='Model Data 0', + x=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + y=np.array([1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5]), + ) +] +EXPERIMENTAL_DATA = [ + DataSet1D( + name='Example Data 0', + x=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + y=np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]), + ye=np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]), + ) +] + class Project: def __init__(self): @@ -95,6 +119,15 @@ def experiments(self, experiments: List[DataSet1D]) -> None: def path_json(self): return self.path / 'project.json' + def sample_data_for_model_at_index(self, index: int = 0) -> DataSet1D: + return MODELS_SAMPLE_DATA[index] + + def model_data_for_model_at_index(self, index: int = 0) -> DataSet1D: + return MODELS_MODEL_DATA[index] + + def experimental_data_for_model_at_index(self, index: int = 0) -> DataSet1D: + return EXPERIMENTAL_DATA[index] + def default_model(self): self._replace_collection(MaterialCollection(), self._materials) diff --git a/src/easyreflectometry/sample/assemblies/surfactant_layer.py b/src/easyreflectometry/sample/assemblies/surfactant_layer.py index 9c7d4144..6f892a69 100644 --- a/src/easyreflectometry/sample/assemblies/surfactant_layer.py +++ b/src/easyreflectometry/sample/assemblies/surfactant_layer.py @@ -2,6 +2,7 @@ from typing import Optional +from easyscience import global_object from easyscience.Constraints import ObjConstraint from easyscience.Objects.new_variable import Parameter @@ -44,8 +45,17 @@ def __init__( :param conformal_roughness: Constrain the roughness to be the same for both layers, defaults to `False`. :param interface: Calculator interface, defaults to `None`. """ + if unique_name is None: + unique_name = global_object.generate_unique_name(self.__class__.__name__) + if tail_layer is None: - air = Material(0, 0, 'Air') + air = Material( + sld=0, + isld=0, + name='Air', + unique_name=unique_name + '_MaterialTail', + interface=interface, + ) tail_layer = LayerAreaPerMolecule( molecular_formula='C32D64', thickness=16, @@ -54,9 +64,17 @@ def __init__( area_per_molecule=48.2, roughness=3, name='DPPC Tail', + unique_name=unique_name + '_LayerAreaPerMoleculeTail', + interface=interface, ) if head_layer is None: - d2o = Material(6.36, 0, 'D2O') + d2o = Material( + sld=6.36, + isld=0, + name='D2O', + unique_name=unique_name + '_MaterialHead', + interface=interface, + ) head_layer = LayerAreaPerMolecule( molecular_formula='C10H18NO8P', thickness=10.0, @@ -65,8 +83,16 @@ def __init__( area_per_molecule=48.2, roughness=3.0, name='DPPC Head', + unique_name=unique_name + '_LayerAreaPerMoleculeHead', + interface=interface, ) - surfactant = LayerCollection(tail_layer, head_layer, name=name) + surfactant = LayerCollection( + tail_layer, + head_layer, + name='Layers', + unique_name=unique_name + '_LayerCollection', + interface=interface, + ) super().__init__( name=name, unique_name=unique_name, diff --git a/src/easyreflectometry/sample/collections/base_collection.py b/src/easyreflectometry/sample/collections/base_collection.py index 1c618226..851d1070 100644 --- a/src/easyreflectometry/sample/collections/base_collection.py +++ b/src/easyreflectometry/sample/collections/base_collection.py @@ -37,6 +37,32 @@ def names(self) -> list: """ return [i.name for i in self] + def move_up(self, index: int): + """Move the element at the given index up in the collection. + + :param index: Index of the element to move up. + """ + if index == 0: + return + self.insert(index - 1, self.pop(index)) + + def move_down(self, index: int): + """Move the element at the given index down in the collection. + + :param index: Index of the element to move down. + """ + if index == len(self) - 1: + return + self.insert(index + 1, self.pop(index)) + + def remove(self, index: int): + """ + Remove an element from the elements. + + :param index: Index of the element to remove + """ + self.pop(index) + @property def _dict_repr(self) -> dict: """ diff --git a/src/easyreflectometry/sample/collections/layer_collection.py b/src/easyreflectometry/sample/collections/layer_collection.py index 2ddfabcc..d3080ab2 100644 --- a/src/easyreflectometry/sample/collections/layer_collection.py +++ b/src/easyreflectometry/sample/collections/layer_collection.py @@ -20,3 +20,25 @@ def __init__( layers = [] super().__init__(name, interface, unique_name=unique_name, *layers, **kwargs) + + def add_layer(self, layer: Optional[Layer] = None): + """Add a layer to the collection. + + :param layer: Layer to add. + """ + if layer is None: + layer = Layer( + name='New EasyLayer', + interface=self.interface, + ) + self.append(layer) + + def duplicate_layer(self, index: int): + """Duplicate a layer in the collection. + + :param layer: Assembly to add. + """ + to_be_duplicated = self[index] + duplicate = Layer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + duplicate.name = duplicate.name + ' duplicate' + self.append(duplicate) diff --git a/src/easyreflectometry/sample/collections/material_collection.py b/src/easyreflectometry/sample/collections/material_collection.py index 3e1a2690..f4a42a10 100644 --- a/src/easyreflectometry/sample/collections/material_collection.py +++ b/src/easyreflectometry/sample/collections/material_collection.py @@ -45,7 +45,8 @@ def add_material(self, material: Optional[Material] = None): :param material: Material to add. """ if material is None: - material = Material(sld=2.074, isld=0.000, name='Si new', interface=self.interface) + material = Material(sld=2.074, isld=0.000, name='Si new') + material.interface = self.interface self.append(material) def duplicate_material(self, index: int): @@ -57,28 +58,3 @@ def duplicate_material(self, index: int): duplicate = Material.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) duplicate.name = duplicate.name + ' duplicate' self.append(duplicate) - - def move_material_up(self, index: int): - """Move the material at the given index up in the collection. - - :param index: Index of the material to move up. - """ - if index == 0: - return - self.insert(index - 1, self.pop(index)) - - def move_material_down(self, index: int): - """Move the material at the given index down in the collection. - - :param index: Index of the material to move down. - """ - if index == len(self) - 1: - return - self.insert(index + 1, self.pop(index)) - - def remove_material(self, index: int): - """Remove the material at the given index from the collection. - - :param index: Index of the material to remove. - """ - self.pop(index) diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py index ec9df191..c763c240 100644 --- a/src/easyreflectometry/sample/collections/sample.py +++ b/src/easyreflectometry/sample/collections/sample.py @@ -56,7 +56,10 @@ def add_assembly(self, assembly: Optional[BaseAssembly] = None): :param assembly: Assembly to add. """ if assembly is None: - assembly = Multilayer(name='New EasyMultilayer', interface=self.interface) + assembly = Multilayer( + name='New EasyMultilayer', + interface=self.interface, + ) self._enable_changes_to_outermost_layers() self.append(assembly) self._disable_changes_to_outermost_layers() @@ -78,26 +81,22 @@ def duplicate_assembly(self, index: int): self.append(duplicate) self._disable_changes_to_outermost_layers() - def move_assembly_up(self, index: int): + def move_up(self, index: int): """Move the assembly at the given index up in the sample. :param index: Index of the assembly to move up. """ - if index == 0: - return self._enable_changes_to_outermost_layers() - self.insert(index - 1, self.pop(index)) + super().move_up(index) self._disable_changes_to_outermost_layers() - def move_assembly_down(self, index: int): + def move_down(self, index: int): """Move the assembly at the given index down in the sample. :param index: Index of the assembly to move down. """ - if index == len(self) - 1: - return self._enable_changes_to_outermost_layers() - self.insert(index + 1, self.pop(index)) + super().move_down(index) self._disable_changes_to_outermost_layers() def remove_assembly(self, index: int): diff --git a/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py b/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py index 00737aa1..326fbb9e 100644 --- a/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py +++ b/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py @@ -89,12 +89,18 @@ def __init__( :param name: Name of the layer, defaults to "EasyLayerAreaPerMolecule" :param interface: Interface object, defaults to `None` """ - if solvent is None: - solvent = Material(6.36, 0, 'D2O', interface=interface) - if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) + if solvent is None: + solvent = Material( + sld=6.36, + isld=0, + name='D2O', + unique_name=unique_name + '_MaterialSolvent', + interface=interface, + ) + # Create the solvated molecule and corresponding constraints if molecular_formula is None: molecular_formula = DEFAULTS['molecular_formula'] @@ -102,8 +108,8 @@ def __init__( sld=0.0, isld=0.0, name=molecular_formula, + unique_name=unique_name + '_MaterialMolecule', interface=interface, - unique_name=unique_name + 'Material', ) thickness = get_as_parameter( @@ -155,14 +161,15 @@ def __init__( material=molecule_material, solvent=solvent, solvent_fraction=solvent_fraction, + unique_name=unique_name + '_MaterialSolvated', interface=interface, - unique_name=unique_name + 'MaterialSolvated', ) super().__init__( material=solvated_molecule_material, thickness=thickness, roughness=roughness, name=name, + unique_name=unique_name, interface=interface, ) self._add_component('_scattering_length_real', _scattering_length_real) diff --git a/src/easyreflectometry/sample/elements/materials/material.py b/src/easyreflectometry/sample/elements/materials/material.py index 1485abaa..aadf2dfd 100644 --- a/src/easyreflectometry/sample/elements/materials/material.py +++ b/src/easyreflectometry/sample/elements/materials/material.py @@ -69,7 +69,13 @@ def __init__( unique_name_prefix=f'{unique_name}_Isld', ) - super().__init__(name=name, sld=sld, isld=isld, interface=interface) + super().__init__( + name=name, + sld=sld, + isld=isld, + interface=interface, + unique_name=unique_name, + ) # Representation @property diff --git a/tests/data/test_data_store.py b/tests/data/test_data_store.py new file mode 100644 index 00000000..78d080da --- /dev/null +++ b/tests/data/test_data_store.py @@ -0,0 +1,44 @@ +from numpy.testing import assert_almost_equal + +from easyreflectometry.data.data_store import DataSet1D + + +class TestDataStore: + def test_constructor(self): + # When + data = DataSet1D( + x=[1, 2, 3], y=[4, 5, 6], ye=[7, 8, 9], xe=[10, 11, 12], x_label='label_x', y_label='label_y', name='MyDataSet1D' + ) + + # Then Expect + assert data.name == 'MyDataSet1D' + assert_almost_equal(data.x, [1, 2, 3]) + assert data.x_label == 'label_x' + assert_almost_equal(data.xe, [10, 11, 12]) + assert_almost_equal(data.y, [4, 5, 6]) + assert data.y_label == 'label_y' + assert_almost_equal(data.ye, [7, 8, 9]) + + def test_repr(self): + # When + data = DataSet1D( + x=[1, 2, 3], y=[4, 5, 6], ye=[7, 8, 9], xe=[10, 11, 12], x_label='label_x', y_label='label_y', name='MyDataSet1D' + ) + + # Then + repr = str(data) + + # Expect + assert repr == r"1D DataStore of 'label_x' Vs 'label_y' with 3 data points" + + def test_data_points(self): + # When + data = DataSet1D( + x=[1, 2, 3], y=[4, 5, 6], ye=[7, 8, 9], xe=[10, 11, 12], x_label='label_x', y_label='label_y', name='MyDataSet1D' + ) + + # Then + points = data.data_points() + + # Expect + assert list(points) == [(1, 4), (2, 5), (3, 6)] diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 97fd02a6..a86864d9 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -389,7 +389,7 @@ def test_repr(self): assert ( model.__repr__() - == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: 5.0 %\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: 5.0 %\n color: black\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 ) def test_repr_resolution_function(self): @@ -398,7 +398,7 @@ def test_repr_resolution_function(self): model.resolution_function = resolution_function assert ( model.__repr__() - == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: function of Q\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 + == 'EasyModel:\n scale: 1.0\n background: 1.0e-08\n resolution: function of Q\n color: black\n sample:\n EasySample:\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n - EasyMultilayer:\n EasyLayerCollection:\n - EasyLayer:\n material:\n EasyMaterial:\n sld: 4.186e-6 1/Å^2\n isld: 0.000e-6 1/Å^2\n thickness: 10.000 Å\n roughness: 3.300 Å\n' # noqa: E501 ) diff --git a/tests/model/test_model_collection.py b/tests/model/test_model_collection.py index ebaa418a..f9974ead 100644 --- a/tests/model/test_model_collection.py +++ b/tests/model/test_model_collection.py @@ -59,7 +59,7 @@ def test_delete_model(self): # Then collection = ModelCollection(model_1, model_2) - collection.remove_model(0) + collection.remove(0) # Expect assert len(collection) == 1 diff --git a/tests/sample/collections/test_base_collection.py b/tests/sample/collections/test_base_collection.py index 78279009..b9ad429e 100644 --- a/tests/sample/collections/test_base_collection.py +++ b/tests/sample/collections/test_base_collection.py @@ -63,3 +63,104 @@ def test_as_dict(self): assert p.as_dict()['name'] == 'name' assert len(p.as_dict()['data']) == 1 assert p.as_dict()['data'][0]['name'] == 'layer' + + def test_move_up(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + elem_3 = Layer(name='layer_3') + mock_interface = MagicMock() + + p = BaseCollection('name', mock_interface, elem_1, elem_2, elem_3) + p.append(Layer(name='layer_4')) + + # Then + p.move_up(3) + + # Expect + assert p[0].name == 'layer_1' + assert p[1].name == 'layer_2' + assert p[2].name == 'layer_4' + assert p[3].name == 'layer_3' + + def test_move_up_to_top_and_further(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + elem_3 = Layer(name='layer_3') + mock_interface = MagicMock() + + p = BaseCollection('name', mock_interface, elem_1, elem_2, elem_3) + p.append(Layer(name='layer_4')) + + # Then + p.move_up(3) + p.move_up(2) + p.move_up(1) + p.move_up(0) + + # Then + assert p[0].name == 'layer_4' + assert p[1].name == 'layer_1' + assert p[2].name == 'layer_2' + assert p[3].name == 'layer_3' + + def test_move_down(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + elem_3 = Layer(name='layer_3') + mock_interface = MagicMock() + + p = BaseCollection('name', mock_interface, elem_1, elem_2, elem_3) + p.append(Layer(name='layer_4')) + + # Then + p.move_down(2) + + # Expect + assert p[0].name == 'layer_1' + assert p[1].name == 'layer_2' + assert p[2].name == 'layer_4' + assert p[3].name == 'layer_3' + + def test_move_down_to_bottom_and_further(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + elem_3 = Layer(name='layer_3') + mock_interface = MagicMock() + + p = BaseCollection('name', mock_interface, elem_1, elem_2, elem_3) + p.append(Layer(name='layer_4')) + p.append(Layer(name='layer_5')) + + # Then + p.move_down(3) + p.move_down(4) + + # Then + assert p[0].name == 'layer_1' + assert p[1].name == 'layer_2' + assert p[2].name == 'layer_3' + assert p[3].name == 'layer_5' + assert p[4].name == 'layer_4' + + def test_remove(self): + # When + elem_1 = Layer(name='layer_1') + elem_2 = Layer(name='layer_2') + elem_3 = Layer(name='layer_3') + mock_interface = MagicMock() + + p = BaseCollection('name', mock_interface, elem_1, elem_2, elem_3) + p.append(Layer(name='layer_4')) + + # Then + p.remove(1) + + # Then + assert len(p) == 3 + assert p[0].name == 'layer_1' + assert p[1].name == 'layer_3' + assert p[2].name == 'layer_4' diff --git a/tests/sample/collections/test_layer_collection.py b/tests/sample/collections/test_layer_collection.py index 2e17d3ab..2f457448 100644 --- a/tests/sample/collections/test_layer_collection.py +++ b/tests/sample/collections/test_layer_collection.py @@ -88,3 +88,27 @@ def test_dict_round_trip(self): # Expect assert sorted(r.as_data_dict()) == sorted(s.as_data_dict()) + + def test_add_layer(self): + # When + p = LayerCollection() + m = Layer(name='Layer m') + + # Then + p.add_layer() + p.add_layer(m) + + # Expect + assert p[1] == m + + def test_duplicate_layer(self): + # When + p = LayerCollection() + m = Layer(name='Layer m') + p.add_layer(m) + + # Then + p.duplicate_layer(0) + + # Expect + assert p[1].name == 'Layer m duplicate' diff --git a/tests/sample/collections/test_material_collection.py b/tests/sample/collections/test_material_collection.py index e25074fe..514ef01f 100644 --- a/tests/sample/collections/test_material_collection.py +++ b/tests/sample/collections/test_material_collection.py @@ -75,97 +75,25 @@ def test_dict_round_trip(self): assert sorted(p.as_data_dict()) == sorted(q.as_data_dict()) def test_add_material(self): - # Given - p = MaterialCollection() - m = Material(6.908, -0.278, 'Boron') - # When - p.add_material(m) - - # Then - assert p[3] == m - - def test_duplicate_material(self): - # Given p = MaterialCollection() m = Material(6.908, -0.278, 'Boron') - p.add_material(m) - - # When - p.duplicate_material(3) # Then - assert p[4].name == 'Boron duplicate' - - def test_move_material_up(self): - # Given - p = MaterialCollection() - k = Material(0.487, 0.000, 'Bottom') - p.add_material(k) - - # When - p.move_material_up(3) - - # Then - assert p[2].name == 'Bottom' - assert p[3].name == 'Si' - - def test_move_material_up_to_top_and_further(self): - # Given - p = MaterialCollection() - m = Material(0.487, 0.000, 'Bottom') + p.add_material() p.add_material(m) - # When - p.move_material_up(3) - p.move_material_up(2) - p.move_material_up(1) - p.move_material_up(0) - - # Then - assert p[0].name == 'Bottom' - assert p[3].name == 'Si' - - def test_move_material_down(self): - # Given - p = MaterialCollection() - m = Material(0.487, 0.000, 'Bottom') - p.add_material(m) + # Expect + assert p[4] == m + def test_duplicate_material(self): # When - p.move_material_down(2) - - # Then - assert p[2].name == 'Bottom' - assert p[3].name == 'Si' - - def test_move_material_down_to_bottom_and_further(self): - # Given p = MaterialCollection() - k = Material(0.487, 0.000, 'Middle') - m = Material(0.487, 0.000, 'Bottom') - p.add_material(k) + m = Material(6.908, -0.278, 'Boron') p.add_material(m) - # When - p.move_material_down(3) - p.move_material_down(4) - # Then - assert p[0].name == 'Air' - assert p[3].name == 'Bottom' - assert p[4].name == 'Middle' - - def test_remove_material(self): - # Given - p = MaterialCollection() - m = Material(0.487, 0.000, 'Bottom') - p.add_material(m) - - # When - p.remove_material(1) + p.duplicate_material(3) - # Then - assert len(p) == 3 - assert p[0].name == 'Air' - assert p[2].name == 'Bottom' + # Expect + assert p[4].name == 'Boron duplicate' diff --git a/tests/sample/collections/test_sample.py b/tests/sample/collections/test_sample.py index da4d2180..1cca441a 100644 --- a/tests/sample/collections/test_sample.py +++ b/tests/sample/collections/test_sample.py @@ -45,14 +45,16 @@ def test_add_assembly(self): surfactant = SurfactantLayer() # Then + p.add_assembly() p.add_assembly(surfactant) # Expect assert_equal(p[0].name, 'EasyMultilayer') assert_equal(p[1].name, 'EasyMultilayer') - assert_equal(p[2].name, 'EasySurfactantLayer') - p._enable_changes_to_outermost_layers.assert_called_once_with() - p._disable_changes_to_outermost_layers.assert_called_once_with() + assert_equal(p[2].name, 'New EasyMultilayer') + assert_equal(p[3].name, 'EasySurfactantLayer') + p._enable_changes_to_outermost_layers.assert_called() + p._disable_changes_to_outermost_layers.assert_called() # Problems with parameterized tests START def test_duplicate_assembly_multilayer(self): @@ -123,7 +125,7 @@ def test_move_assembly_up(self): p._disable_changes_to_outermost_layers = MagicMock() # Then - p.move_assembly_up(2) + p.move_up(2) # Expect assert_equal(p[0].name, 'EasyMultilayer') @@ -141,14 +143,14 @@ def test_move_assembly_up_index_0(self): p._disable_changes_to_outermost_layers = MagicMock() # Then - p.move_assembly_up(0) + p.move_up(0) # Expect assert_equal(p[0].name, 'EasyMultilayer') assert_equal(p[1].name, 'EasyMultilayer') assert_equal(p[2].name, surfactant.name) - p._enable_changes_to_outermost_layers.assert_not_called() - p._disable_changes_to_outermost_layers.assert_not_called() + p._enable_changes_to_outermost_layers.assert_called() + p._disable_changes_to_outermost_layers.assert_called() def test_move_assembly_down(self): # When @@ -159,7 +161,7 @@ def test_move_assembly_down(self): p._disable_changes_to_outermost_layers = MagicMock() # Then - p.move_assembly_down(1) + p.move_down(1) # Expect assert_equal(p[0].name, 'EasyMultilayer') @@ -177,14 +179,14 @@ def test_move_assembly_down_index_2(self): p._disable_changes_to_outermost_layers = MagicMock() # Then - p.move_assembly_down(2) + p.move_down(2) # Expect assert_equal(p[0].name, 'EasyMultilayer') assert_equal(p[1].name, 'EasyMultilayer') assert_equal(p[2].name, surfactant.name) - p._enable_changes_to_outermost_layers.assert_not_called() - p._disable_changes_to_outermost_layers.assert_not_called() + p._enable_changes_to_outermost_layers.assert_called() + p._disable_changes_to_outermost_layers.assert_called() def test_remove_assembly(self): # When @@ -220,8 +222,8 @@ def test_superphase(self): p = Sample() layer = Multilayer(Layer(name='new layer')) p.add_assembly(layer) - p.move_assembly_up(2) - p.move_assembly_up(1) + p.move_up(2) + p.move_up(1) # Then layer = p.superphase From d6a661a25c94249ed76b9ae7ba506f04dc105f4c Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:43:32 +0200 Subject: [PATCH 08/11] 194 pass real reflectivity and sld data from the project method exposing lib data (#196) * expose reflectivity data * sld profile in project * reflectivity curve with 500 datapoints * code cleaning * remove fir_function from calculator_base * default minimizer * pr response * ruff --- docs/src/tutorials/magnetism.ipynb | 20 +-- .../sample/resolution_functions.ipynb | 132 +++++++++++++++--- .../calculators/bornagain/calculator.py | 20 --- .../calculators/calculator_base.py | 4 +- src/easyreflectometry/calculators/factory.py | 21 +++ src/easyreflectometry/model/model.py | 4 + .../model/model_collection.py | 3 +- src/easyreflectometry/project.py | 91 +++++++----- .../sample/assemblies/surfactant_layer.py | 1 + .../sample/collections/base_collection.py | 4 + .../refl1d/test_refl1d_calculator.py | 8 +- .../refnx/test_refnx_calculator.py | 6 +- tests/model/test_model.py | 4 +- tests/test_project.py | 59 +++++++- tests/test_topmost_nesting.py | 4 +- 15 files changed, 278 insertions(+), 103 deletions(-) diff --git a/docs/src/tutorials/magnetism.ipynb b/docs/src/tutorials/magnetism.ipynb index b1bd0e60..ee2ef4e9 100644 --- a/docs/src/tutorials/magnetism.ipynb +++ b/docs/src/tutorials/magnetism.ipynb @@ -248,7 +248,7 @@ "model.resolution_function = PercentageFhwm(0)\n", "model_interface = model.interface()\n", "model_interface.magnetism = False\n", - "model_data_no_magnetism_ref1d_easy = model.interface().fit_func(\n", + "model_data_no_magnetism_ref1d_easy = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -278,7 +278,7 @@ "model.interface = interface\n", "model_interface = model.interface()\n", "model_interface.include_magnetism = True\n", - "model_data_magnetism = model.interface().fit_func(\n", + "model_data_magnetism = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -291,7 +291,7 @@ "model_interface.include_magnetism = True\n", "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[1], magnetism_rhoM=10, magnetism_thetaM=70)\n", "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[2], magnetism_rhoM=5, magnetism_thetaM=175)\n", - "model_data_magnetism_layer_1 = model.interface().fit_func(\n", + "model_data_magnetism_layer_1 = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -351,7 +351,7 @@ "model_interface.include_magnetism = True\n", "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[1], magnetism_rhoM=10, magnetism_thetaM=70)\n", "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[2], magnetism_rhoM=5, magnetism_thetaM=175)\n", - "model_data_magnetism_easy = model.interface().fit_func(\n", + "model_data_magnetism_easy = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -474,7 +474,7 @@ "interface.switch('refnx')\n", "model.interface = interface\n", "model_interface = model.interface()\n", - "model_data_no_magnetism_refnx = model.interface().fit_func(\n", + "model_data_no_magnetism_refnx = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -484,7 +484,7 @@ "interface.switch('refl1d')\n", "model.interface = interface\n", "model_interface = model.interface()\n", - "model_data_no_magnetism_ref1d = model.interface().fit_func(\n", + "model_data_no_magnetism_ref1d = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -514,7 +514,7 @@ "model.interface = interface\n", "model_interface = model.interface()\n", "model_interface.magnetism = True\n", - "model_data_magnetism = model.interface().fit_func(\n", + "model_data_magnetism = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -525,7 +525,7 @@ "model.interface = interface\n", "model_interface = model.interface()\n", "model_interface.magnetism = False\n", - "model_data_no_magnetism = model.interface().fit_func(\n", + "model_data_no_magnetism = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -555,7 +555,7 @@ ], "metadata": { "kernelspec": { - "display_name": "erl_311", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -569,7 +569,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.11.10" } }, "nbformat": 4, diff --git a/docs/src/tutorials/sample/resolution_functions.ipynb b/docs/src/tutorials/sample/resolution_functions.ipynb index 26037dfd..6df180de 100644 --- a/docs/src/tutorials/sample/resolution_functions.ipynb +++ b/docs/src/tutorials/sample/resolution_functions.ipynb @@ -63,10 +63,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "549734c1-bbd9-41f3-8a20-d7a8ded37802", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "numpy: 1.26.0\n", + "scipp: 24.9.1\n", + "easyreflectometry: 1.1.1\n", + "refnx: 0.1.49\n" + ] + } + ], "source": [ "print(f'numpy: {np.__version__}')\n", "print(f'scipp: {sc.__version__}')\n", @@ -125,10 +136,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "31ab44d3-826a-4270-9046-ad667dcb66ba", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plot(dict_reference['0'])" ] @@ -191,10 +213,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "311b25a1-6d5d-4e91-a72e-394ad8dcf464", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "SLD 4/8 Layer:\n", + " SLD 4 Layer/SLD 8 Layer:\n", + " - SLD 4 Layer:\n", + " material:\n", + " Sld 4:\n", + " sld: 4.000e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 100.000 Å\n", + " roughness: 2.000 Å\n", + " - SLD 8 Layer:\n", + " material:\n", + " Sld 8:\n", + " sld: 8.000e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 150.000 Å\n", + " roughness: 2.000 Å" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "two_layers = Multilayer([sld_4_layer, sld_8_layer], name='SLD 4/8 Layer')\n", "two_layers" @@ -236,10 +284,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "f1500603-d85d-4e16-b697-e1bf16502991", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "refnx\n" + ] + } + ], "source": [ "interface = CalculatorFactory()\n", "model.interface = interface\n", @@ -292,10 +348,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "e59d3153-f0da-4fce-a4f0-a424010acbec", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAGdCAYAAADqsoKGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACCOElEQVR4nO3dd3hUVf7H8ffMJJNQEzoEAgkgiChFmqhACEjRRXGxlwVUVl1ZXREVF1LIoCg2XMUuguVnFxuCYEITQaUEaSIl9C4kQELazPn9MXNvZlInydTk+3qePJJ778w9k4nMh3O+5xyDUkohhBBCCBHkjP5ugBBCCCGEJ0ioEUIIIUSNIKFGCCGEEDWChBohhBBC1AgSaoQQQghRI0ioEUIIIUSNIKFGCCGEEDWChBohhBBC1Agh/m6Ar9hsNg4fPkyDBg0wGAz+bo4QQggh3KCU4uzZs0RFRWE0lt8XU2tCzeHDh4mOjvZ3M4QQQghRBQcOHKBNmzblXlNrQk2DBg0A+w+lYcOGfm6NEEIIIdxx5swZoqOj9c/x8tSaUKMNOTVs2FBCjRBCCBFk3CkdkUJhIYQQQtQIEmqEEEIIUSNIqBFCCCFEjVBramqEEEJUj1KKwsJCrFarv5siapjQ0FBMJlO1n0dCjRBCiArl5+dz5MgRcnJy/N0UUQMZDAbatGlD/fr1q/U8QRVqvvvuOx555BFsNhuPP/4499xzj7+bJIQQNZ7NZiMjIwOTyURUVBRms1kWMRUeo5TixIkTHDx4kAsuuKBaPTZBE2oKCwuZNGkSy5YtIyIigl69enH99dfTpEkTfzdNCCFqtPz8fGw2G9HR0dStW9ffzRE1ULNmzdi7dy8FBQXVCjVBUyj866+/0rVrV1q3bk39+vUZOXIkS5Ys8XezhBCi1qhoiXohqspTPX8++w1duXIlo0aNIioqCoPBwFdffVXimjlz5hATE0N4eDj9+vXj119/1c8dPnyY1q1b69+3bt2aQ4cO+aLppYuLg8hIGDKk5LkhQ6BRI/s1QgghhPAJn4Wa7Oxsunfvzpw5c0o9/8knnzBp0iSSkpLYsGED3bt3Z/jw4Rw/ftxXTawckwmysiAtzR5gNEOG2I9lZsIvv0iwEUIIIXzEZ6Fm5MiRzJgxg+uvv77U8y+88AITJkxg/PjxXHTRRbz++uvUrVuXuXPnAhAVFeXSM3Po0CGioqLKvF9eXh5nzpxx+fKo1FSIjbX/OTMTm9FYFGg0ubn2YBMT49l7CyGECBrjxo1j9OjR1X6e5ORkevToUe3nqckCYoA0Pz+f9evXM3ToUP2Y0Whk6NChrFmzBoC+ffuyZcsWDh06xLlz51i0aBHDhw8v8zlnzpxJRESE/uWVHbr37MHarp29vUq5BhqA8HB7sDl2THpshBDCD8aNG4fBYMBgMBAaGkpsbCyPPfYYubm5/m5auUor05g8eTKpqal+ac/vv//OgAEDCA8PJzo6mlmzZrmcX7p0KZ06daJhw4bceeed5Ofn6+eysrLo1KkT+/bt83o7AyLUnDx5EqvVSosWLVyOt2jRgqNHjwIQEhLC888/z+DBg+nRowePPPJIuTOfnnjiCbKysvSvAwcOeKXtmevXo0o7oQUa7b+bNkmwEUIIPxgxYgRHjhxhz549vPjii7zxxhskJSX5u1mVVr9+fb/M+D1z5gzDhg2jXbt2rF+/nmeffZbk5GTefPNNwD7l/7bbbuO+++5jzZo1rFu3Tj8HMGXKFO677z7aOToBvCkgQo27rr32Wv7880927drFP//5z3KvDQsL03fk9ubO3E1uuoniNdsKXANNZKS9xmbTJkhO9ko7hBDCl5RSZGdn+/xLqVL/GVmusLAwWrZsSXR0NKNHj2bo0KEsXbpUP2+z2Zg5cyaxsbHUqVOH7t278/nnn+vnT58+ze23306zZs2oU6cOF1xwAe+++65+fvPmzcTHx1OnTh2aNGnCP//5T86dO1dme2JiYpg9e7bLsR49epDs+HyIcZQsXH/99RgMBv374sNPNpuNlJQU2rRpQ1hYGD169GDx4sX6+b1792IwGPjyyy8ZPHgwdevWpXv37voIiLs+/PBD8vPzmTt3Ll27duWWW27hwQcf5IUXXgDsHRMnT57kX//6F127duXaa69l+/btAPz888/89ttvPPTQQ5W6Z1UFRKhp2rQpJpOJY8eOuRw/duwYLVu29FOr3FC8hsbBgFOw0QIN2P+7YoXPmieEEN6Sk5ND/fr1ff5V3RWNt2zZws8//4zZbNaPzZw5k/fee4/XX3+drVu38vDDD3PHHXewwvH3dUJCAtu2bWPRokVs376d1157jaZNmwL2STDDhw+nUaNG/Pbbb3z22Wf8+OOPTJw4scpt/O233wB49913OXLkiP59cS+99BLPP/88zz33HL///jvDhw/n2muvZefOnS7XTZ06lcmTJ5Oenk6nTp249dZbKSws1M8bDAbmzZtXZnvWrFnDwIEDXX5mw4cPZ8eOHZw+fZpmzZrRqlUrlixZQk5ODqtWraJbt24UFBRw//3388Ybb3hkCwR3BESoMZvN9OrVy2Ws0GazkZqaSv/+/f3YsnKUFmjCw/U/6sFGCzQao1F6a4QQwoe+++476tevT3h4OJdccgnHjx/n0UcfBeyTSp566inmzp3L8OHDad++PePGjeOOO+7gjTfeAGD//v307NmT3r17ExMTw9ChQxk1ahQA//d//0dubi7vvfceF198MfHx8bzyyiu8//77Jf6h7q5mzZoBEBkZScuWLfXvi3vuued4/PHHueWWW+jcuTPPPPMMPXr0KNELNHnyZK655ho6derE9OnT2bdvH7t27dLPd+7cmYiIiDLbc/To0VLLQ7RzBoOBTz/9FIvFQteuXenZsyd33XUXTz/9NIMHDyY8PJwrrriCzp0788orr1TlR+I2n60ofO7cOZcfYkZGBunp6TRu3Ji2bdsyadIkxo4dS+/evenbty+zZ88mOzub8ePH+6qJlWO1QlgY5OXZvy9eQwMlhqWIj7cHIZvNp00VQghPq1u3brlDLN68b2UNHjyY1157jezsbF588UVCQkIYM2YMALt27SInJ4errrrK5TH5+fn07NkTgPvvv58xY8awYcMGhg0bxujRo7n88ssB2L59O927d6devXr6Y6+44gpsNhs7duwoEQY85cyZMxw+fJgrrrjC5fgVV1zBpk2bXI5169ZN/3OrVq0AOH78OBdeeCEAf/zxR7Xbc+WVV7r0KP3555+89957bNy4kYEDB/LQQw8xcuRILr74YgYOHOjSJk/yWahZt24dgwcP1r+fNGkSAGPHjmXevHncfPPNnDhxgsTERI4ePaqPDXrrF6Lali+397jMm2ef3eQcaJyCjU4LNFDUWyM9NkKIIGUwGFw+yANZvXr16NixIwBz586le/fuvPPOO9x99916MFu4cKHLAq9gr8UB+5Ik+/bt4/vvv2fp0qUMGTKEBx54gOeee65K7TEajSVqgwoKCqr0XO4IDQ3V/6yt3GurxD+uW7ZsWWp5iHauNPfeey/PP/88NpuNjRs3cuONN1K3bl0GDRrEihUrvBZqfDb8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tltzu3bt9GM7d+5UgKpbt65bOz4Hgs2bNytANWrUyOW4zWbTP/B27NjhlXvfd999ClCjRo2y39Ox/YC+6nBycrmPv/HGGxWgHnnkkYpv5vi9+LhrVwWop59+2uV4dX5f7r33Xvfb4S3FXsett95q76Fp1arc1/f8888rQDVr1kydPXvWly2uNWrsLt2iepQUCguHiIgITMCzDRpAQoK+Tk2jRo2Kdu728iyUpk2bAvZ1rDRaPU1UVJRnN6D0Im013dOnT+uLGIL9teTk5BASElKlFXfd8Z///AeDwcC3337L+vXreaVRI73exRYaiikpqdzH33rrrYB9g8wK/35wzFqa5lip/dJLL7Uf98Dvi7ZRsbabuV847Vh/5IEH+Oijj5gG3H/kSLnF0A8++CAdOnTgxIkTzJkzp+r3L1YXBfb1gN544w3e79SJuW3b8uCDD+rF56IMXo9YAUJ6apQym83SUyOUUkrt2rVL7xFRSqkWLVooQKWnp/usDc61FNrGmloNw8CBA33WDk9o3LixAtTmzZv1YytXrlSAat++vVfvrfUoNGjQQCU49lEq0GppKug9OX/+vN6btGnTpgrvpf09CqgTJ0546iXoe4+ZzWb9d8FvqlBjptUwtWrVqurtL9ZTdODAAXXppZfqParTHD/30NBQ9dJLL1XtHkFKempEqZT01AiHhg0bAvZ9lqxWq30WlNNxX4iIiND/rPVwHDlyBLD31AQTbabWwYMH9WN79+4F8FovjeZ///sfXbp04aGzZ0lRim/69MFUUKD3OpS3Sm94eLg++2jJkiUV3ktbHTk6OlrvafOE2NhYIiMjyc/PL3fLCV/YNmaMS2+XO7O7br31Vlq0aMGRI0f46quvqnZjp56i8//9L8OGDePqDRuwAD8NG0aXDz9kxIgRFBQU8NBDDzFr1qyq3aeGk1BTi0ioEZr69evrfz579iy5ubkljntbSEgIDRo0AIq2adCGn6q1UJ0flBdq2rVr59V7N23alPQbbsAC7J8wgVG//GIfunP6kCwv2AxzLLzoTqjZsmULAN26dfNI2zUGg4GePXsCRRuA+svuceMIAwqMRowFBW5t3WA2m5kwYQIAb7/9dtVv7njP6sycycbt27EApx9+mCt/+IHbbruN77//nhkzZgD2qebff/991e9VQ0moEaIWCg8P12tWnBe/q1evnk/b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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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mNHirbc9OTnZuH/37t290obqOnv2rFFAusQxm2zlhRfWiwLSmTNnKkDdeuutZZ5TWFioAgICFKD27t1b420YNWqUAtSSJUtq/Nru+nzIEKVALWrfXhUWFhYdcFo9Wn9PX375ZQWouY0b14v3WghfIisKi1J5u8vfuaemSZMmXmxJ1QUFBfFSq1bEA//avZtcYOQPP9SLAtKhQ4cC5ffU7N+/n/z8fAIDA+nYsWPN3dyxJkz//v0B2Lx5c9Exd4btyltbxo3r/PHHH2z87TfigZ7vvONaExUfr610HR1tvKfTpk2jW7duzDpzhjWXXFI7PYw19NqEaMgk1DQg3g41zrOd6s3Mp1JEREQwB1yKS+t6oAGMQLFjx44yfxd27doFQLdu3Wq2+NmxsvS//vwTcAo17g7b6StUF//wd/M6zz77LIlKsX7CBEaPHl3yhGXLtC8Hf39/YmNjAfjXrl3YExIq195KyM/P5/333+eH1FRISODInXe6nlDHhzaFqFNqv+OobpDhJ6WaNGni1eGnpUuXGve/9NJLvdKGmjBlyhRjCKomNnz0lMLCQhUUFKQAtXv37lLPefbZZxWgrrjiippvgNNihe3atavSui82m00dvfNObffwpCSX61b2Ojk5OcZihN9//32l752dna3CwsLcfp7Bab0j3YYNG1TXrl1VHKhEp7VxPujfX2VlZcnaOEIoWXyvVBJqlGrUqJFXQ80333xj3P+aa67xShtqwo+jRxsfzu3bt69XHzwDBgxQgPrss89KPf5///d/ClAzZ86slfvnPvJIlcPg77//rvr27atw+vC3+fu7fZ033nhDAapz587KZrO51f477rhDAerGG29063lKqRK/Jxs3blRNmjQxXsvnQ4aoK664QsWbTEqBynP8WR9+r4SoTRJqSiGhRhkFoN4KNatXrzbuf8stt3ilDdXm1NsAqIsuusjl8br+AXTddddpBa9z55Z6fPz48QpQr776aq21Idc5kFTS3r17VYsWLRSgAgMDVePGjY3rFPr5uXX/888/XwHq0Ucfdbfpxu9wcHCwys7Odvv5+u/J2YcfVhEREUagOTNrlnHKqlWrjNeWbza7HbyE8DVSKCxK5dUVhYGQkJBS/16v2GzkzJzJHMe3F110kfaXam746Cm9e/cGKHOtmj8dNS+RkZG10wCr1WXdlzILY52KZpVS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ISd7QEClek9OnjzpuoheBfSNT5s0aVKjO5/n5OQY7di7d2+NXbdKKrMVRbFeHavVqgD1fOvWNVpb88EHH6gOHTq41KlOmTJFHTp0qEauX19IT40Qolwmk8noldFnG4WFhWE2e/Z/CfqmlsV7alq3bu3RdlRXt27dANeemj/++AOA7t271+q9W7Rowbp161g+dixWIGfmTEx5eUVrypTTcxAWFsbIkSMB+Prrryu81/r16wEYPHgwlhqctty4cWPOPfdcAFavXl1j162KbZMnG71ddn//0nu7ii2GOH36dEJCQrjr2DH+uPHGGllr54knnmDKlCkcPHiQ0NBQ4/fogw8+YNCgQfz+++/VvocvklAjRAOlhxp9Bd+wsDCPt6FFixZA/R5+gtJDjT781KNHj1q/f+uXXmJ0SgokJxM8d672oPNieeUEG32dmG+++abC++j7cvXv37/6jS5myJAhQNGChd6y7dprCQQKzGbMBQWl/+yKrbUTFhbGtGnTAHgoO7vaa+28//77zJw5E4D777+fo0ePsmvXLn7//Xf69evH0aNHueiii4wlU0QRCTVCNFCl9dR4mq+EGr2mZsLatcaHYImemtpcabZYz4GhEvUuF198MQArVqwgPz+/3NvooaZv377Va28pBg4cCMCGDRsq/6SkpLIDWxV+3qcffJArf/+deGCTvmVIJddtuv322wFtW4pjx465dV9nR44c4dZbbwXggQceYP78+QQFBQHQr18/Vq5cSVRUFBkZGVx99dUUFhZW+V6+SEKNEA1U8Z4aTxYJ64qHGv3DoL4OP/196hQkJGCfPZs9e/YAjp6a2l5ptljPgYsKVunt1asXzZs3Jzc311gssCy1GWr04acNGzZUvljYaT8qpRRbtmzh448/5vg997j/87Zaafrkk8QDPw4fzuDBgyvd2wXQu3dvBg8ejM1mM3Ywr4oHHniArKwshgwZwmOPPVbieFhYGJ988gmhoaH88ssvLFy4sMr38kUSaoRooIqHGn0mkif5Sk9Nhw4dCAwMZLbdzsn77sOclMSD+fkEBATQccmSOr3SrNls5rzzzgNg1apVZZ538uRJDh06BECfPn1qvB29e/cmICCAzMxM43eyQk6h493evenXrx8bJ0+m5XPP8V7v3mTefXel768KC3k6LIw5wB133FHyHpWok7nyyisBbc+tqti+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at++vRo5cqS6/PLLPdNYL2mQoWbo0KHqzjvvNL632WwqPDxczZ07t9Tzr7rqKnXppZe6PDZs2DB1++2312o7hcbd98tZp06dJNR4WHXeL6WUKiwsVE2bNlVLliyprSYKJ9V9v5RSauDAgSouLq42miecVOW9KiwsVOedd5569dVX1dSpU30+1DS44Sd9J/Fx48YZj1W0k/iaNWtczgcYP358jew8LspXlfdLeE9NvF9nzpyhoKCA5s2b11YzhUN13y+lFCkpKezcuZNRo0bVZlMbvKq+V8nJybRu3Zqbb77ZE830ujq5oWVtqspO4kePHi31/KNHj9ZaO4WmpnZ+F55RE+/XQw89RHh4eIl/SIiaV9X3KzMzk/bt25OXl4fFYuG///0vF154YW03t0Grynv1888/89prr5GWluaBFtYNDS7UCCHqrscff5z33nuP5cuXExQU5O3miDI0bdqUtLQ0srOzSUlJITY2li5duhAdHe3tpgmH06dPc+ONN/LKK6/QsmVLbzfHYxpcqKnKTuJt27aVnce9RHZ+r1+q837Nnz+fxx9/nB9//JH+/fvXZjOFQ1XfL7PZTLdu3QAYMGAA27dvZ+7cuRJqapG779WePXvYt28fEydONB6z2+0A+Pn5sXPnTrp27Vq7jfaCBldTU5WdxEeMGOFyPsAPP/wgO497gOz8Xr9U9f2aN28eVquVb7/9lsGDB3uiqYKa++/LbreTl5dXG00UDu6+Vz179mTz5s2kpaUZX5dddhljxowhLS2NiIgITzbfc7xdqewN7733ngoMDFRvvPGG2rZtm7rttttUWFiYOnr0qFJKqRtvvFHNnDnTOH/VqlXKz89PzZ8/X23fvl0lJibKlG4Pcvf9ysvLUxs3blQbN25U7dq1UzNmzFAbN25Uf/zxh7deQoPi7vv1+OOPq4CAAPXhhx+qI0eOGF+nT5/21ktoUNx9vx577DH1/fffqz179qht27ap+fPnKz8/P/XKK6946yU0GO6+V8U1hNlPDTLUKKXU888/rzp27KgCAgLU0KFD1S+//GIcGz16tJo6darL+e+//77q0aOHCggIUH369FFfffWVh1vcsLnzfqWnpyugxNfo0aM93/AGyp33q1OnTqW+X4mJiZ5veAPlzvv1yCOPqG7duqmgoCDVrFkzNWLECPXee+95odUNk7ufXc4aQqiRXbqFEEII4RMaXE2NEEIIIXyThBohhBBC+AQJNUIIIYTwCRJqhBBCCOETJNQIIYQQwidIqBFCCCGET5BQI4QQQgifIKFGCCGEED5BQo0QQgghfIKEGiGEEEL4BAk1QgghhPAJEmqEEEII4RP+H/NlsEh2fxxJAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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NUREROQVGGqIiIjIKzDUEBERkVdol6Hm1ltvRffu3XHHHXfUO7dmzRoMGjQIV155Jd588003tI6IiIjao3YZav75z3/inXfeqXfcaDQiNTUVeXl52LlzJ55//nn8/vvvbmghERERtTftMtTExcWhW7du9Y5v27YNV199NUJCQtC1a1dMmjQJ69atc0MLiYiIqL1xONR8++23mDx5MoKDg6FSqfD555/Xu2bp0qUIDw+Hr68vxo4di23btjmjrTh16hRCQkKUxyEhITh58qRT7k1ERESezeFQU1FRgaioKCxdutTu+VWrViE1NRXp6enYsWMHoqKikJycjNOnTyvXREdHY+jQofW+Tp061fKfhIiIiDo0raNPmDRpEiZNmtTg+RdffBHTp0/HtGnTAADLli3DF198geXLl+PJJ58EABQUFLSoscHBwTY9MydPnkRMTIzda6uqqlBVVaU8LisrAwCUl5e36LWJiIjI9eTPbSFEk9c6HGoaU11dje3bt2Pu3LnKMbVajaSkJGzdurXV94+JicGePXtw8uRJ+Pv746uvvkJaWprda7OzszF//vx6x0NDQ1vdDiIiInKtCxcuwN/fv9FrnBpqfvvtN5hMJgQGBtocDwwMxP79+5t9n6SkJPz888+oqKhAv379sHr1aowbNw5arRYvvPAC4uPjYTab8fjjj6Nnz5527zF37lykpqYqj81mM86ePYuePXtCpVI1+vrl5eUIDQ3F8ePH4efn1+x2k3vw/fIsfL88C98vz+Gt75UQAhcuXEBwcHCT1zo11DjLN9980+C5W265BbfcckuT99Dr9dDr9TbHAgICHGqHn5+fV/3F8HZ8vzwL3y/PwvfLc3jje9VUD43MqVO6e/XqBY1Gg9LSUpvjpaWlCAoKcuZLEREREdlwaqjR6XQYNWoUcnNzlWNmsxm5ubkYN26cM1+KiIiIyIbDw08XL17EwYMHlcdFRUUoKChAjx49EBYWhtTUVKSkpGD06NGIiYlBTk4OKioqlNlQnkCv1yM9Pb3e8BW1T3y/PAvfL8/C98tz8L0CVKI5c6Ss5OfnIz4+vt7xlJQUrFixAgDwyiuv4Pnnn0dJSQmio6Px8ssvY+zYsU5pMBEREZE9DocaIiIiovaoXe79REREROQohhoiIiLyCgw1RERE5BU6bKhxdCfx1atX46qrroKvry+GDRuGL7/80kUtJcCx92vv3r24/fbbER4eDpVKhZycHNc1lAA49n698cYbGD9+PLp3747u3bsjKSmpyf8eybkceb8+/fRTjB49GgEBAejSpQuio6Px7rvvurC1HZujn12yDz/8ECqVClOmTGnbBrqb6IA+/PBDodPpxPLly8XevXvF9OnTRUBAgCgtLbV7/ebNm4VGoxHPPfecKCwsFPPmzRM+Pj5i9+7dLm55x+To+7Vt2zYxZ84c8cEHH4igoCCxePFi1za4g3P0/br77rvF0qVLxc6dO8W+ffvE1KlThb+/vzhx4oSLW94xOfp+bdiwQXz66aeisLBQHDx4UOTk5AiNRiPWrl3r4pZ3PI6+V7KioiIREhIixo8fL/7whz+4prFu0iFDTUxMjHj44YeVxyaTSQQHB4vs7Gy71//xj38UN910k82xsWPHioceeqhN20kSR98va/3792eocbHWvF9CCGE0GkW3bt3EypUr26qJZKW175cQQowYMULMmzevLZpHVlryXhmNRnHttdeKN998U6SkpHh9qOlww0/yTuJJSUnKsaZ2Et+6davN9QCQnJzslJ3HqXEteb/IfZzxfl26dAk1NTXo0aNHWzWTLFr7fgkhkJubiwMHDuCGG25oy6Z2eC19rzIzM9GnTx888MADrmim27XLDS3bUkt2Ei8pKbF7fUlJSZu1kyTO2vmdXMMZ79cTTzyB4ODgev+QIOdr6ftVVlaGkJAQVFVVQaPR4N///jcmTJjQ1s3t0FryXn333Xd46623UFBQ4IIWtg8dLtQQUfv1zDPP4MMPP0R+fj58fX3d3RxqQLdu3VBQUICLFy8iNzcXqampGDBgAOLi4tzdNLK4cOEC7r33Xrzxxhvo1auXu5vjMh0u1LRkJ/GgoCDuPO4m3Pnds7Tm/Vq0aBGeeeYZfPPNNxg+fHhbNpMsWvp+qdVqREZGAgCio6Oxb98+ZGdnM9S0IUffq0OHDuHIkSOYPHmycsxsNgMAtFotDhw4gCuuuKJtG+0GHa6mpiU7iY8bN87megBYv349dx53Ae787lla+n4999xzyMrKwtq1azF69GhXNJXgvP++zGYzqqqq2qKJZOHoe3XVVVdh9+7dKCgoUL5uueUWxMfHo6CgAKGhoa5svuu4u1LZHT788EOh1+vFihUrRGFhoZgxY4YICAgQJSUlQggh7r33XvHkk08q12/evFlotVqxaNEisW/fPpGens4p3S7k6PtVVVUldu7cKXbu3Cn69u0r5syZI3bu3Cl+/fVXd/0IHYqj79czzzwjdDqd+Pjjj0VxcbHydeHCBXf9CB2Ko+/XwoULxbp168ShQ4dEYWGhWLRokdBqteKNN95w14/QYTj6XtXVEWY/dchQI4QQS5YsEWFhYUKn04mYmBjx/fffK+diY2NFSkqKzfUfffSRGDhwoNDpdOLqq68WX3zxhYtb3LE58n4VFRUJAPW+YmNjXd/wDsqR96t///5236/09HTXN7yDcuT9euqpp0RkZKTw9fUV3bt3F+PGjRMffvihG1rdMTn62WWtI4Qa7tJNREREXqHD1dQQERGRd2KoISIiIq/AUENERERegaGGiIiIvAJDDREREXkFhhoiIiLyCgw1RERE5BUYaoiIiMgrMNQQERGRV2CoISIiIq/AUENERERegaGGiIiIvML/Bw7ZetL11/ORAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "for key in resolution_function_dict.keys():\n", " reference_coords = dict_reference[key]['coords']['Qz_0'].values\n", @@ -306,7 +393,7 @@ " num=1000,\n", " )\n", " model.resolution_function = resolution_function_dict[key]\n", - " model_data = model.interface().fit_func(\n", + " model_data = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", " )\n", @@ -341,10 +428,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "ca0932f6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAGdCAYAAADqsoKGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACNZElEQVR4nO3dd3xT1fvA8U+6WS1TSqHsPcsWB8vKFMQJCDJkqICiyE/EhesrDtyiIoIgyhBFlCGCyFCGzCJ7yd4IFFqgtM39/XHuuUnTFDqTNn3er1dfNzl3nSSleXjOshmGYSCEEEIIkcf5ebsCQgghhBDZQYIaIYQQQvgECWqEEEII4RMkqBFCCCGET5CgRgghhBA+QYIaIYQQQvgECWqEEEII4RMkqBFCCCGETwjwdgU8xW63c/z4cYoUKYLNZvN2dYQQQgiRDoZhcOnSJSIiIvDzu34uJt8ENcePHycyMtLb1RBCCCFEJhw5coRy5cpd95h8E9QUKVIEUG9KaGiol2sjhBBCiPS4ePEikZGR1vf49eSboEY3OYWGhkpQI4QQQuQx6ek6Ih2FhRBCCOETJKgRQgghhE+QoEYIIYQQPiHf9KkRQgiR+xmGQVJSEsnJyd6uivCgwMBA/P39s3wdCWqEEELkCteuXePEiRNcvnzZ21URHmaz2ShXrhyFCxfO0nXyVFAzf/58nnnmGex2O6NGjWLgwIHerpIQQohsYLfbOXDgAP7+/kRERBAUFCQTpeYThmFw5swZjh49SrVq1bKUsckzQU1SUhIjRoxg2bJlhIWF0bhxY+655x5KlCjh7aoJIYTIomvXrmG324mMjKRgwYLero7wsFKlSnHw4EESExOzFNTkmY7C69ato06dOpQtW5bChQvTsWNHFi9e7O1qsXv3btq2bcuePXu8XRUhhMjzbjQNvvBN2ZWV89hvz8qVK+nSpQsRERHYbDbmzp2b6pjx48dTsWJFQkJCaN68OevWrbP2HT9+nLJly1rPy5Yty7FjxzxR9TQlJiby8MMPs2zZMqKiovjss88wDMOrdRJCCCHyK48FNfHx8TRo0IDx48e73T9r1ixGjBjBmDFj2LRpEw0aNKB9+/acPn3aU1XMsDfffJP169cDcOXKFYYOHUqHDh28HmwJIYQQ+ZHHgpqOHTvyxhtvcM8997jd//777zNo0CD69+9P7dq1+eKLLyhYsCCTJ08GICIiIkWwcOzYMSIiItK8X0JCAhcvXkzxk53Wr1/P66+/DkBzoL5ZvnjxYurVq8dvv/2WrfcTQgiRv6TVqpGWV155haioqOse069fP7p165aleuVmuaLx8tq1a2zcuJHo6GirzM/Pj+joaNasWQNAs2bN2LZtG8eOHSMuLo5ff/2V9u3bp3nNsWPHEhYWZv1k5wrdly9f5uGHHyY5OZkiwExgPfAs6g09f/48nTt3ZuLEidl2TyGEELlPly5d6NChg9t9f/75JzabjX/++SdT1z5x4gQdO3bMSvXynVwx+uns2bMkJydTunTpFOWlS5dm165dAAQEBPDee+/Rpk0b7HY7zz777HVHPo0ePZoRI0ZYz/Uqn9nhvffeY/fu3QAEApuBisDbQAfgfuBccjKDBw8mICCA/v37Z8t9hRAivyhSpAjXrl3zah2CgoK4dOnSdY8ZMGAA9913H0ePHqVcuXIp9n399dc0adKE+vXrp3G2e9euXSMoKIjw8PAM1zm/yxWZmvTq2rUre/bsYd++fQwePPi6xwYHB1srcmf3ytzPPPMMw4YNA+AccC/QD7gItAFWA7pL88CBA1m4cGG23VsIIfKDa9eu5YqfG7nrrrsoVaoUU6ZMSVEeFxfH7Nmz6datGz179qRs2bIULFiQevXqMWPGjBTHtm7dmmHDhvHUU09RsmRJqxXCtflp1KhRVK9enYIFC1K5cmVeeuklEhMTU9VpwoQJ1tD4Bx98kNjY2DTrb7fbGTt2LJUqVaJAgQI0aNCAH3744YavO7fKFUFNyZIl8ff359SpUynKT506lSsj1YIFC/LJJ5+wePFia0TWVKAFcAioASwBSqB+YR5++GGOHj3qtfoKIYTIGQEBAfTp04cpU6akGP06e/ZskpOT6d27N40bN2bBggVs27aNwYMH8/DDD6cY3QswdepUgoKCWLVqFV988YXbexUpUoQpU6awY8cOPvroIyZOnMgHH3yQ4ph9+/bx/fffM2/ePBYtWsTmzZsZMmRImvUfO3Ys33zzDV988QXbt2/n6aefpnfv3qxYsSIL74oXGV4AGD/99FOKsmbNmhnDhg2znicnJxtly5Y1xo4dmy33jI2NNQAjNjY2W66nnTt3zujRo4cBGIARCcZhMAww/gDDzyxv2bKlkZycnK33FkIIX3HlyhVjx44dxpUrVwzDMIygoCDr76q3foKCgtJV9507dxqAsWzZMqvs9ttvN3r37u32+M6dOxvPPPOM9bxVq1ZGw4YNUx3n7rvS2bvvvms0btzYej5mzBjD39/fOHr0qFX266+/Gn5+fsaJEycMwzCMvn37GnfffbdhGIZx9epVo2DBgsbq1atTXHfAgAFGz54907xvTnD9/J1l5PvbY5mauLg4YmJiiImJAeDAgQPExMRw+PBhAEaMGMHEiROZOnUqO3fu5PHHHyc+Pj7X90cpVqwY06dP56233gLgCNAeiEM1Rb1gHrdy5UqmTp3qnUoKIYTIMTVr1uSWW26xRuvu27ePP//8kwEDBpCcnMzrr79OvXr1KF68OIULF+a3336zvvu0xo0b3/A+s2bN4tZbbyU8PJzChQvz4osvprpO+fLlU8zp1qJFC+x2u9UP1Nm+ffu4fPkyd955J4ULF7Z+vvnmG/bv35+Zt8LrPNZReMOGDbRp08Z6rjvx9u3blylTptC9e3fOnDnDyy+/zMmTJ4mKimLRokWpOg/nRjabjVGjRmEYBqNHj2Yn8DgwDXgZmANsB5577jnuvfdewsLCvFldIYQQ2WzAgAE88cQTjB8/nq+//poqVarQqlUr3n77bT766CM+/PBD6tWrR6FChXjqqadS9dcpVKjQda+/Zs0aevXqxauvvkr79u0JCwtj5syZvPfee5muc1xcHAALFixIEQiB6peaF3ksqGnduvUNZ9sdNmyY1QE3Lxo1ahT//PMPM2bM4FvgHlQn4o+BO4DTp0/z1ltvMXbsWK/WUwghcrugoCBvVyFDdXjwwQcZPnw406dP55tvvuHxxx/HZrOxatUq7r77bnr37g2ofpZ79uyhdu3aGarL6tWrqVChAi+88IJVdujQoVTHHT58mOPHj1vzuK1duxY/Pz9q1KiR6tjatWsTHBzM4cOHadWqVYbqk1vliiHdvsJms/HFF1+watUqDh8+zAigI9AWuBv4GbUUxLPPPkuxYsW8WlchhMjNbjSUOrcpXLgw3bt3Z/To0Vy8eJF+/foBUK1aNX744QdWr15NsWLFeP/99zl16lSGg5pq1apx+PBhZs6cSdOmTVmwYAE//fRTquNCQkLo27cv48aN4+LFizz55JM8+OCDbgfdFClShJEjR/L0009jt9u57bbbiI2NZdWqVYSGhtK3b99MvRfelCtGP/mS0NBQKx14CND90l80t5cuXeKTTz7xRtWEEELkoAEDBnD+/Hnat29vZUpefPFFGjVqRPv27WndujXh4eGZmtG3a9euPP300wwbNoyoqChWr17NSy+9lOq4qlWrcu+999KpUyfatWtH/fr1+eyzz9K87uuvv85LL73E2LFjqVWrFh06dGDBggVUqlQpw3XMDWzGjdqEfMTFixcJCwsjNjY2W+escccwDNq2bcvy5cspCRwECqEm5vsNKF68OMeOHSMkJCRH6yGEEHnF1atXOXDgAJUqVZK/jfnQ9T7/jHx/S6YmB9hsNl5++WUAzgITzPL/M7fnzp1jzpw53qiaEEII4bMkqMkhrVu3pkWLFgB8CNhRnYWrmvsnTJjg/kQhhBBCZIoENTnEZrNZw9aPAL+a5QPN7cqVK611rYQQQgiRdRLU5KC7777bmmfnS7OsP2oRTIDvvvvOG9USQgghfJIENTkoMDDQmhF5AXASuAnVDAXw/fff33DuHiGEEEKkjwQ1OUwHNcmAXve0u7nds2cPW7Zs8Ua1hBBCCJ8jQU0Oq169Og0bNgRgllnWDdDzVH7//fdeqJUQQgjheySo8YAHH3wQgFXAcaAo0M7c525GSCGEEEJknAQ1HvDAAw8Aai37H82yLuZ2165d/Pvvv96olhBCCOFTJKjxgCpVqtCgQQMA5ptlnZz2L1y40ON1EkIIkT369euHzWbDZrMRFBRE1apVee2110hKSvJ21a7LZrMxd+5cb1cjW0lQ4yGdO3cGYAUQD5QD6pv7FixY4KVaCSGEyA4dOnTgxIkT7N27l2eeeYZXXnmFd999N8PXSU5Oxm6350AN8wcJajykUyeVm0kAlpplnc3tsmXLiI+P90a1hBBCZIPg4GDCw8OpUKECjz/+ONHR0fzyyy8kJCQwcuRIypYtS6FChWjevDnLly+3zpsyZQpFixbll19+oXbt2gQHB3P48GESEhIYNWoUkZGRBAcHU7VqVSZNmmSdt23bNjp27EjhwoUpXbo0Dz/8MGfPnrX2t27dmieffJJnn32W4sWLEx4eziuvvGLtr1ixIgD33HMPNpvNer5//35rjrXChQvTtGlTfv/99xSv9cSJE3Tu3JkCBQpQqVIlpk+fTsWKFfnwww+tYy5cuMDAgQMpVaoUoaGhtG3b1iOjfSWo8ZCbb76Z4sWLA6Abmzqa24SEBFatWuWVegkhRK5lGBAf7/mfbJg/rECBAly7do1hw4axZs0aZs6cyT///MMDDzxAhw4d2Lt3r3Xs5cuXefvtt/nqq6/Yvn07N910E3369GHGjBl8/PHH7Ny5kwkTJlC4cGFABQxt27alYcOGbNiwgUWLFnHq1ClrUIo2depUChUqxN9//80777zDa6+9xpIlSwBYv349AF9//TUnTpywnsfFxdGpUyeWLl3K5s2b6dChA126dOHw4cPWdfv06cPx48dZvnw5P/74I19++SWnT59Oce8HHniA06dP8+uvv7Jx40YaNWrEHXfcwblz57L83l6XkU/ExsYagBEbG+u1OvTs2dMAjErqn4yRAEZB1X/YGD16tNfqJYQQ3nblyhVjx44dxpUrVxyFcXGGYf699OhPXFyG6t63b1/j7rvvNgzDMOx2u7FkyRIjODjY6Nevn+Hv728cO3YsxfF33HGH9Tf/66+/NgAjJibG2r97924DMJYsWeL2fq+//rrRrl27FGVHjhwxAGP37t2GYRhGq1atjNtuuy3FMU2bNjVGjRplPQeMn3766Yavr06dOsYnn3xiGIZh7Ny50wCM9evXW/v37t1rAMYHH3xgGIZh/Pnnn0ZoaKhx9erVFNepUqWKMWHCBLf3cPv5mzLy/R2QsyGTcNa2bVtmzJjBAeAwUB64Bfgd1QQlhBAib5o/fz6FCxcmMTERu93OQw89xP3338+UKVOoXr16imMTEhIoUaKE9TwoKIj69etbz2NiYvD396dVq1Zu77VlyxaWLVtmZW6c7d+/37qf8zUBypQpkyqj4iouLo5XXnmFBQsWcOLECZKSkrhy5YqVqdm9ezcBAQE0atTIOqdq1aoUK1YsRf3i4uJSvEaAK1eusH///uveP6skqPGgNm3aWI+XAX2BNqigZv369cTFxbn9JRVCiHypYEGIi/POfTOoTZs2fP755wQFBREREUFAQACzZs3C39+fjRs34u/vn+J457/1BQoUwGazpXh+PXFxcXTp0oW333471b4yZcpYjwMDA1Pss9lsN+yEPHLkSJYsWcK4ceOoWrUqBQoU4P777+fatWvXPc+1fmXKlEnRd0grWrRouq+TGRLUeFDlypWJjIzkyJEjKYIaUD3e//rrLzp06ODFGgohRC5is0GhQt6uRboUKlSIqlWrpihr2LAhycnJnD59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sg2/4Ogw/cILL6QImEGN9rr77rsB+ATVtFgJx6rlEydOlJFQQogcJUGNj3P+4vFGUKMzNc5DuvNSUFOpUiVANafo1+A6o05lc7vPqaxWrVqZut9dd91lBaI6L6OboM6ePWsFGqCyMy+99BLt27dn69atqa61a9cuOnfuzP/+978M10Mvz+CPownpa3MbERFBjx493J6n+85cAfQa4zqfc+bMGWbPnu3uNCGEyBYS1Pg450xNolO5pzM1zkO6dXOKc1CTG0c/AZQvX956rHuElHc5ppK5/dfc2mw2KlSokKn7FS1alDvuUOOGdL+adqh5YSDlKKgXXniBN954A4BwVBDxHyoAm4Zj5NSLL77Il19+me46GIbBt99+a907HDWf0K/m/t69e6f4vXJWv359WrZsCcDnqCar9jj6In333XfprocQQmSUBDU+ztvNT66ZmrzW/BQZGWk9PqzLXI7RmRod1JQtWzZL76tugtqOatIKBjqa+3766ScMw2DmzJnW/C9RqFFXj6De26Ko7Mp6oIZ53hNPPJHu2X137drFv/+qV6PzMdNxBMV9+/a97vl6XpuDgF4oQV/n999/57///nNzlhBCZJ0ENT7O281Prpka5+Yn56+2YsWK5Wg9Mss5qNGZGtegRmdqDpjbypUrkxV33323tXK66yioo0ePMm3aNGsIdQVgCVAKNZNxa+A2YCuqA/McoCBqePbTTz+drs7Dv/6qcjI2QK8xruvRuHHjFCuYu9OtWzcr8zbTLHvQ3CYlJTF37twb1kEIITJDghofl9syNQVQM9OCI1NTsGBBQkJCcrQemVWkSBFrYc60mp9cMzW6H05mhYeHc+uttwKOfjWdURkbUJmS+Ph4glGzD5dETXzXCrXK9yogGrWuVm3UEgYAS5YsSdew8EWLFgHQCDXZ3kUc61npNaquJzAwkHvvVWHYz6iRdvVQo6gAvv/++xteQwghMkOCGh+XW/rUXHIq00HBBXObW7M0ms7WuGt+CsOReTpobrOaqQFHE9R61ErmRXCMItLeAZoAZ1GZnItO+04DT5qP/w9HIPnBBx9c977x8fGsWLECcDR5/Y4jINZDzm/kwQdVbuYCjiYovfL4H3/8waVLl9ycJYQQWSNBjY9zbn5Kdir3dKYmEcdw5whzazVJOS18mRvpoMZd85POyZxCLegI2RvUGKgJ7wCG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5aWoz2M48CBwK6rfVlNUxsYPOHPmDMOHD0/XfYTIzySoyUe83VE41qnMOajRQ75zu8aN1Xin353K9ApKBQoUoGbNmqnOyQ0iIiKsbJkOanRAceHChRTz1eilOw+a2+zqJ+QcWOkskM4K7d271+05b775JufPn8eG6qhbHDUrcSQQjpr5NwCYiSMYe+qpp4iNjU11rS+//FLVAxU0JKEm1APo3Lmz1RE8LX369AHz/gdQwYhenPPnn39OVxPU9OnTrazUu6hpAfagFhp1DvjXAe1RTYWdcAwjnz17NvPnz7/hfYTIzySoyUfc/X87KCgox+974cIFIOUf7sNOj7NrVeucpvtWnEL1f5gDfGruu/fee3NtcGaz2axVvHVA4bze9ooVK6zHOqjRn4/zCuVZUbVqVeuxDmF0Hfbv35+qo29sbCyff/45APejZvq9iMpknENla3qjFsosgpr5F1RG4/XXX09xrW3btlmjiPSIp3nASfPxoEGDuJEHH3zQevyLub3b3O7bt++GnZ2Tk5N59dVXARVYDTDLH0EF+AEBASxYsICnn34aUE1sz5jH/A/VXAcwYsSIdHWK9qbLly8zbdo0HnnkETp06MB9993H66+/bvXbEiJHGflEbGysARixsbHerorH3X333QZgDADDAGOMyrobzZo188j9b7nlFgMwipr3N8C4z6xDXvoVtNvtRvv27a1665/Q0FBj79693q7edT300EMGYDQy3//jTvWvUKGCARjFnD6fYHPf2LFjs60O5cuXNwDjCfMePzrVwfX9++yzz6x9G1x+b51/KoJxydx/v1kWGBho7N+/37rWk08+aWC+prPmsR3NY8uWLWskJiamq/6NGjUyAKONeY3TYNjM67z33nvXPXfevHlWnWea5892eh2vvfaaYRjqd6xbt24G5rVXmcdOczp22rRpGXznPcNutxvffPONUapUqVSfk/7p1auXcfr0aW9XVeQxGfn+zjvfKFmUn4OaRYsWGQEBAQZglHL6A/PNN9945P6jR482AMPP6UuzhVmHqlWreqQO2eXkyZPGPffcY72H1atXNxYvXuztat3Q888/bwBGcafPIMTlCyfKLD/hVPbdd99lWx2io6MNwOhg3ucfp/ssXLgwxbGNGzc2AKOpeWw8GCXMY6Ojo42ZM2da544xj9kNRoBZ1r9/f8MwDOPy5ctG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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "key = '1'\n", "reference_coords = dict_reference[key]['coords']['Qz_0'].values\n", @@ -356,14 +454,14 @@ ")\n", "\n", "model.resolution_function = resolution_function_dict[key]\n", - "model_data = model.interface().fit_func(\n", + "model_data = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", "plt.plot(model_coords, model_data, 'k-', label=f'Variable', linewidth=5)\n", "\n", "model.resolution_function = PercentageFhwm(1.0)\n", - "model_data = model.interface().fit_func(\n", + "model_data = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", @@ -380,7 +478,7 @@ ], "metadata": { "kernelspec": { - "display_name": "erl_311", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -394,7 +492,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.11.10" } }, "nbformat": 4, diff --git a/src/easyreflectometry/calculators/bornagain/calculator.py b/src/easyreflectometry/calculators/bornagain/calculator.py index 205cf935..5788bff4 100644 --- a/src/easyreflectometry/calculators/bornagain/calculator.py +++ b/src/easyreflectometry/calculators/bornagain/calculator.py @@ -1,6 +1,5 @@ __author__ = 'github.com/arm61' -import numpy as np from easyscience.Objects.Inferface import ItemContainer from easyreflectometry.model import Model @@ -180,22 +179,3 @@ def remove_item_from_model(self, item_id: int) -> None: :type layer_id: int """ self._wrapper.remove_item(item_id) - - def fit_func(self, x_array: np.ndarray) -> np.ndarray: - """ - Function to perform a fit - :param x_array: points to be calculated at - :type x_array: np.ndarray - :return: calculated points - :rtype: np.ndarray - """ - return self._wrapper.calculate(x_array) - - def sld_profile(self) -> tuple([np.ndarray, np.ndarray]): - """ - Return the scattering length density profile. - - :return: z and sld(z) - :rtype: tuple[np.ndarray, np.ndarray] - """ - return self._wrapper.sld_profile() diff --git a/src/easyreflectometry/calculators/calculator_base.py b/src/easyreflectometry/calculators/calculator_base.py index 8811b7c3..0913c108 100644 --- a/src/easyreflectometry/calculators/calculator_base.py +++ b/src/easyreflectometry/calculators/calculator_base.py @@ -158,8 +158,8 @@ def remove_item_from_model(self, item_id: str, model_id: str) -> None: """ self._wrapper.remove_item(item_id, model_id) - def fit_func(self, x_array: np.ndarray, model_id: str) -> np.ndarray: - """Function to perform a fit. + def reflectity_profile(self, x_array: np.ndarray, model_id: str) -> np.ndarray: + """Determines the reflectivity profile for the given range and model. :param x_array: points to be calculated at :param model_id: The model id diff --git a/src/easyreflectometry/calculators/factory.py b/src/easyreflectometry/calculators/factory.py index 6b2d087a..e328fae5 100644 --- a/src/easyreflectometry/calculators/factory.py +++ b/src/easyreflectometry/calculators/factory.py @@ -1,4 +1,5 @@ __author__ = 'github.com/wardsimon' +from typing import Callable from easyscience.Objects.Inferface import InterfaceFactoryTemplate @@ -14,3 +15,23 @@ def reset_storage(self) -> None: def sld_profile(self, model_id: str) -> tuple: return self().sld_profile(model_id) + + @property + def fit_func(self) -> Callable: + """ + Pass through to the underlying interfaces fitting function. + + :param x_array: points to be calculated at + :type x_array: np.ndarray + :param args: positional arguments for the fitting function + :type args: Any + :param kwargs: key/value pair arguments for the fitting function. + :type kwargs: Any + :return: points calculated at positional values `x` + :rtype: np.ndarray + #""" + + def __fit_func(*args, **kwargs): + return self().reflectity_profile(*args, **kwargs) + + return __fit_func diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index 2d24e88e..d4c4c9e4 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -8,6 +8,7 @@ from typing import Union import numpy as np +from easyscience import global_object from easyscience.Objects.new_variable import Parameter from easyscience.Objects.ObjectClasses import BaseObj @@ -74,6 +75,8 @@ def __init__( :param interface: Calculator interface, defaults to `None`. """ + if unique_name is None: + unique_name = global_object.generate_unique_name(self.__class__.__name__) if sample is None: sample = Sample(interface=interface) @@ -92,6 +95,7 @@ def __init__( background=background, ) self.resolution_function = resolution_function + # Must be set after resolution function self.interface = interface diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index 041d14b8..3ca9ee76 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -20,6 +20,7 @@ def __init__( *models: Tuple[Model], name: str = 'EasyModels', interface=None, + unique_name: Optional[str] = None, populate_if_none: bool = True, **kwargs, ): @@ -32,7 +33,7 @@ def __init__( # Else collisions might occur in global_object.map self.populate_if_none = False - super().__init__(name, interface, *models, **kwargs) + super().__init__(name, interface, unique_name=unique_name, *models, **kwargs) def add_model(self, model: Optional[Model] = None): """Add a model to the collection. diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 8b80988d..7a4c5920 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -10,35 +10,29 @@ from easyscience import global_object from easyscience.fitting import AvailableMinimizers +from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.data import DataSet1D from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection +from easyreflectometry.model import PercentageFhwm from easyreflectometry.sample import Layer from easyreflectometry.sample import MaterialCollection from easyreflectometry.sample import Multilayer from easyreflectometry.sample import Sample from easyreflectometry.sample.collections.base_collection import BaseCollection -MODELS_SAMPLE_DATA = [ - DataSet1D( - name='Sample Data 0', - x=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - y=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - ) -] -MODELS_MODEL_DATA = [ - DataSet1D( - name='Model Data 0', - x=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - y=np.array([1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5]), - ) -] +Q_MIN = 0.001 +Q_MAX = 0.3 +Q_ELEMENTS = 500 + +DEFAULT_MINIZER = AvailableMinimizers.LMFit_leastsq + EXPERIMENTAL_DATA = [ DataSet1D( name='Example Data 0', - x=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - y=np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5]), - ye=np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]), + x=np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS), + y=3 * np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS), + ye=0.1 * np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS), ) ] @@ -49,8 +43,8 @@ def __init__(self): self._path_project_parent = Path(os.path.expanduser('~')) self._models = ModelCollection(populate_if_none=False, unique_name='project_models') self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') - self._calculator = None - self._minimizer: AvailableMinimizers = None + self._calculator = CalculatorFactory() + self._minimizer = DEFAULT_MINIZER self._experiments: List[DataSet1D] = None self._colors = None self._report = None @@ -69,8 +63,8 @@ def reset(self): self._info = self._default_info() self._path_project_parent = Path(os.path.expanduser('~')) - self._calculator = None - self._minimizer = None + self._calculator = CalculatorFactory() + self._minimizer = DEFAULT_MINIZER self._experiments = None self._colors = None self._report = None @@ -119,11 +113,33 @@ def experiments(self, experiments: List[DataSet1D]) -> None: def path_json(self): return self.path / 'project.json' - def sample_data_for_model_at_index(self, index: int = 0) -> DataSet1D: - return MODELS_SAMPLE_DATA[index] + def sld_data_for_model_at_index(self, index: int = 0) -> DataSet1D: + self.models[index].interface = self._calculator + sld = self.models[index].interface().sld_profile(self._models[index].unique_name) + return DataSet1D( + name=f'SLD for Model {index}', + x=sld[0], + y=sld[1], + ) - def model_data_for_model_at_index(self, index: int = 0) -> DataSet1D: - return MODELS_MODEL_DATA[index] + def sample_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.array] = None) -> DataSet1D: + original_resolution_function = self.models[index].resolution_function + self.models[index].resolution_function = PercentageFhwm(0) + reflectivity_data = self.model_data_for_model_at_index(index, q_range) + self.models[index].resolution_function = original_resolution_function + + return reflectivity_data + + def model_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.array] = None) -> DataSet1D: + if q_range is None: + q_range = np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS) + self.models[index].interface = self._calculator + reflectivity = self.models[index].interface().reflectity_profile(q_range, self._models[index].unique_name) + return DataSet1D( + name=f'Reflectivity for Model {index}', + x=q_range, + y=reflectivity, + ) def experimental_data_for_model_at_index(self, index: int = 0) -> DataSet1D: return EXPERIMENTAL_DATA[index] @@ -132,20 +148,21 @@ def default_model(self): self._replace_collection(MaterialCollection(), self._materials) layers = [ - Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer'), - Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='Multi-layer'), - Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer'), + Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer', interface=self._calculator), + Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='Multi-layer', interface=self._calculator), + Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer', interface=self._calculator), ] - items = [ - Multilayer(layers[0], name='Superphase'), - Multilayer(layers[1], name='Multi-layer'), - Multilayer(layers[2], name='Subphase'), + assemblies = [ + Multilayer(layers[0], name='Superphase', interface=self._calculator), + Multilayer(layers[1], name='Multi-layer', interface=self._calculator), + Multilayer(layers[2], name='Subphase', interface=self._calculator), ] - sample = Sample(*items) + sample = Sample(*assemblies, interface=self._calculator) sample[0].layers[0].thickness.enabled = False sample[0].layers[0].roughness.enabled = False sample[-1].layers[-1].thickness.enabled = False - self._replace_collection([Model(sample=sample)], self._models) + model = Model(sample=sample, interface=self._calculator) + self._replace_collection([model], self._models) def add_material(self, material: MaterialCollection) -> None: if material in self._materials: @@ -216,7 +233,7 @@ def as_dict(self, include_materials_not_in_model=False): if self._minimizer is not None: project_dict['minimizer'] = self._minimizer.name if self._calculator is not None: - project_dict['calculator'] = [self._calculator.current_interface_name] + project_dict['calculator'] = self._calculator.current_interface_name if self._colors is not None: project_dict['colors'] = self._colors return project_dict @@ -263,9 +280,7 @@ def from_dict(self, project_dict: dict): else: self._experiments = None if 'calculator' in keys: - self._calculator = project_dict['calculator'] - else: - self._calculator = None + self._calculator.switch(project_dict['calculator']) def _from_dict_extract_experiments(self, project_dict: dict): self._experiments: List[DataSet1D] = [] diff --git a/src/easyreflectometry/sample/assemblies/surfactant_layer.py b/src/easyreflectometry/sample/assemblies/surfactant_layer.py index 6f892a69..b104d1e4 100644 --- a/src/easyreflectometry/sample/assemblies/surfactant_layer.py +++ b/src/easyreflectometry/sample/assemblies/surfactant_layer.py @@ -45,6 +45,7 @@ def __init__( :param conformal_roughness: Constrain the roughness to be the same for both layers, defaults to `False`. :param interface: Calculator interface, defaults to `None`. """ + # We need to generate a unique name to create the nested objects if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) diff --git a/src/easyreflectometry/sample/collections/base_collection.py b/src/easyreflectometry/sample/collections/base_collection.py index 851d1070..6bd238c7 100644 --- a/src/easyreflectometry/sample/collections/base_collection.py +++ b/src/easyreflectometry/sample/collections/base_collection.py @@ -1,6 +1,7 @@ from typing import List from typing import Optional +from easyscience import global_object from easyscience.Objects.Groups import BaseCollection as EasyBaseCollection from easyreflectometry.parameter_utils import yaml_dump @@ -15,6 +16,9 @@ def __init__( unique_name: Optional[str] = None, **kwargs, ): + if unique_name is None: + unique_name = global_object.generate_unique_name(self.__class__.__name__) + super().__init__(name, unique_name=unique_name, *args, **kwargs) self.interface = interface diff --git a/tests/calculators/refl1d/test_refl1d_calculator.py b/tests/calculators/refl1d/test_refl1d_calculator.py index ba6e39a8..78e582f1 100644 --- a/tests/calculators/refl1d/test_refl1d_calculator.py +++ b/tests/calculators/refl1d/test_refl1d_calculator.py @@ -27,7 +27,7 @@ def test_init(self): assert_equal(p._model_link['background'], 'bkg') assert_equal(p.name, 'refl1d') - def test_fit_func(self): + def test_reflectity_profile(self): p = Refl1d() p._wrapper.create_material('Material1') p._wrapper.update_material('Material1', rho=0.000, irho=0.000) @@ -63,7 +63,7 @@ def test_fit_func(self): 1.2687053e-07, 1.0188127e-07, ] - assert_almost_equal(p.fit_func(q, 'MyModel'), expected) + assert_almost_equal(p.reflectity_profile(q, 'MyModel'), expected) def test_calculate2(self): p = Refl1d() @@ -107,7 +107,7 @@ def test_calculate2(self): 3.5132221e-07, 2.5347996e-07, ] - assert_almost_equal(p.fit_func(q, 'MyModel'), expected) + assert_almost_equal(p.reflectity_profile(q, 'MyModel'), expected) def test_calculate_magnetic(self): p = Refl1d() @@ -151,7 +151,7 @@ def test_calculate_magnetic(self): 1.30026616e-07, 1.05139655e-07, ] - assert_almost_equal(p.fit_func(q, 'MyModel'), expected) + assert_almost_equal(p.reflectity_profile(q, 'MyModel'), expected) def test_sld_profile(self): p = Refl1d() diff --git a/tests/calculators/refnx/test_refnx_calculator.py b/tests/calculators/refnx/test_refnx_calculator.py index 782df2c0..27283c10 100644 --- a/tests/calculators/refnx/test_refnx_calculator.py +++ b/tests/calculators/refnx/test_refnx_calculator.py @@ -27,7 +27,7 @@ def test_init(self): assert_equal(p._model_link['background'], 'bkg') assert_equal(p.name, 'refnx') - def test_fit_func(self): + def test_reflectity_profile(self): p = Refnx() p._wrapper.create_material('Material1') p._wrapper.update_material('Material1', real=0.000, imag=0.000) @@ -62,7 +62,7 @@ def test_fit_func(self): 1.26726993e-07, 1.01842852e-07, ] - assert_almost_equal(p.fit_func(q, 'MyModel'), expected) + assert_almost_equal(p.reflectity_profile(q, 'MyModel'), expected) def test_calculate2(self): p = Refnx() @@ -105,7 +105,7 @@ def test_calculate2(self): 3.4981523e-07, 2.5424356e-07, ] - assert_almost_equal(p.fit_func(q, 'MyModel'), expected) + assert_almost_equal(p.reflectity_profile(q, 'MyModel'), expected) def test_sld_profile(self): p = Refnx() diff --git a/tests/model/test_model.py b/tests/model/test_model.py index a86864d9..dd51338a 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -427,6 +427,6 @@ def test_dict_round_trip(interface): if interface is not None: assert model.interface().name == model_from_dict.interface().name assert_almost_equal( - model.interface().fit_func([0.3], model.unique_name), - model_from_dict.interface().fit_func([0.3], model_from_dict.unique_name), + model.interface().reflectity_profile([0.3], model.unique_name), + model_from_dict.interface().reflectity_profile([0.3], model_from_dict.unique_name), ) diff --git a/tests/test_project.py b/tests/test_project.py index 25691682..53032877 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2,8 +2,10 @@ import os from pathlib import Path +import numpy as np from easyscience import global_object from easyscience.fitting import AvailableMinimizers +from numpy.testing import assert_allclose from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection @@ -28,8 +30,8 @@ def test_constructor(self): assert project._path_project_parent == Path(os.path.expanduser('~')) assert len(project._materials) == 0 assert len(project._models) == 0 - assert project._calculator is None - assert project._minimizer is None + assert project._calculator.current_interface_name == 'refnx' + assert project._minimizer == AvailableMinimizers.LMFit_leastsq assert project._experiments is None assert project._report is None assert project._created is False @@ -66,8 +68,8 @@ def test_reset(self): assert len(project._materials) == 0 assert project._path_project_parent == Path(os.path.expanduser('~')) - assert project._calculator is None - assert project._minimizer is None + assert project._calculator.current_interface_name == 'refnx' + assert project._minimizer == AvailableMinimizers.LMFit_leastsq assert project._experiments is None assert project._report is None assert project._created is False @@ -109,6 +111,51 @@ def test_default_model(self): assert len(project._models.data[0].sample) == 3 assert len(project._materials) == 3 + def test_sld_data_for_model_at_index(self): + # When + project = Project() + project.default_model() + + # Then + sample_data = project.sld_data_for_model_at_index(0) + + # Expect + assert len(sample_data.x) == 500 + assert_allclose( + np.array([4.6119497e-08, 6.3189932e00, 6.3350000e00, 2.0740000e00]), + np.array([sample_data.y[0], sample_data.y[100], sample_data.y[300], sample_data.y[499]]), + ) + + def test_sample_data_for_model_at_index(self): + # When + project = Project() + project.default_model() + + # Then + sample_data = project.sample_data_for_model_at_index(0, np.array([0.01, 0.05, 0.1, 0.5])) + + # Expect + assert len(sample_data.y) == 4 + assert_allclose( + np.array([1.00000001e00, 1.74684509e-03, 1.66360864e-04, 1.73359103e-08]), + sample_data.y, + ) + + def test_model_data_for_model_at_index(self): + # When + project = Project() + project.default_model() + + # Then + model_data = project.model_data_for_model_at_index(0, np.array([0.01, 0.05, 0.1, 0.5])) + + # Expect + assert len(model_data.y) == 4 + assert_allclose( + np.array([0.9738701849233727, 0.0017678986451491123, 0.00016581714423990004, 3.3290653551465554e-08]), + model_data.y, + ) + def test_minimizer(self): # When project = Project() @@ -201,7 +248,9 @@ def test_as_dict(self): keys = list(project_dict.keys()) keys.sort() assert keys == [ + 'calculator', 'info', + 'minimizer', 'models', 'with_experiments', ] @@ -212,6 +261,8 @@ def test_as_dict(self): 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } + assert project_dict['calculator'] == 'refnx' + assert project_dict['minimizer'] == 'LMFit_leastsq' assert project_dict['models']['data'] == [] assert project_dict['with_experiments'] is False diff --git a/tests/test_topmost_nesting.py b/tests/test_topmost_nesting.py index a270b72d..52b2d31b 100644 --- a/tests/test_topmost_nesting.py +++ b/tests/test_topmost_nesting.py @@ -44,8 +44,8 @@ def test_copy(): assert model._resolution_function.smearing(5.5) == model_copy._resolution_function.smearing(5.5) assert model.interface().name == model_copy.interface().name assert_almost_equal( - model.interface().fit_func([0.3], model.unique_name), - model_copy.interface().fit_func([0.3], model_copy.unique_name), + model.interface().reflectity_profile([0.3], model.unique_name), + model_copy.interface().reflectity_profile([0.3], model_copy.unique_name), ) assert model.unique_name != model_copy.unique_name assert model.name == model_copy.name From e95df0fa16b2ad1e368fb8b012f7054514e77a89 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:39:01 +0200 Subject: [PATCH 09/11] 197 add experiment data to project in order for this to be exposed to the app (#198) * experiments and q values * pass all four elements in datapoint * adjust tests and set resolution function when loading measured data * from steps to resolution --- src/easyreflectometry/data/data_store.py | 8 ++- src/easyreflectometry/project.py | 76 +++++++++++++++++++----- tests/data/test_data_store.py | 2 +- tests/test_project.py | 68 ++++++++++++++++++++- 4 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/easyreflectometry/data/data_store.py b/src/easyreflectometry/data/data_store.py index aec66e57..eed59e2b 100644 --- a/src/easyreflectometry/data/data_store.py +++ b/src/easyreflectometry/data/data_store.py @@ -101,6 +101,10 @@ def __init__( x = np.array(x) if not isinstance(y, np.ndarray): y = np.array(y) + if not isinstance(ye, np.ndarray): + ye = np.array(ye) + if not isinstance(xe, np.ndarray): + xe = np.array(xe) self.x = x self.y = y @@ -129,8 +133,8 @@ def is_experiment(self) -> bool: def is_simulation(self) -> bool: return self._model is None - def data_points(self) -> int: - return zip(self.x, self.y) + def data_points(self) -> tuple[float, float, float, float]: + return zip(self.x, self.y, self.ye, self.xe) def __repr__(self) -> str: return "1D DataStore of '{:s}' Vs '{:s}' with {} data points".format(self.x_label, self.y_label, len(self.x)) diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 7a4c5920..54d1e58f 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -2,6 +2,7 @@ import json import os from pathlib import Path +from typing import Dict from typing import List from typing import Optional from typing import Union @@ -9,9 +10,12 @@ import numpy as np from easyscience import global_object from easyscience.fitting import AvailableMinimizers +from scipp import DataGroup from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.data import DataSet1D +from easyreflectometry.data import load +from easyreflectometry.model import LinearSpline from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection from easyreflectometry.model import PercentageFhwm @@ -23,19 +27,10 @@ Q_MIN = 0.001 Q_MAX = 0.3 -Q_ELEMENTS = 500 +Q_RESOLUTION = 500 DEFAULT_MINIZER = AvailableMinimizers.LMFit_leastsq -EXPERIMENTAL_DATA = [ - DataSet1D( - name='Example Data 0', - x=np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS), - y=3 * np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS), - ye=0.1 * np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS), - ) -] - class Project: def __init__(self): @@ -45,9 +40,12 @@ def __init__(self): self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') self._calculator = CalculatorFactory() self._minimizer = DEFAULT_MINIZER - self._experiments: List[DataSet1D] = None + self._experiments: Dict[DataGroup] = {} self._colors = None self._report = None + self._q_min = None + self._q_max = None + self._q_resolution = None # Project flags self._created = False @@ -65,7 +63,7 @@ def reset(self): self._path_project_parent = Path(os.path.expanduser('~')) self._calculator = CalculatorFactory() self._minimizer = DEFAULT_MINIZER - self._experiments = None + self._experiments = {} self._colors = None self._report = None @@ -73,6 +71,36 @@ def reset(self): self._created = False self._with_experiments = False + @property + def q_min(self): + if self._q_min is None: + return Q_MIN + return self._q_min + + @q_min.setter + def q_min(self, value: float) -> None: + self._q_min = value + + @property + def q_max(self): + if self._q_max is None: + return Q_MAX + return self._q_max + + @q_max.setter + def q_max(self, value: float) -> None: + self._q_max = value + + @property + def q_resolution(self): + if self._q_resolution is None: + return Q_RESOLUTION + return self._q_resolution + + @q_resolution.setter + def q_resolution(self, value: int) -> None: + self._q_resolution = value + @property def created(self) -> bool: return self._created @@ -113,6 +141,16 @@ def experiments(self, experiments: List[DataSet1D]) -> None: def path_json(self): return self.path / 'project.json' + def load_experiment_for_model_at_index(self, path: Union[Path, str], index: Optional[int] = 0) -> None: + self._experiments[index] = load(str(path)) + # Set the resolution function if variance data is present + if sum(self._experiments[index]['coords']['Qz_0'].variances) != 0: + resolution_function = LinearSpline( + q_data_points=self._experiments[index]['coords']['Qz_0'].values, + fwhm_values=np.sqrt(self._experiments[index]['coords']['Qz_0'].variances), + ) + self._models[index].resolution_function = resolution_function + def sld_data_for_model_at_index(self, index: int = 0) -> DataSet1D: self.models[index].interface = self._calculator sld = self.models[index].interface().sld_profile(self._models[index].unique_name) @@ -132,7 +170,7 @@ def sample_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.ar def model_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.array] = None) -> DataSet1D: if q_range is None: - q_range = np.linspace(Q_MIN, Q_MAX, Q_ELEMENTS) + q_range = np.linspace(self.q_min, self.q_max, self.q_resolution) self.models[index].interface = self._calculator reflectivity = self.models[index].interface().reflectity_profile(q_range, self._models[index].unique_name) return DataSet1D( @@ -142,7 +180,17 @@ def model_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.arr ) def experimental_data_for_model_at_index(self, index: int = 0) -> DataSet1D: - return EXPERIMENTAL_DATA[index] + if index in self._experiments.keys(): + return DataSet1D( + name=f'Experiment for Model {index}', + x=self._experiments[index]['coords']['Qz_0'].values, + y=self._experiments[index]['data']['R_0'].values, + ye=self._experiments[index]['data']['R_0'].variances, + xe=self._experiments[index]['coords']['Qz_0'].variances, + model=self.models[index], + ) + else: + raise IndexError(f'No experiment data for model at index {index}') def default_model(self): self._replace_collection(MaterialCollection(), self._materials) diff --git a/tests/data/test_data_store.py b/tests/data/test_data_store.py index 78d080da..6c84b808 100644 --- a/tests/data/test_data_store.py +++ b/tests/data/test_data_store.py @@ -41,4 +41,4 @@ def test_data_points(self): points = data.data_points() # Expect - assert list(points) == [(1, 4), (2, 5), (3, 6)] + assert list(points) == [(1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)] diff --git a/tests/test_project.py b/tests/test_project.py index 53032877..93a8ebbf 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -6,13 +6,20 @@ from easyscience import global_object from easyscience.fitting import AvailableMinimizers from numpy.testing import assert_allclose +from scipp import DataGroup +import easyreflectometry +from easyreflectometry.data import DataSet1D +from easyreflectometry.model import LinearSpline from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection +from easyreflectometry.model import PercentageFhwm from easyreflectometry.project import Project from easyreflectometry.sample import Material from easyreflectometry.sample import MaterialCollection +PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests', '_static') + class TestProject: def test_constructor(self): @@ -32,7 +39,7 @@ def test_constructor(self): assert len(project._models) == 0 assert project._calculator.current_interface_name == 'refnx' assert project._minimizer == AvailableMinimizers.LMFit_leastsq - assert project._experiments is None + assert project._experiments == {} assert project._report is None assert project._created is False assert project._with_experiments is False @@ -70,7 +77,7 @@ def test_reset(self): assert project._path_project_parent == Path(os.path.expanduser('~')) assert project._calculator.current_interface_name == 'refnx' assert project._minimizer == AvailableMinimizers.LMFit_leastsq - assert project._experiments is None + assert project._experiments == {} assert project._report is None assert project._created is False assert project._with_experiments is False @@ -465,3 +472,60 @@ def test_create(self, tmp_path): 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } + + def test_load_experiment(self): + # When + project = Project() + project.models = ModelCollection(Model(), Model(), Model(), Model(), Model(), Model()) + fpath = os.path.join(PATH_STATIC, 'example.ort') + + # Then + project.load_experiment_for_model_at_index(fpath, 5) + + # Expect + assert list(project.experiments.keys()) == [5] + assert isinstance(project.experiments[5], DataGroup) + assert isinstance(project.models[5].resolution_function, LinearSpline) + assert isinstance(project.models[4].resolution_function, PercentageFhwm) + + def test_experimental_data_at_index(self): + # When + project = Project() + project.models = ModelCollection(Model()) + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath) + + # Then + data = project.experimental_data_for_model_at_index() + + # Expect + assert data.name == 'Experiment for Model 0' + assert data.is_experiment + assert isinstance(data, DataSet1D) + assert len(data.x) == 408 + assert len(data.xe) == 408 + assert len(data.y) == 408 + assert len(data.ye) == 408 + + def test_q(self): + # When + project = Project() + + # Then + q = project.q_min, project.q_max, project.q_resolution + + # Expect + assert q == (0.001, 0.3, 500) + + def test_set_q(self): + # When + project = Project() + + # Then + project.q_min = 1 + project.q_max = 2 + project.q_resolution = 3 + + # Expect + q = project.q_min, project.q_max, project.q_resolution + assert q == (1, 2, 3) From e64c4b4ec657bdfa1411c9826def4176eec7178b Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:24:58 +0100 Subject: [PATCH 10/11] 199 make fitter a part of project (#200) * minor functionality and code cleaning * expose parameters * added method to fit data in DataSet1D formar * syntax fit * rename method * ruff * sample index in project * est all indicies * indicies setters updated * bug indicies * material index * layer index * index to zero rather than none * only parameters in project * load and save project with experimental data * only be recursive on dicts * enabling load and save * load fix when no experiments * default info updated * name for add model * renameelement added to collections * added most used materials to project * add material in get index of given material * minor adjustments to model and sample * latest version of easyscience * update adding element to collection * ruff * fitter as property * added tests --- pyproject.toml | 2 +- src/easyreflectometry/data/__init__.py | 2 + src/easyreflectometry/data/measurement.py | 13 + src/easyreflectometry/fitting.py | 23 +- src/easyreflectometry/model/model.py | 4 +- .../model/model_collection.py | 2 +- .../model/resolution_functions.py | 2 +- src/easyreflectometry/project.py | 261 ++++++++++++------ .../sample/assemblies/repeating_multilayer.py | 2 +- src/easyreflectometry/sample/base_core.py | 2 +- .../sample/collections/base_collection.py | 2 +- .../sample/collections/layer_collection.py | 2 +- .../sample/collections/material_collection.py | 2 +- .../sample/collections/sample.py | 3 +- .../sample/elements/layers/layer.py | 2 +- .../layers/layer_area_per_molecule.py | 2 +- .../sample/elements/materials/material.py | 2 +- .../elements/materials/material_density.py | 2 +- .../elements/materials/material_mixture.py | 2 +- .../elements/materials/material_solvated.py | 2 +- .../{parameter_utils.py => utils.py} | 19 ++ tests/sample/collections/test_sample.py | 2 +- tests/sample/elements/layers/test_layer.py | 2 +- .../elements/materials/test_material.py | 2 +- tests/test_fitting.py | 6 +- tests/test_parameter_utils.py | 2 +- tests/test_project.py | 179 +++++++++--- 27 files changed, 398 insertions(+), 148 deletions(-) rename src/easyreflectometry/{parameter_utils.py => utils.py} (74%) diff --git a/pyproject.toml b/pyproject.toml index e8de39c5..d9659381 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] requires-python = ">=3.9,<3.13" dependencies = [ - 'easyscience @ git+https://github.com/EasyScience/EasyScience.git@develop', + "easyscience>=1.2.0", "scipp>=23.12.0", "refnx>=0.1.15", "refl1d>=0.8.14", diff --git a/src/easyreflectometry/data/__init__.py b/src/easyreflectometry/data/__init__.py index fdecf1dd..18294db3 100644 --- a/src/easyreflectometry/data/__init__.py +++ b/src/easyreflectometry/data/__init__.py @@ -1,9 +1,11 @@ from .data_store import DataSet1D from .data_store import ProjectData from .measurement import load +from .measurement import load_as_dataset __all__ = [ load, + load_as_dataset, ProjectData, DataSet1D, ] diff --git a/src/easyreflectometry/data/measurement.py b/src/easyreflectometry/data/measurement.py index ff502af7..3664af4d 100644 --- a/src/easyreflectometry/data/measurement.py +++ b/src/easyreflectometry/data/measurement.py @@ -8,6 +8,19 @@ from orsopy.fileio import Header from orsopy.fileio import orso +from easyreflectometry.data import DataSet1D + + +def load_as_dataset(fname: Union[TextIO, str]) -> DataSet1D: + """Load data from an ORSO .ort file as a DataSet1D.""" + data_group = load(fname) + return DataSet1D( + x=data_group['coords']['Qz_0'].values, + y=data_group['data']['R_0'].values, + ye=data_group['data']['R_0'].variances, + xe=data_group['coords']['Qz_0'].variances, + ) + def load(fname: Union[TextIO, str]) -> sc.DataGroup: """Load data from an ORSO .ort file. diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index 89e4eb64..d0cc3f46 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -3,12 +3,14 @@ import numpy as np import scipp as sc from easyscience.fitting import AvailableMinimizers -from easyscience.fitting.multi_fitter import MultiFitter as easyFitter +from easyscience.fitting import FitResults +from easyscience.fitting.multi_fitter import MultiFitter as EasyScienceMultiFitter +from easyreflectometry.data import DataSet1D from easyreflectometry.model import Model -class Fitter: +class MultiFitter: def __init__(self, *args: Model): r"""A convinence class for the :py:class:`easyscience.Fitting.Fitting` which will populate the :py:class:`sc.DataGroup` appropriately @@ -26,7 +28,7 @@ def wrapped(*args, **kwargs): self._fit_func = [func_wrapper(m.interface.fit_func, m.unique_name) for m in args] self._models = args - self.easy_f = easyFitter(args, self._fit_func) + self.easy_science_multi_fitter = EasyScienceMultiFitter(args, self._fit_func) def fit(self, data: sc.DataGroup, id: int = 0) -> sc.DataGroup: """ @@ -39,14 +41,14 @@ def fit(self, data: sc.DataGroup, id: int = 0) -> sc.DataGroup: x = [data['coords'][f'Qz_{i}'].values for i in refl_nums] y = [data['data'][f'R_{i}'].values for i in refl_nums] dy = [1 / np.sqrt(data['data'][f'R_{i}'].variances) for i in refl_nums] - result = self.easy_f.fit(x, y, weights=dy) + result = self.easy_science_multi_fitter.fit(x, y, weights=dy) new_data = data.copy() for i, _ in enumerate(result): id = refl_nums[i] new_data[f'R_{id}_model'] = sc.array( dims=[f'Qz_{id}'], values=self._fit_func[i](data['coords'][f'Qz_{id}'].values) ) - sld_profile = self.easy_f._fit_objects[i].interface.sld_profile(self._models[i].unique_name) + sld_profile = self.easy_science_multi_fitter._fit_objects[i].interface.sld_profile(self._models[i].unique_name) new_data[f'SLD_{id}'] = sc.array(dims=[f'z_{id}'], values=sld_profile[1] * 1e-6, unit=sc.Unit('1/angstrom') ** 2) new_data['attrs'][f'R_{id}_model'] = {'model': sc.scalar(self._models[i].as_dict())} new_data['coords'][f'z_{id}'] = sc.array( @@ -54,13 +56,22 @@ def fit(self, data: sc.DataGroup, id: int = 0) -> sc.DataGroup: ) return new_data + def fit_single_data_set_1d(self, data: DataSet1D) -> FitResults: + """ + Perform the fitting and populate the DataGroups with the result. + + :param data: DataGroup to be fitted to and populated + :param method: Optimisation method + """ + return self.easy_science_multi_fitter.fit(x=[data.x], y=[data.y], weights=[data.ye])[0] + def switch_minimizer(self, minimizer: AvailableMinimizers) -> None: """ Switch the minimizer for the fitting. :param minimizer: Minimizer to be switched to """ - self.easy_f.switch_minimizer(minimizer) + self.easy_science_multi_fitter.switch_minimizer(minimizer) def _flatten_list(this_list: list) -> list: diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index d4c4c9e4..e16a369d 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -12,10 +12,10 @@ from easyscience.Objects.new_variable import Parameter from easyscience.Objects.ObjectClasses import BaseObj -from easyreflectometry.parameter_utils import get_as_parameter -from easyreflectometry.parameter_utils import yaml_dump from easyreflectometry.sample import BaseAssembly from easyreflectometry.sample import Sample +from easyreflectometry.utils import get_as_parameter +from easyreflectometry.utils import yaml_dump from .resolution_functions import PercentageFhwm from .resolution_functions import ResolutionFunction diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index 3ca9ee76..7ae3b9f2 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -41,7 +41,7 @@ def add_model(self, model: Optional[Model] = None): :param model: Model to add. """ if model is None: - model = Model(name='Model new', interface=self.interface) + model = Model(name='EasyModel added', interface=self.interface) self.append(model) def duplicate_model(self, index: int): diff --git a/src/easyreflectometry/model/resolution_functions.py b/src/easyreflectometry/model/resolution_functions.py index da276078..379ef207 100644 --- a/src/easyreflectometry/model/resolution_functions.py +++ b/src/easyreflectometry/model/resolution_functions.py @@ -59,4 +59,4 @@ def smearing(self, q: Union[np.array, float]) -> np.array: def as_dict( self, skip: Optional[List[str]] = None ) -> dict[str, str]: # skip is kept for consistency of the as_dict signature - return {'smearing': 'LinearSpline', 'q_data_points': self.q_data_points, 'fwhm_values': self.fwhm_values} + return {'smearing': 'LinearSpline', 'q_data_points': list(self.q_data_points), 'fwhm_values': list(self.fwhm_values)} diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 54d1e58f..5eed8269 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -10,20 +10,25 @@ import numpy as np from easyscience import global_object from easyscience.fitting import AvailableMinimizers +from easyscience.fitting.fitter import DEFAULT_MINIMIZER +from easyscience.Objects.new_variable import Parameter from scipp import DataGroup from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.data import DataSet1D -from easyreflectometry.data import load +from easyreflectometry.data import load_as_dataset +from easyreflectometry.fitting import MultiFitter from easyreflectometry.model import LinearSpline from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection from easyreflectometry.model import PercentageFhwm from easyreflectometry.sample import Layer +from easyreflectometry.sample import Material from easyreflectometry.sample import MaterialCollection from easyreflectometry.sample import Multilayer from easyreflectometry.sample import Sample from easyreflectometry.sample.collections.base_collection import BaseCollection +from easyreflectometry.utils import collect_unique_names_from_dict Q_MIN = 0.001 Q_MAX = 0.3 @@ -39,13 +44,18 @@ def __init__(self): self._models = ModelCollection(populate_if_none=False, unique_name='project_models') self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') self._calculator = CalculatorFactory() - self._minimizer = DEFAULT_MINIZER self._experiments: Dict[DataGroup] = {} - self._colors = None + self._fitter: MultiFitter = None + self._colors: list[str] = None self._report = None - self._q_min = None - self._q_max = None - self._q_resolution = None + self._q_min: float = None + self._q_max: float = None + self._q_resolution: int = None + self._current_material_index = 0 + self._current_model_index = 0 + self._current_assembly_index = 0 + self._current_layer_index = 0 + self._fitter_model_index = None # Project flags self._created = False @@ -56,20 +66,17 @@ def reset(self): del self._materials global_object.map._clear() - self._models = ModelCollection(populate_if_none=False, unique_name='project_models') - self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials') + self.__init__() - self._info = self._default_info() - self._path_project_parent = Path(os.path.expanduser('~')) - self._calculator = CalculatorFactory() - self._minimizer = DEFAULT_MINIZER - self._experiments = {} - self._colors = None - self._report = None - - # Project flags - self._created = False - self._with_experiments = False + @property + def parameters(self) -> List[Parameter]: + unique_names_in_project = collect_unique_names_from_dict(self.as_dict()) + parameters = [] + for vertice_str in global_object.map.vertices(): + vertice_obj = global_object.map.get_item_by_key(vertice_str) + if isinstance(vertice_obj, Parameter) and vertice_str in unique_names_in_project: + parameters.append(vertice_obj) + return parameters @property def q_min(self): @@ -101,6 +108,53 @@ def q_resolution(self): def q_resolution(self, value: int) -> None: self._q_resolution = value + @property + def current_material_index(self) -> Optional[int]: + return self._current_material_index + + @current_material_index.setter + def current_material_index(self, value: int) -> None: + if value < 0 or value >= len(self._materials): + raise ValueError(f'Index {value} out of range') + if self._current_material_index != value: + self._current_material_index = value + + @property + def current_model_index(self) -> Optional[int]: + return self._current_model_index + + @current_model_index.setter + def current_model_index(self, value: int) -> None: + if value < 0 or value >= len(self._models): + raise ValueError(f'Index {value} out of range') + if self._current_model_index != value: + self._current_model_index = value + self._current_assembly_index = 0 + self._current_layer_index = 0 + + @property + def current_assembly_index(self) -> Optional[int]: + return self._current_assembly_index + + @current_assembly_index.setter + def current_assembly_index(self, value: int) -> None: + if value < 0 or value >= len(self._models[self._current_model_index].sample): + raise ValueError(f'Index {value} out of range') + if self._current_assembly_index != value: + self._current_assembly_index = value + self._current_layer_index = 0 + + @property + def current_layer_index(self) -> Optional[int]: + return self._current_layer_index + + @current_layer_index.setter + def current_layer_index(self, value: int) -> None: + if value < 0 or value >= len(self._models[self._current_model_index].sample[self._current_assembly_index].layers): + raise ValueError(f'Index {value} out of range') + if self._current_layer_index != value: + self._current_layer_index = value + @property def created(self) -> bool: return self._created @@ -119,35 +173,85 @@ def models(self) -> ModelCollection: @models.setter def models(self, models: ModelCollection) -> None: self._replace_collection(models, self._models) + # Use setter to update indicies for current model, assembly and layer + self.current_model_index = 0 self._materials.extend(self._get_materials_in_models()) + for model in self._models: + model.interface = self._calculator + + @property + def fitter(self) -> MultiFitter: + if len(self._models): + if (self._fitter is None) or (self._fitter_model_index != self._current_model_index): + minimizer = self.minimizer + self._fitter = MultiFitter(self._models[self._current_model_index]) + self.minimizer = minimizer + self._fitter_model_index = self._current_model_index + return self._fitter + + @property + def calculator(self) -> str: + return self._calculator.current_interface_name + + @calculator.setter + def calculator(self, calculator: str) -> None: + self._calculator.switch(calculator) @property def minimizer(self) -> AvailableMinimizers: - return self._minimizer + if self._fitter is not None: + return self._fitter.easy_science_multi_fitter.minimizer.enum + return DEFAULT_MINIMIZER @minimizer.setter def minimizer(self, minimizer: AvailableMinimizers) -> None: - self._minimizer = minimizer + if self._fitter is not None: + self._fitter.easy_science_multi_fitter.switch_minimizer(minimizer) @property - def experiments(self) -> List[DataSet1D]: + def experiments(self) -> Dict[int, DataSet1D]: return self._experiments @experiments.setter - def experiments(self, experiments: List[DataSet1D]) -> None: + def experiments(self, experiments: Dict[int, DataSet1D]) -> None: self._experiments = experiments @property def path_json(self): return self.path / 'project.json' + def get_index_air(self) -> int: + if 'Air' not in [material.name for material in self._materials]: + self._materials.add_material(Material(name='Air', sld=0.0, isld=0.0)) + return [material.name for material in self._materials].index('Air') + + def get_index_si(self) -> int: + if 'Si' not in [material.name for material in self._materials]: + self._materials.add_material(Material(name='Si', sld=2.07, isld=0.0)) + return [material.name for material in self._materials].index('Si') + + def get_index_sio2(self) -> int: + if 'SiO2' not in [material.name for material in self._materials]: + self._materials.add_material(Material(name='SiO2', sld=3.47, isld=0.0)) + return [material.name for material in self._materials].index('SiO2') + + def get_index_d2o(self) -> int: + if 'D2O' not in [material.name for material in self._materials]: + self._materials.add_material(Material(name='D2O', sld=6.36, isld=0.0)) + return [material.name for material in self._materials].index('D2O') + def load_experiment_for_model_at_index(self, path: Union[Path, str], index: Optional[int] = 0) -> None: - self._experiments[index] = load(str(path)) + self._experiments[index] = load_as_dataset(str(path)) + self._experiments[index].name = f'Experiment for Model {index}' + self._experiments[index].model = self.models[index] + + self._with_experiments = True + # Set the resolution function if variance data is present - if sum(self._experiments[index]['coords']['Qz_0'].variances) != 0: + if sum(self._experiments[index].ye) != 0: resolution_function = LinearSpline( - q_data_points=self._experiments[index]['coords']['Qz_0'].values, - fwhm_values=np.sqrt(self._experiments[index]['coords']['Qz_0'].variances), + q_data_points=self._experiments[index].y, + fwhm_values=np.sqrt(self._experiments[index].ye), ) self._models[index].resolution_function = resolution_function @@ -181,14 +285,7 @@ def model_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.arr def experimental_data_for_model_at_index(self, index: int = 0) -> DataSet1D: if index in self._experiments.keys(): - return DataSet1D( - name=f'Experiment for Model {index}', - x=self._experiments[index]['coords']['Qz_0'].values, - y=self._experiments[index]['data']['R_0'].values, - ye=self._experiments[index]['data']['R_0'].variances, - xe=self._experiments[index]['coords']['Qz_0'].variances, - model=self.models[index], - ) + return self._experiments[index] else: raise IndexError(f'No experiment data for model at index {index}') @@ -196,21 +293,18 @@ def default_model(self): self._replace_collection(MaterialCollection(), self._materials) layers = [ - Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer', interface=self._calculator), - Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='Multi-layer', interface=self._calculator), - Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer', interface=self._calculator), + Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer'), + Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='D2O Layer'), + Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer'), ] assemblies = [ - Multilayer(layers[0], name='Superphase', interface=self._calculator), - Multilayer(layers[1], name='Multi-layer', interface=self._calculator), - Multilayer(layers[2], name='Subphase', interface=self._calculator), + Multilayer(layers[0], name='Superphase'), + Multilayer(layers[1], name='D2O'), + Multilayer(layers[2], name='Subphase'), ] - sample = Sample(*assemblies, interface=self._calculator) - sample[0].layers[0].thickness.enabled = False - sample[0].layers[0].roughness.enabled = False - sample[-1].layers[-1].thickness.enabled = False - model = Model(sample=sample, interface=self._calculator) - self._replace_collection([model], self._models) + sample = Sample(*assemblies) + model = Model(sample=sample) + self.models = ModelCollection([model]) def add_material(self, material: MaterialCollection) -> None: if material in self._materials: @@ -226,8 +320,8 @@ def remove_material(self, index: int) -> None: def _default_info(self): return dict( - name='Example Project', - short_description='reflectometry, 1D', + name='ExampleProject', + short_description='Reflectometry, 1D', samples='None', experiments='None', modified=datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), @@ -274,12 +368,13 @@ def as_dict(self, include_materials_not_in_model=False): project_dict['with_experiments'] = self._with_experiments if self._models is not None: project_dict['models'] = self._models.as_dict(skip=['interface']) + project_dict['models']['unique_name'] = project_dict['models']['unique_name'] + '_to_prevent_collisions_on_load' if include_materials_not_in_model: self._as_dict_add_materials_not_in_model_dict(project_dict) if self._with_experiments: self._as_dict_add_experiments(project_dict) - if self._minimizer is not None: - project_dict['minimizer'] = self._minimizer.name + if self.fitter is not None: + project_dict['fitter_minimizer'] = self.fitter.easy_science_multi_fitter.minimizer.name if self._calculator is not None: project_dict['calculator'] = self._calculator.current_interface_name if self._colors is not None: @@ -295,55 +390,49 @@ def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict): project_dict['materials_not_in_model'] = MaterialCollection(materials_not_in_model).as_dict(skip=['interface']) def _as_dict_add_experiments(self, project_dict: dict): - project_dict['experiments'] = [] - project_dict['experiments_models'] = [] - project_dict['experiments_names'] = [] - for experiment in self._experiments: - if self._experiments[0].xe is not None: - project_dict['experiments'].append([experiment.x, experiment.y, experiment.ye, experiment.xe]) - else: - project_dict['experiments'].append([experiment.x, experiment.y, experiment.ye]) - project_dict['experiments_models'].append(experiment.model.name) - project_dict['experiments_names'].append(experiment.name) + project_dict['experiments'] = {} + project_dict['experiments_models'] = {} + project_dict['experiments_names'] = {} + + for key, experiment in self._experiments.items(): + project_dict['experiments'][key] = [list(experiment.x), list(experiment.y), list(experiment.ye)] + if experiment.xe is not None: + project_dict['experiments'][key].append(list(experiment.xe)) + project_dict['experiments_models'][key] = experiment.model.name + project_dict['experiments_names'][key] = experiment.name def from_dict(self, project_dict: dict): keys = list(project_dict.keys()) self._info = project_dict['info'] self._with_experiments = project_dict['with_experiments'] + if 'calculator' in keys: + self._calculator.switch(project_dict['calculator']) if 'models' in keys: - self._models = None - self._models = ModelCollection.from_dict(project_dict['models']) - + self.models = ModelCollection.from_dict(project_dict['models']) self._replace_collection(self._get_materials_in_models(), self._materials) - if 'materials_not_in_model' in keys: self._materials.extend(MaterialCollection.from_dict(project_dict['materials_not_in_model'])) - - if 'minimizer' in keys: - self._minimizer = AvailableMinimizers[project_dict['minimizer']] + if 'fitter_minimizer' in keys: + self.fitter.easy_science_multi_fitter.switch_minimizer(AvailableMinimizers[project_dict['fitter_minimizer']]) else: - self._minimizer = None + self._fitter = None if 'experiments' in keys: self._experiments = self._from_dict_extract_experiments(project_dict) else: - self._experiments = None - if 'calculator' in keys: - self._calculator.switch(project_dict['calculator']) - - def _from_dict_extract_experiments(self, project_dict: dict): - self._experiments: List[DataSet1D] = [] - - for i in range(len(project_dict['experiments'])): - self._experiments.append( - DataSet1D( - name=project_dict['experiments_names'][i], - x=project_dict['experiments'][i][0], - y=project_dict['experiments'][i][1], - ye=project_dict['experiments'][i][2], - xe=project_dict['experiments'][i][3], - model=self._models[project_dict['experiments_models'][i]], - ) + self._experiments = {} + + def _from_dict_extract_experiments(self, project_dict: dict) -> Dict[int, DataSet1D]: + experiments = {} + for key in project_dict['experiments'].keys(): + experiments[int(key)] = DataSet1D( + name=project_dict['experiments_names'][key], + x=project_dict['experiments'][key][0], + y=project_dict['experiments'][key][1], + ye=project_dict['experiments'][key][2], + xe=project_dict['experiments'][key][3], + model=self._models[project_dict['experiments_models'][key]], ) + return experiments def _get_materials_in_models(self) -> MaterialCollection: materials_in_model = MaterialCollection(populate_if_none=False) diff --git a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py index 800603f1..e904c34c 100644 --- a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py +++ b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py @@ -4,7 +4,7 @@ from easyscience import global_object from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter +from easyreflectometry.utils import get_as_parameter from ..collections.layer_collection import LayerCollection from ..elements.layers.layer import Layer diff --git a/src/easyreflectometry/sample/base_core.py b/src/easyreflectometry/sample/base_core.py index c17c3722..79965a31 100644 --- a/src/easyreflectometry/sample/base_core.py +++ b/src/easyreflectometry/sample/base_core.py @@ -2,7 +2,7 @@ from easyscience.Objects.ObjectClasses import BaseObj -from easyreflectometry.parameter_utils import yaml_dump +from easyreflectometry.utils import yaml_dump class BaseCore(BaseObj): diff --git a/src/easyreflectometry/sample/collections/base_collection.py b/src/easyreflectometry/sample/collections/base_collection.py index 6bd238c7..cb8e2152 100644 --- a/src/easyreflectometry/sample/collections/base_collection.py +++ b/src/easyreflectometry/sample/collections/base_collection.py @@ -4,7 +4,7 @@ from easyscience import global_object from easyscience.Objects.Groups import BaseCollection as EasyBaseCollection -from easyreflectometry.parameter_utils import yaml_dump +from easyreflectometry.utils import yaml_dump class BaseCollection(EasyBaseCollection): diff --git a/src/easyreflectometry/sample/collections/layer_collection.py b/src/easyreflectometry/sample/collections/layer_collection.py index d3080ab2..0761f861 100644 --- a/src/easyreflectometry/sample/collections/layer_collection.py +++ b/src/easyreflectometry/sample/collections/layer_collection.py @@ -28,7 +28,7 @@ def add_layer(self, layer: Optional[Layer] = None): """ if layer is None: layer = Layer( - name='New EasyLayer', + name='EasyLayer added', interface=self.interface, ) self.append(layer) diff --git a/src/easyreflectometry/sample/collections/material_collection.py b/src/easyreflectometry/sample/collections/material_collection.py index f4a42a10..a97f89d9 100644 --- a/src/easyreflectometry/sample/collections/material_collection.py +++ b/src/easyreflectometry/sample/collections/material_collection.py @@ -45,7 +45,7 @@ def add_material(self, material: Optional[Material] = None): :param material: Material to add. """ if material is None: - material = Material(sld=2.074, isld=0.000, name='Si new') + material = Material(sld=0.0, isld=0.0, name='Material added') material.interface = self.interface self.append(material) diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py index c763c240..12394474 100644 --- a/src/easyreflectometry/sample/collections/sample.py +++ b/src/easyreflectometry/sample/collections/sample.py @@ -49,6 +49,7 @@ def __init__( if not issubclass(type(assembly), BaseAssembly): raise ValueError('The elements must be an Assembly.') super().__init__(name, interface, unique_name=unique_name, *assemblies, **kwargs) + self._disable_changes_to_outermost_layers() def add_assembly(self, assembly: Optional[BaseAssembly] = None): """Add an assembly to the sample. @@ -57,7 +58,7 @@ def add_assembly(self, assembly: Optional[BaseAssembly] = None): """ if assembly is None: assembly = Multilayer( - name='New EasyMultilayer', + name='EasyMultilayer added', interface=self.interface, ) self._enable_changes_to_outermost_layers() diff --git a/src/easyreflectometry/sample/elements/layers/layer.py b/src/easyreflectometry/sample/elements/layers/layer.py index 08a75c54..00d6672b 100644 --- a/src/easyreflectometry/sample/elements/layers/layer.py +++ b/src/easyreflectometry/sample/elements/layers/layer.py @@ -6,7 +6,7 @@ from easyscience import global_object from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter +from easyreflectometry.utils import get_as_parameter from ...base_core import BaseCore from ..materials.material import Material diff --git a/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py b/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py index 326fbb9e..fea67833 100644 --- a/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py +++ b/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py @@ -6,9 +6,9 @@ from easyscience.Constraints import FunctionalConstraint from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter from easyreflectometry.special.calculations import area_per_molecule_to_scattering_length_density from easyreflectometry.special.calculations import neutron_scattering_length +from easyreflectometry.utils import get_as_parameter from ..materials.material import Material from ..materials.material_solvated import DEFAULTS as MATERIAL_SOLVATED_DEFAULTS diff --git a/src/easyreflectometry/sample/elements/materials/material.py b/src/easyreflectometry/sample/elements/materials/material.py index aadf2dfd..1973426c 100644 --- a/src/easyreflectometry/sample/elements/materials/material.py +++ b/src/easyreflectometry/sample/elements/materials/material.py @@ -7,7 +7,7 @@ from easyscience import global_object from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter +from easyreflectometry.utils import get_as_parameter from ...base_core import BaseCore diff --git a/src/easyreflectometry/sample/elements/materials/material_density.py b/src/easyreflectometry/sample/elements/materials/material_density.py index a584ed19..388f6e84 100644 --- a/src/easyreflectometry/sample/elements/materials/material_density.py +++ b/src/easyreflectometry/sample/elements/materials/material_density.py @@ -6,10 +6,10 @@ from easyscience.Constraints import FunctionalConstraint from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter from easyreflectometry.special.calculations import density_to_sld from easyreflectometry.special.calculations import molecular_weight from easyreflectometry.special.calculations import neutron_scattering_length +from easyreflectometry.utils import get_as_parameter from .material import DEFAULTS as MATERIAL_DEFAULTS from .material import Material diff --git a/src/easyreflectometry/sample/elements/materials/material_mixture.py b/src/easyreflectometry/sample/elements/materials/material_mixture.py index 0c87b74a..f3f558d9 100644 --- a/src/easyreflectometry/sample/elements/materials/material_mixture.py +++ b/src/easyreflectometry/sample/elements/materials/material_mixture.py @@ -5,8 +5,8 @@ from easyscience.Constraints import FunctionalConstraint from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter from easyreflectometry.special.calculations import weighted_average +from easyreflectometry.utils import get_as_parameter from ...base_core import BaseCore from .material import DEFAULTS as MATERIAL_DEFAULTS diff --git a/src/easyreflectometry/sample/elements/materials/material_solvated.py b/src/easyreflectometry/sample/elements/materials/material_solvated.py index 2de796c1..db68a0f9 100644 --- a/src/easyreflectometry/sample/elements/materials/material_solvated.py +++ b/src/easyreflectometry/sample/elements/materials/material_solvated.py @@ -4,7 +4,7 @@ from easyscience import global_object from easyscience.Objects.new_variable import Parameter -from easyreflectometry.parameter_utils import get_as_parameter +from easyreflectometry.utils import get_as_parameter from .material import Material from .material_mixture import MaterialMixture diff --git a/src/easyreflectometry/parameter_utils.py b/src/easyreflectometry/utils.py similarity index 74% rename from src/easyreflectometry/parameter_utils.py rename to src/easyreflectometry/utils.py index e4e1a821..01850fb0 100644 --- a/src/easyreflectometry/parameter_utils.py +++ b/src/easyreflectometry/utils.py @@ -52,3 +52,22 @@ def get_as_parameter( def yaml_dump(dict_repr: dict) -> str: return yaml.dump(dict_repr, sort_keys=False, allow_unicode=True) + + +def collect_unique_names_from_dict(structure_dict: dict, unique_names: Optional[list[str]] = None) -> dict: + """ + This function returns a list with the 'unique_name' found the input dictionary. + """ + if unique_names is None: + unique_names = [] + + if isinstance(structure_dict, dict): + for key, value in structure_dict.items(): + if isinstance(value, dict): + collect_unique_names_from_dict(value, unique_names) + elif isinstance(value, list): + for element in value: + collect_unique_names_from_dict(element, unique_names) + if key == 'unique_name': + unique_names.append(value) + return unique_names diff --git a/tests/sample/collections/test_sample.py b/tests/sample/collections/test_sample.py index 1cca441a..fb9955bf 100644 --- a/tests/sample/collections/test_sample.py +++ b/tests/sample/collections/test_sample.py @@ -51,7 +51,7 @@ def test_add_assembly(self): # Expect assert_equal(p[0].name, 'EasyMultilayer') assert_equal(p[1].name, 'EasyMultilayer') - assert_equal(p[2].name, 'New EasyMultilayer') + assert_equal(p[2].name, 'EasyMultilayer added') assert_equal(p[3].name, 'EasySurfactantLayer') p._enable_changes_to_outermost_layers.assert_called() p._disable_changes_to_outermost_layers.assert_called() diff --git a/tests/sample/elements/layers/test_layer.py b/tests/sample/elements/layers/test_layer.py index 2b75e717..7d6cb6bd 100644 --- a/tests/sample/elements/layers/test_layer.py +++ b/tests/sample/elements/layers/test_layer.py @@ -13,10 +13,10 @@ from numpy.testing import assert_equal from easyreflectometry.calculators.factory import CalculatorFactory -from easyreflectometry.parameter_utils import get_as_parameter from easyreflectometry.sample.elements.layers.layer import DEFAULTS from easyreflectometry.sample.elements.layers.layer import Layer from easyreflectometry.sample.elements.materials.material import Material +from easyreflectometry.utils import get_as_parameter class TestLayer(unittest.TestCase): diff --git a/tests/sample/elements/materials/test_material.py b/tests/sample/elements/materials/test_material.py index 7c7b495e..8ef9b9d0 100644 --- a/tests/sample/elements/materials/test_material.py +++ b/tests/sample/elements/materials/test_material.py @@ -8,9 +8,9 @@ import numpy as np from easyscience import global_object -from easyreflectometry.parameter_utils import get_as_parameter from easyreflectometry.sample.elements.materials.material import DEFAULTS from easyreflectometry.sample.elements.materials.material import Material +from easyreflectometry.utils import get_as_parameter class TestMaterial: diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 159e8d8a..45b75ab5 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -8,7 +8,7 @@ import easyreflectometry from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.data.measurement import load -from easyreflectometry.fitting import Fitter +from easyreflectometry.fitting import MultiFitter from easyreflectometry.model import Model from easyreflectometry.model import PercentageFhwm from easyreflectometry.sample import Layer @@ -55,8 +55,8 @@ def test_fitting(minimizer): model.scale.bounds = (0.5, 1.5) interface = CalculatorFactory() model.interface = interface - fitter = Fitter(model) - fitter.easy_f.switch_minimizer(minimizer) + fitter = MultiFitter(model) + fitter.easy_science_multi_fitter.switch_minimizer(minimizer) analysed = fitter.fit(data) assert 'R_0_model' in analysed.keys() assert 'SLD_0' in analysed.keys() diff --git a/tests/test_parameter_utils.py b/tests/test_parameter_utils.py index 910f4b4b..8438031d 100644 --- a/tests/test_parameter_utils.py +++ b/tests/test_parameter_utils.py @@ -2,7 +2,7 @@ import pytest from numpy.testing import assert_equal -from easyreflectometry.parameter_utils import get_as_parameter +from easyreflectometry.utils import get_as_parameter PARAMETER_DETAILS = { 'test_parameter': { diff --git a/tests/test_project.py b/tests/test_project.py index 93a8ebbf..1603c9bc 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,15 +1,17 @@ import datetime import os from pathlib import Path +from unittest.mock import MagicMock import numpy as np from easyscience import global_object from easyscience.fitting import AvailableMinimizers +from easyscience.Objects.new_variable import Parameter from numpy.testing import assert_allclose -from scipp import DataGroup import easyreflectometry from easyreflectometry.data import DataSet1D +from easyreflectometry.fitting import MultiFitter from easyreflectometry.model import LinearSpline from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection @@ -28,8 +30,8 @@ def test_constructor(self): # Expect assert project._info == { - 'name': 'Example Project', - 'short_description': 'reflectometry, 1D', + 'name': 'ExampleProject', + 'short_description': 'Reflectometry, 1D', 'samples': 'None', 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), @@ -38,11 +40,19 @@ def test_constructor(self): assert len(project._materials) == 0 assert len(project._models) == 0 assert project._calculator.current_interface_name == 'refnx' - assert project._minimizer == AvailableMinimizers.LMFit_leastsq assert project._experiments == {} assert project._report is None assert project._created is False assert project._with_experiments is False + assert project._current_material_index == 0 + assert project._current_model_index == 0 + assert project._current_assembly_index == 0 + assert project._current_layer_index == 0 + assert project._fitter_model_index is None + assert project._fitter is None + assert project._q_min is None + assert project._q_max is None + assert project._q_resolution is None def test_reset(self): # When @@ -51,20 +61,27 @@ def test_reset(self): project._materials.append(Material()) project._models.append(Model()) project._calculator = 'calculator' - project._minimizer = 'minimizer' project._experiments = 'experiments' project._report = 'report' project._created = True project._with_experiments = True project._path_project_parent = 'project_path' - + project._fitter = 'fitter' + project._current_material_index = 10 + project._current_model_index = 10 + project._current_assembly_index = 10 + project._current_layer_index = 10 + project._fitter_model_index = 10 + project._q_min = 'q_min' + project._q_max = 'q_max' + project._q_resolution == 'q_resolution' # Then project.reset() # Expect assert project._info == { - 'name': 'Example Project', - 'short_description': 'reflectometry, 1D', + 'name': 'ExampleProject', + 'short_description': 'Reflectometry, 1D', 'samples': 'None', 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), @@ -76,12 +93,20 @@ def test_reset(self): assert project._path_project_parent == Path(os.path.expanduser('~')) assert project._calculator.current_interface_name == 'refnx' - assert project._minimizer == AvailableMinimizers.LMFit_leastsq assert project._experiments == {} assert project._report is None assert project._created is False assert project._with_experiments is False assert global_object.map.vertices() == ['project_models', 'project_materials'] + assert project._fitter is None + assert project._current_material_index == 0 + assert project._current_model_index == 0 + assert project._current_assembly_index == 0 + assert project._current_layer_index == 0 + assert project._fitter_model_index is None + assert project._q_min is None + assert project._q_max is None + assert project._q_resolution is None def test_models(self): # When @@ -103,6 +128,7 @@ def test_models(self): assert project._materials[0] == material assert project._materials[1] == models[0].sample[0].layers[0].material assert project._materials[2] == models[0].sample[1].layers[0].material + assert project.models[0].interface == project._calculator def test_default_model(self): # When @@ -167,11 +193,65 @@ def test_minimizer(self): # When project = Project() + # Then Expect + assert project.minimizer == AvailableMinimizers.LMFit_leastsq + + def test_set_minimizer(self): + # When + project = Project() + project._fitter = MagicMock() + project._fitter.easy_science_multi_fitter = MagicMock() + project._fitter.easy_science_multi_fitter.switch_minimizer = MagicMock() + # Then project.minimizer = 'minimizer' # Expect - assert project.minimizer == 'minimizer' + project._fitter.easy_science_multi_fitter.switch_minimizer.assert_called_once_with('minimizer') + + def test_fitter_none(self): + # When + project = Project() + + # Then Expect + assert project.fitter is None + + def test_fitter_model(self): + # When + project = Project() + project.default_model() + + # Then Expect + assert isinstance(project.fitter, MultiFitter) + + def test_fitter_same_model_index(self): + # When + project = Project() + project.default_model() + fitter_0 = project.fitter + project._models.append(Model()) + + # Then + fitter_1 = project.fitter + + # Expect + assert fitter_0 is fitter_1 + + def test_fitter_new_model_index(self): + # When + project = Project() + project.default_model() + fitter_0 = project.fitter + model = Model() + project._models.append(model) + project._models[1].interface = project._models[0].interface + project._current_model_index = 1 + + # Then + fitter_1 = project.fitter + + # Expect + assert fitter_0 is not fitter_1 def test_experiments(self): # When @@ -189,7 +269,7 @@ def test_path_json(self, tmp_path): project.set_path_project_parent(tmp_path) # Then Expect - assert project.path_json == Path(tmp_path) / 'Example Project' / 'project.json' + assert project.path_json == Path(tmp_path) / 'ExampleProject' / 'project.json' def test_add_material(self): # When @@ -237,8 +317,8 @@ def test_default_info(self): # Expect assert info == { - 'name': 'Example Project', - 'short_description': 'reflectometry, 1D', + 'name': 'ExampleProject', + 'short_description': 'Reflectometry, 1D', 'samples': 'None', 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), @@ -257,19 +337,17 @@ def test_as_dict(self): assert keys == [ 'calculator', 'info', - 'minimizer', 'models', 'with_experiments', ] assert project_dict['info'] == { - 'name': 'Example Project', - 'short_description': 'reflectometry, 1D', + 'name': 'ExampleProject', + 'short_description': 'Reflectometry, 1D', 'samples': 'None', 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } assert project_dict['calculator'] == 'refnx' - assert project_dict['minimizer'] == 'LMFit_leastsq' assert project_dict['models']['data'] == [] assert project_dict['with_experiments'] is False @@ -284,7 +362,7 @@ def test_as_dict_models(self): # Expect models_dict = models.as_dict(skip=['interface']) - models_dict['unique_name'] = 'project_models' + models_dict['unique_name'] = 'project_models_to_prevent_collisions_on_load' assert project_dict['models'] == models_dict def test_as_dict_materials_not_in_model(self): @@ -304,14 +382,15 @@ def test_as_dict_materials_not_in_model(self): def test_as_dict_minimizer(self): # When project = Project() - minimizer = AvailableMinimizers.LMFit - project.minimizer = minimizer + project._fitter = MagicMock() + project._fitter.easy_science_multi_fitter = MagicMock() + project._fitter.easy_science_multi_fitter.minimizer = AvailableMinimizers.LMFit # Then project_dict = project.as_dict() # Expect - assert project_dict['minimizer'] == 'LMFit' + assert project_dict['fitter_minimizer'] == 'LMFit' def test_replace_collection(self): # When @@ -353,6 +432,8 @@ def test_dict_round_trip(self): project.add_material(material) minimizer = AvailableMinimizers.LMFit project.minimizer = minimizer + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath) project_dict = project.as_dict(include_materials_not_in_model=True) project_materials_dict = project._materials.as_dict() @@ -376,12 +457,14 @@ def test_save_as_json(self, tmp_path): global_object.map._clear() project = Project() project.set_path_project_parent(tmp_path) - project._models.append(Model()) + project.default_model() project._info['name'] = 'Test Project' + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath) + # Then project.save_as_json() - project.path_json # Expect assert project.path_json.exists() @@ -396,7 +479,7 @@ def test_save_as_json_overwrite(self, tmp_path): # Then project._info['short_description'] = 'short_description' - project._models.append(Model()) + project.default_model() project.save_as_json(overwrite=True) # Expect @@ -412,7 +495,7 @@ def test_save_as_json_dont_overwrite(self, tmp_path): # Then project._info['short_description'] = 'short_description' - project._models.append(Model()) + project.default_model() project.save_as_json() # Expect @@ -423,7 +506,7 @@ def test_load_from_json(self, tmp_path): global_object.map._clear() project = Project() project.set_path_project_parent(tmp_path) - project._models.append(Model()) + project.default_model() project._info['name'] = 'name' project._info['short_description'] = 'short_description' project._info['samples'] = 'samples' @@ -455,19 +538,19 @@ def test_create(self, tmp_path): project = Project() project.set_path_project_parent(tmp_path) project._info['modified'] = 'modified' - project._info['name'] = 'Test Project' + project._info['name'] = 'TestProject' # Then project.create() # Expect - assert project.path == tmp_path / 'Test Project' + assert project.path == tmp_path / 'TestProject' assert project.path.exists() assert (project.path / 'experiments').exists() assert project.created is True assert project._info == { - 'name': 'Test Project', - 'short_description': 'reflectometry, 1D', + 'name': 'TestProject', + 'short_description': 'Reflectometry, 1D', 'samples': 'None', 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), @@ -476,7 +559,8 @@ def test_create(self, tmp_path): def test_load_experiment(self): # When project = Project() - project.models = ModelCollection(Model(), Model(), Model(), Model(), Model(), Model()) + model_5 = Model() + project.models = ModelCollection(Model(), Model(), Model(), Model(), Model(), model_5) fpath = os.path.join(PATH_STATIC, 'example.ort') # Then @@ -484,7 +568,9 @@ def test_load_experiment(self): # Expect assert list(project.experiments.keys()) == [5] - assert isinstance(project.experiments[5], DataGroup) + assert isinstance(project.experiments[5], DataSet1D) + assert project.experiments[5].name == 'Experiment for Model 5' + assert project.experiments[5].model == model_5 assert isinstance(project.models[5].resolution_function, LinearSpline) assert isinstance(project.models[4].resolution_function, PercentageFhwm) @@ -529,3 +615,32 @@ def test_set_q(self): # Expect q = project.q_min, project.q_max, project.q_resolution assert q == (1, 2, 3) + + def test_calculator(self): + # When + project = Project() + + # Then Expect + assert project.calculator == 'refnx' + + def test_set_calculator(self): + # When + project = Project() + + # Then + project.calculator = 'refl1d' + + # Expect + assert project._calculator.current_interface_name == 'refl1d' + + def test_parameters(self): + # When + project = Project() + project.default_model() + + # Then + parameters = project.parameters + + # Expect + assert len(parameters) == 14 + assert isinstance(parameters[0], Parameter) From 0cd7dcbf892f9270a613b8fcd2b235e900404070 Mon Sep 17 00:00:00 2001 From: Andreas Pedersen <48797331+andped10@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:49:00 +0100 Subject: [PATCH 11/11] 201 move reporting to the lib (#202) * first summary element * refinement section * test rename * utils with counting * experiments section * html summary * skeleton parameter * replace in template * paramaters as table * html formatting * html formatting * summary formatting * constained parameters * constraints in summary * summary * cleaning project info * pdf summary * summary update * code cleaning * figures in summary * only figures when requested * legends and eps files * back to jpg * notebook for project object * text adjustments project notebook * link to pdf * always add project information * pr response * reduce code complexity * trying to understand CodeFactor * more CodeFactor * trying to make CodaFactor happy * CodaFactor problems * CodeFactor * codefactor * cidefactor * codefactor * codefactor * code factor * codefactor * code factor * CodeFactor * code factor * code factor * codefactor * codafactor not using free * code factor * code factor * code facoter * count functionality back to utils --- docs/src/tutorials/project.ipynb | 315 ++++++++++++++++++ pyproject.toml | 3 +- src/easyreflectometry/project.py | 6 +- src/easyreflectometry/summary/__init__.py | 3 + .../summary/html_templates.py | 149 +++++++++ src/easyreflectometry/summary/summary.py | 194 +++++++++++ src/easyreflectometry/utils.py | 12 + tests/summary/test_summary.py | 207 ++++++++++++ tests/test_project.py | 20 +- tests/test_utils.py | 43 +++ 10 files changed, 932 insertions(+), 20 deletions(-) create mode 100644 docs/src/tutorials/project.ipynb create mode 100644 src/easyreflectometry/summary/__init__.py create mode 100644 src/easyreflectometry/summary/html_templates.py create mode 100644 src/easyreflectometry/summary/summary.py create mode 100644 tests/summary/test_summary.py create mode 100644 tests/test_utils.py diff --git a/docs/src/tutorials/project.ipynb b/docs/src/tutorials/project.ipynb new file mode 100644 index 00000000..4687a175 --- /dev/null +++ b/docs/src/tutorials/project.ipynb @@ -0,0 +1,315 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Project\n", + "This notebook serves to demonstrate some of the functionality of the Project object.\n", + "\n", + "## Setup\n", + "First configure matplotlib to place figures in notebook and import needed modules." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from easyreflectometry import Project\n", + "from easyreflectometry.summary import Summary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Project object\n", + "\n", + "First we will create a `Project` object. There should only be one such object. The project is follwoing set to have the current folder at its root and the we give it the name: `MyNewProject`. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()\n", + "project.set_path_project_parent('.')\n", + "project._info['name'] = 'MyNewProject'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will then populate this `Project` with the default model." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EasyModels:\n", + "- EasyModel:\n", + " scale: 1.0\n", + " background: 1.0e-08\n", + " resolution: 5.0 %\n", + " color: black\n", + " sample:\n", + " EasySample:\n", + " - Superphase:\n", + " Vacuum Layer:\n", + " - Vacuum Layer:\n", + " material:\n", + " Air:\n", + " sld: 0.000e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 0.000 Å\n", + " roughness: 0.000 Å\n", + " - D2O:\n", + " D2O Layer:\n", + " - D2O Layer:\n", + " material:\n", + " D2O:\n", + " sld: 6.335e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 100.000 Å\n", + " roughness: 3.000 Å\n", + " - Subphase:\n", + " Si Layer:\n", + " - Si Layer:\n", + " material:\n", + " Si:\n", + " sld: 2.074e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 0.000 Å\n", + " roughness: 1.200 Å\n", + "\n" + ] + } + ], + "source": [ + "project.default_model()\n", + "print(project.models)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also see which materials that are defined in the `Project`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EasyMaterials:\n", + "- Air:\n", + " sld: 0.000e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + "- D2O:\n", + " sld: 6.335e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + "- Si:\n", + " sld: 2.074e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + "\n" + ] + } + ], + "source": [ + "print(project._materials)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is possible to save a `Project` object. It will be place in the project folder and the state will be save in the file name `project.json`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as_json(overwrite=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also generate a summary of the project. We are also going to store the PDF and HTML files with the summary in the project folder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "WindowsPath('MyNewProject/summary.pdf')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary = Summary(project)\n", + "summary.save_pdf_summary(str(project.path / 'summary.pdf'))\n", + "summary.save_html_summary(str(project.path / 'summary.html'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Link to PDF summary](MyNewProject/summary.pdf) in `MyNewProject/summary.pdf`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can reset the project to a blank state." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EasyModels: []\n", + "\n", + "EasyMaterials: []\n", + "\n" + ] + } + ], + "source": [ + "project.reset()\n", + "print(project.models)\n", + "print(project._materials)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then let us try to load the state we saved above." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EasyModels:\n", + "- EasyModel:\n", + " scale: 1.0\n", + " background: 1.0e-08\n", + " resolution: 5.0 %\n", + " color: black\n", + " sample:\n", + " EasySample:\n", + " - Superphase:\n", + " EasyLayerCollection:\n", + " - Vacuum Layer:\n", + " material:\n", + " Air:\n", + " sld: 0.000e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 0.000 Å\n", + " roughness: 0.000 Å\n", + " - D2O:\n", + " EasyLayerCollection:\n", + " - D2O Layer:\n", + " material:\n", + " D2O:\n", + " sld: 6.335e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 100.000 Å\n", + " roughness: 3.000 Å\n", + " - Subphase:\n", + " EasyLayerCollection:\n", + " - Si Layer:\n", + " material:\n", + " Si:\n", + " sld: 2.074e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + " thickness: 0.000 Å\n", + " roughness: 1.200 Å\n", + "\n", + "EasyMaterials:\n", + "- Air:\n", + " sld: 0.000e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + "- D2O:\n", + " sld: 6.335e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + "- Si:\n", + " sld: 2.074e-6 1/Å^2\n", + " isld: 0.000e-6 1/Å^2\n", + "\n" + ] + } + ], + "source": [ + "project.set_path_project_parent('.')\n", + "project._info['name'] = 'MyNewProject'\n", + "project.load_from_json()\n", + "print(project.models)\n", + "print(project._materials)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index d9659381..4c95bda3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,8 @@ dependencies = [ "refnx>=0.1.15", "refl1d>=0.8.14", "orsopy>=0.0.4", - "pint==0.23" # Only to ensure that unit is reported as dimensionless rather than empty string + "pint==0.23", # Only to ensure that unit is reported as dimensionless rather than empty string + "xhtml2pdf>=0.2.16" ] [project.optional-dependencies] diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 5eed8269..4b3ee991 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -320,10 +320,8 @@ def remove_material(self, index: int) -> None: def _default_info(self): return dict( - name='ExampleProject', + name='DefaultEasyReflectometryProject', short_description='Reflectometry, 1D', - samples='None', - experiments='None', modified=datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), ) @@ -349,9 +347,9 @@ def save_as_json(self, overwrite=False): print(exception) def load_from_json(self, path: Optional[Union[Path, str]] = None): - path = Path(path) if path is None: path = self.path_json + path = Path(path) if path.exists(): with open(path, 'r') as file: project_dict = json.load(file) diff --git a/src/easyreflectometry/summary/__init__.py b/src/easyreflectometry/summary/__init__.py new file mode 100644 index 00000000..af9d5fa4 --- /dev/null +++ b/src/easyreflectometry/summary/__init__.py @@ -0,0 +1,3 @@ +from .summary import Summary + +__all__ = [Summary] diff --git a/src/easyreflectometry/summary/html_templates.py b/src/easyreflectometry/summary/html_templates.py new file mode 100644 index 00000000..dc8afc0e --- /dev/null +++ b/src/easyreflectometry/summary/html_templates.py @@ -0,0 +1,149 @@ +HTML_TEMPLATE = """ + + + + + + + + + + + + + + + project_information_section + + + + + + + + sample_section + + + + + + + + experiments_section + + + + + + + + refinement_section + +

Summary

Sample

Experiments

Refinement

+ + figures_section + + +""" + + +HTML_PROJECT_INFORMATION_TEMPLATE = """ + +

Project information

+ + + + Title + project_title + + + Description + project_description + + + No. of experiments + num_experiments + +""" + +HTML_PARAMETER_HEADER_TEMPLATE = """ + + parameter_name + parameter_value + parameter_unit + parameter_error + +""" + +HTML_PARAMETER_TEMPLATE = """ + + parameter_name + parameter_value + parameter_unit + parameter_error + +""" + +HTML_DATA_COLLECTION_TEMPLATE = """ + + Experiment datablock + experiment_name + + + Measured intensity range: + [range_min, range_max] + + + No. of data points + num_data_points + + + Resolution function + resolution_function + + +""" + +HTML_REFINEMENT_TEMPLATE = """ + + Calculation engine + calculation_engine + + + Minimization engine + minimization_engine + + + + + + + No. of parameters: + num_total_params + + + No. of free parameters: + num_free_params + + + No. of fixed parameters: + num_fixed_params + + + No. of constraints + num_constriants + +""" + +HTML_FIGURES_TEMPLATE = """ +SLD plot +
+Fit experiment plot +""" diff --git a/src/easyreflectometry/summary/summary.py b/src/easyreflectometry/summary/summary.py new file mode 100644 index 00000000..e36a7724 --- /dev/null +++ b/src/easyreflectometry/summary/summary.py @@ -0,0 +1,194 @@ +import matplotlib.pyplot as plt +import numpy as np +from easyscience import global_object +from xhtml2pdf import pisa + +from easyreflectometry import Project +from easyreflectometry.utils import count_fixed_parameters +from easyreflectometry.utils import count_free_parameters +from easyreflectometry.utils import count_parameter_user_constraints + +from .html_templates import HTML_DATA_COLLECTION_TEMPLATE +from .html_templates import HTML_FIGURES_TEMPLATE +from .html_templates import HTML_PARAMETER_HEADER_TEMPLATE +from .html_templates import HTML_PARAMETER_TEMPLATE +from .html_templates import HTML_PROJECT_INFORMATION_TEMPLATE +from .html_templates import HTML_REFINEMENT_TEMPLATE +from .html_templates import HTML_TEMPLATE + + +class Summary: + def __init__(self, project: Project): + self._project = project + + def compile_html_summary(self, figures: bool = False) -> str: + html = HTML_TEMPLATE + + html = html.replace('project_information_section', self._project_information_section()) + + html = html.replace('sample_section', self._sample_section()) + + experiments_section = self._experiments_section() + if experiments_section == '': # no experiments + experiments_section = 'No experiments' + html = html.replace('experiments_section', experiments_section) + + html = html.replace('refinement_section', self._refinement_section()) + + if figures: + html = html.replace('figures_section', self._figures_section()) + else: + html = html.replace('figures_section', '') + + return html + + def save_html_summary(self, filename: str) -> None: + html = self.compile_html_summary(figures=True) + with open(filename, 'w') as f: + f.write(html) + + def save_pdf_summary(self, filename: str) -> None: + html = self.compile_html_summary(figures=True) + + with open(filename, 'w+b') as result_file: + pisa_status = pisa.CreatePDF( + html, + dest=result_file, + ) + + if pisa_status.err: + print('An error occured when generating PDF summary!') + + def save_sld_plot(self, filename: str) -> None: + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + + sld = self._project.sld_data_for_model_at_index(0) + ax.plot(sld.x, sld.y, color='blue') + + ax.set_xlabel('z (Å)') + ax.set_ylabel('SLD (Å⁻²)') + fig.legend(['SLD']) + fig.savefig(filename, dpi=600) + plt.close() + + def save_fit_experiment_plot(self, filename: str) -> None: + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + legends = [] + + model = self._project.model_data_for_model_at_index(0) + ax.plot(model.x, np.log10(model.y), color='blue') + legends.append('Model') + + try: + experiment = self._project.experimental_data_for_model_at_index(0) + ax.plot(experiment.x, np.log10(experiment.y), color='red') + legends.append('Experiment') + except IndexError: + pass + + ax.set_xlabel('Q (Å⁻¹)') + ax.set_ylabel('Reflectivity') + fig.legend(legends) + fig.savefig(filename, dpi=600) + plt.close() + + def _project_information_section(self) -> str: + html_project = HTML_PROJECT_INFORMATION_TEMPLATE + + name = self._project._info['name'] + short_description = self._project._info['short_description'] + html_project = html_project.replace('project_title', f'{name}') + html_project = html_project.replace('project_description', f'{short_description}') + html_project = html_project.replace('num_experiments', f'{len(self._project.experiments)}') + return html_project + + def _sample_section(self) -> str: + html_parameters = [] + + html_parameter = HTML_PARAMETER_HEADER_TEMPLATE + html_parameter = html_parameter.replace('parameter_name', 'Name') + html_parameter = html_parameter.replace('parameter_value', 'Value') + html_parameter = html_parameter.replace('parameter_unit', 'Unit') + html_parameter = html_parameter.replace('parameter_error', 'Error') + html_parameters.append(html_parameter) + + for parameter in self._project.parameters: + path = global_object.map.find_path( + self._project._models[self._project.current_model_index].unique_name, parameter.unique_name + ) + if 0 < len(path): + name = f'{global_object.map.get_item_by_key(path[-2]).name} {global_object.map.get_item_by_key(path[-1]).name}' + else: + name = parameter.name + value = parameter.value + unit = parameter.unit + error = parameter.error + + html_parameter = HTML_PARAMETER_TEMPLATE + html_parameter = html_parameter.replace('parameter_name', f'{name}') + html_parameter = html_parameter.replace('parameter_value', f'{value}') + html_parameter = html_parameter.replace('parameter_unit', f'{unit}') + html_parameter = html_parameter.replace('parameter_error', f'{error}') + html_parameters.append(html_parameter) + + html_parameters_str = '\n'.join(html_parameters) + + return html_parameters_str + + def _experiments_section(self) -> str: + html_experiments = [] + + for idx, experiment in self._project.experiments.items(): + experiment_name = experiment.name + num_data_points = len(experiment.x) + resolution_function = self._project.models[idx].resolution_function.as_dict()['smearing'] + if resolution_function == 'PercentageFhwm': + precentage = self._project.models[idx].resolution_function.as_dict()['constant'] + resolution_function = f'{resolution_function} {precentage}%' + range_min = min(experiment.y) + range_max = max(experiment.y) + range_units = 'Å⁻¹' + html_experiment = HTML_DATA_COLLECTION_TEMPLATE + html_experiment = html_experiment.replace('experiment_name', f'{experiment_name}') + html_experiment = html_experiment.replace('range_min', f'{range_min}') + html_experiment = html_experiment.replace('range_max', f'{range_max}') + html_experiment = html_experiment.replace('range_units', f'{range_units}') + html_experiment = html_experiment.replace('num_data_points', f'{num_data_points}') + html_experiment = html_experiment.replace('resolution_function', f'{resolution_function}') + html_experiments.append(html_experiment) + + html_experiments_str = '\n'.join(html_experiments) + + return html_experiments_str + + def _refinement_section(self) -> str: + html_refinement = HTML_REFINEMENT_TEMPLATE + num_free_params = count_free_parameters(self._project) + num_fixed_params = count_fixed_parameters(self._project) + num_params = num_free_params + num_fixed_params + # goodness_of_fit = self._project.status.goodnessOfFit + # goodness_of_fit = goodness_of_fit.split(' → ')[-1] + num_constraints = count_parameter_user_constraints(self._project) + + html_refinement = html_refinement.replace('calculation_engine', f'{self._project._calculator.current_interface_name}') + html_refinement = html_refinement.replace('minimization_engine', f'{self._project.minimizer.name}') + # html = html.replace('goodness_of_fit', f'{goodness_of_fit}') + html_refinement = html_refinement.replace('num_total_params', f'{num_params}') + html_refinement = html_refinement.replace('num_free_params', f'{num_free_params}') + html_refinement = html_refinement.replace('num_fixed_params', f'{num_fixed_params}') + html_refinement = html_refinement.replace('num_constriants', f'{num_constraints}') + return html_refinement + + def _figures_section(self) -> None: + html_figures = HTML_FIGURES_TEMPLATE + path_sld = self._project.path / 'sld_plot.jpg' + path_fit_experiment = self._project.path / 'fit_experiment_plot.jpg' + + self.save_sld_plot(path_sld) + self.save_fit_experiment_plot(path_fit_experiment) + + html_figures = html_figures.replace('path_sld_plot', str(path_sld)) + html_figures = html_figures.replace('path_fit_experiment_plot', str(path_fit_experiment)) + return html_figures diff --git a/src/easyreflectometry/utils.py b/src/easyreflectometry/utils.py index 01850fb0..00138804 100644 --- a/src/easyreflectometry/utils.py +++ b/src/easyreflectometry/utils.py @@ -71,3 +71,15 @@ def collect_unique_names_from_dict(structure_dict: dict, unique_names: Optional[ if key == 'unique_name': unique_names.append(value) return unique_names + + +def count_free_parameters(project) -> int: + return sum(1 for parameter in project.parameters if parameter.free) + + +def count_fixed_parameters(project) -> int: + return sum(1 for parameter in project.parameters if not parameter.free) + + +def count_parameter_user_constraints(project) -> int: + return sum(len(parameter.user_constraints.keys()) for parameter in project.parameters if not parameter.free) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py new file mode 100644 index 00000000..3316befd --- /dev/null +++ b/tests/summary/test_summary.py @@ -0,0 +1,207 @@ +import os +from unittest.mock import MagicMock + +import pytest +from easyscience import global_object + +import easyreflectometry +from easyreflectometry import Project +from easyreflectometry.model.resolution_functions import PercentageFhwm +from easyreflectometry.summary import Summary + +PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests', '_static') + + +class TestSummary: + @pytest.fixture + def project(self) -> Project: + global_object.map._clear() + project = Project() + project.default_model() + return project + + def test_constructor(self, project: Project) -> None: + # When Then + result = Summary(project) + + # Expect + assert result._project == project + + def test_compile_html_summary(self, project: Project) -> None: + # When + summary = Summary(project) + summary._project_information_section = MagicMock(return_value='project result html') + summary._sample_section = MagicMock(return_value='sample result html') + summary._experiments_section = MagicMock(return_value='experiments results html') + summary._refinement_section = MagicMock(return_value='refinement result html') + summary._figures_section = MagicMock() + + # Then + result = summary.compile_html_summary() + + # Expect + summary._figures_section.assert_not_called() + assert 'project result html' in result + assert 'sample result html' in result + assert 'experiments results html' in result + assert 'refinement result html' in result + assert 'figures_section' not in result + + def test_compile_html_summary_with_figures(self, project: Project) -> None: + # When + summary = Summary(project) + summary._project_information_section = MagicMock(return_value='project result html') + summary._sample_section = MagicMock(return_value='sample result html') + summary._experiments_section = MagicMock(return_value='experiments results html') + summary._refinement_section = MagicMock(return_value='refinement result html') + summary._figures_section = MagicMock(return_value='figures result html') + + # Then + result = summary.compile_html_summary(figures=True) + + # Expect + assert 'figures result html' in result + + def test_save_html_summary(self, project: Project, tmp_path) -> None: + # When + summary = Summary(project) + summary.compile_html_summary = MagicMock(return_value='html') + file_path = tmp_path / 'filename' + file_path = file_path.with_suffix('.html') + + # Then + summary.save_html_summary(file_path) + + # Expect + assert os.path.exists(file_path) + with open(file_path, 'r') as f: + assert f.read() == 'html' + + def test_save_pdf_summary(self, project: Project, tmp_path) -> None: + # When + summary = Summary(project) + summary.compile_html_summary = MagicMock(return_value='html') + file_path = tmp_path / 'filename' + file_path = file_path.with_suffix('.pdf') + + # Then + summary.save_pdf_summary(file_path) + + # Expect + assert os.path.exists(file_path) + + def test_project_information_section(self, project: Project) -> None: + # When + summary = Summary(project) + + # Then + html = summary._project_information_section() + + # Expect + assert 'DefaultEasyReflectometryProject' in html + assert 'Reflectometry, 1D' in html + + def test_sample_section(self, project: Project) -> None: + # When + summary = Summary(project) + + # Then + html = summary._sample_section() + + # Expect + assert 'Name' in html + assert 'Value' in html + assert 'Unit' in html + assert 'Error' in html + + assert 'sld' in html + assert 'isld' in html + assert 'thickness' in html + assert 'background' in html + + def test_experiments_section(self, project: Project) -> None: + # When + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath) + summary = Summary(project) + + # Then + html = summary._experiments_section() + + # Expect + assert 'Experiment for Model 0' in html + assert 'No. of data points' in html + assert '408' in html + assert 'Resolution function' in html + assert 'LinearSpline' in html + + def test_experiments_section_percentage_fhwm(self, project: Project) -> None: + # When + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath) + project.models[0].resolution_function = PercentageFhwm(5) + summary = Summary(project) + + # Then + html = summary._experiments_section() + + # Expect + assert 'PercentageFhwm 5%' in html + + def test_refinement_section(self, project: Project) -> None: + # When + summary = Summary(project) + + # Then + html = summary._refinement_section() + + # Expect + assert 'refnx' in html + assert 'LMFit_leastsq' in html + assert 'No. of parameters:' in html + assert 'No. of fixed parameters:' in html + assert '14' in html + assert 'No. of free parameters:' in html + assert '0' in html + assert 'No. of constraints' in html + + def test_save_sld_plot(self, project: Project, tmp_path) -> None: + # When + summary = Summary(project) + file_path = tmp_path / 'filename' + file_path = file_path.with_suffix('.jpg') + + # Then + summary.save_sld_plot(file_path) + + # Expect + assert os.path.exists(file_path) + + def test_save_fit_experiment_plot(self, project: Project, tmp_path) -> None: + # When + summary = Summary(project) + file_path = tmp_path / 'filename' + file_path = file_path.with_suffix('.jpg') + fpath = os.path.join(PATH_STATIC, 'example.ort') + project.load_experiment_for_model_at_index(fpath) + + # Then + summary.save_fit_experiment_plot(file_path) + + # Expect + assert os.path.exists(file_path) + + def test_figures_section(self, project: Project) -> None: + # When + summary = Summary(project) + summary.save_sld_plot = MagicMock() + summary.save_fit_experiment_plot = MagicMock() + + # Then + html = summary._figures_section() + + # Expect + summary.save_sld_plot.assert_called_once() + summary.save_fit_experiment_plot.assert_called_once() + assert 'sld_plot' in html + assert 'fit_experiment_plot' in html diff --git a/tests/test_project.py b/tests/test_project.py index 1603c9bc..0d626b1f 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -30,10 +30,8 @@ def test_constructor(self): # Expect assert project._info == { - 'name': 'ExampleProject', + 'name': 'DefaultEasyReflectometryProject', 'short_description': 'Reflectometry, 1D', - 'samples': 'None', - 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } assert project._path_project_parent == Path(os.path.expanduser('~')) @@ -80,10 +78,8 @@ def test_reset(self): # Expect assert project._info == { - 'name': 'ExampleProject', + 'name': 'DefaultEasyReflectometryProject', 'short_description': 'Reflectometry, 1D', - 'samples': 'None', - 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } assert project._models.unique_name == 'project_models' @@ -269,7 +265,7 @@ def test_path_json(self, tmp_path): project.set_path_project_parent(tmp_path) # Then Expect - assert project.path_json == Path(tmp_path) / 'ExampleProject' / 'project.json' + assert project.path_json == Path(tmp_path) / 'DefaultEasyReflectometryProject' / 'project.json' def test_add_material(self): # When @@ -317,10 +313,8 @@ def test_default_info(self): # Expect assert info == { - 'name': 'ExampleProject', + 'name': 'DefaultEasyReflectometryProject', 'short_description': 'Reflectometry, 1D', - 'samples': 'None', - 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } @@ -341,10 +335,8 @@ def test_as_dict(self): 'with_experiments', ] assert project_dict['info'] == { - 'name': 'ExampleProject', + 'name': 'DefaultEasyReflectometryProject', 'short_description': 'Reflectometry, 1D', - 'samples': 'None', - 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } assert project_dict['calculator'] == 'refnx' @@ -551,8 +543,6 @@ def test_create(self, tmp_path): assert project._info == { 'name': 'TestProject', 'short_description': 'Reflectometry, 1D', - 'samples': 'None', - 'experiments': 'None', 'modified': datetime.datetime.now().strftime('%d.%m.%Y %H:%M'), } diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..a5a2bbe7 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,43 @@ +from easyreflectometry import Project +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(): + # When + project = Project() + project.default_model() + project.parameters[0].free = True + + # Then + count = count_free_parameters(project) + + # Expect + assert count == 1 + + +def test_count_fixed_parameters(): + # When + project = Project() + project.default_model() + project.parameters[0].free = True + + # Then + count = count_fixed_parameters(project) + + # Expect + assert count == 13 + + +def test_count_parameter_user_constraints(): + # When + project = Project() + project.default_model() + project.parameters[0].user_constraints['name_other_parameter'] = 'constraint' + + # Then + count = count_parameter_user_constraints(project) + + # Expect + assert count == 1