From 4314f9b37beecf6e80bed2b4c83a3899b713d760 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 17:11:06 +0200 Subject: [PATCH 01/57] Standardize tutorial project save paths --- docs/docs/tutorials/ed-1.py | 6 ++++++ docs/docs/tutorials/ed-10.py | 6 ++++++ docs/docs/tutorials/ed-11.py | 6 ++++++ docs/docs/tutorials/ed-12.py | 6 ++++++ docs/docs/tutorials/ed-13.py | 4 ++-- docs/docs/tutorials/ed-14.py | 2 +- docs/docs/tutorials/ed-15.py | 6 ++++++ docs/docs/tutorials/ed-16.py | 6 ++++++ docs/docs/tutorials/ed-17.py | 2 +- docs/docs/tutorials/ed-18.py | 6 ++++++ docs/docs/tutorials/ed-2.py | 2 +- docs/docs/tutorials/ed-20.py | 2 +- docs/docs/tutorials/ed-23.py | 6 ++++++ docs/docs/tutorials/ed-24.py | 6 ++++++ docs/docs/tutorials/ed-3.py | 2 +- docs/docs/tutorials/ed-4.py | 6 ++++++ docs/docs/tutorials/ed-5.py | 2 +- docs/docs/tutorials/ed-6.py | 6 ++++++ docs/docs/tutorials/ed-7.py | 6 ++++++ docs/docs/tutorials/ed-8.py | 6 ++++++ docs/docs/tutorials/ed-9.py | 6 ++++++ 21 files changed, 92 insertions(+), 8 deletions(-) diff --git a/docs/docs/tutorials/ed-1.py b/docs/docs/tutorials/ed-1.py index 07e1830b3..4b16fd688 100644 --- a/docs/docs/tutorials/ed-1.py +++ b/docs/docs/tutorials/ed-1.py @@ -115,3 +115,9 @@ # %% # Plot measured vs. calculated diffraction patterns project.display.pattern(expt_name='hrpt') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_1_lbco_hrpt') diff --git a/docs/docs/tutorials/ed-10.py b/docs/docs/tutorials/ed-10.py index 751831508..e9271fd76 100644 --- a/docs/docs/tutorials/ed-10.py +++ b/docs/docs/tutorials/ed-10.py @@ -101,3 +101,9 @@ # %% project.display.pattern(expt_name='pdf') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_10_ni_pdf') diff --git a/docs/docs/tutorials/ed-11.py b/docs/docs/tutorials/ed-11.py index e08e81d63..18aedca0e 100644 --- a/docs/docs/tutorials/ed-11.py +++ b/docs/docs/tutorials/ed-11.py @@ -112,3 +112,9 @@ # %% project.display.pattern(expt_name='nomad', include=('measured', 'calculated')) + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_11_si_nomad_pdf') diff --git a/docs/docs/tutorials/ed-12.py b/docs/docs/tutorials/ed-12.py index 3add79927..6cd87d3a1 100644 --- a/docs/docs/tutorials/ed-12.py +++ b/docs/docs/tutorials/ed-12.py @@ -131,3 +131,9 @@ # %% project.display.pattern(expt_name='xray_pdf') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_12_nacl_xray_pdf') diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index 82970780f..416e79999 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -709,7 +709,7 @@ # directory specified by the `dir_path` attribute of the project object. # %% -project_1.save_as(dir_path='data/powder_diffraction_Si') +project_1.save_as(dir_path='projects/ed_13_si') # %% [markdown] # ## 💪 Exercise: Complex Fit – LBCO @@ -1478,7 +1478,7 @@ # the analysis. # %% -project_2.save_as(dir_path='data/powder_diffraction_LBCO_Si') +project_2.save_as(dir_path='projects/ed_13_lbco_si') # %% [markdown] # #### Final Remarks diff --git a/docs/docs/tutorials/ed-14.py b/docs/docs/tutorials/ed-14.py index f0b214526..d161f3ceb 100644 --- a/docs/docs/tutorials/ed-14.py +++ b/docs/docs/tutorials/ed-14.py @@ -21,7 +21,7 @@ refinement of the crystal structure of Tb2Ti2O7 using single crystal neutron diffraction data from HEiDi at FRM II.""" -project.save_as('projects/tbti_heidi') +project.save_as(dir_path='projects/ed_14_tbti_heidi') # %% [markdown] # ## 🧩 Define Structure diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py index 2fcf91934..ebe7310a2 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -140,3 +140,9 @@ # %% structure.show_as_cif() + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_15_taurine_senju') diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py index 89e60625f..8fb264927 100644 --- a/docs/docs/tutorials/ed-16.py +++ b/docs/docs/tutorials/ed-16.py @@ -254,3 +254,9 @@ # %% project.display.pattern(expt_name='nomad') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_16_si_bragg_pdf') diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index eb3605e9d..ae2090132 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -29,7 +29,7 @@ # results can be written to `analysis/results.csv`. # %% -project.save_as(dir_path='projects/cosio_d20_scan') +project.save_as(dir_path='projects/ed_17_cosio_d20_scan') # %% [markdown] # ## 🧩 Define Structure diff --git a/docs/docs/tutorials/ed-18.py b/docs/docs/tutorials/ed-18.py index f3caa2e09..c65c0243f 100644 --- a/docs/docs/tutorials/ed-18.py +++ b/docs/docs/tutorials/ed-18.py @@ -59,3 +59,9 @@ # %% project.display.pattern(expt_name='hrpt') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_18_lbco_hrpt') diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py index 008ca5d0f..521f689e6 100644 --- a/docs/docs/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -234,4 +234,4 @@ # ## 💾 Save Project # %% -project.save_as('projects/lbco_hrpt') +project.save_as(dir_path='projects/ed_2_lbco_hrpt') diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py index 5b4b6fe03..78345a210 100644 --- a/docs/docs/tutorials/ed-20.py +++ b/docs/docs/tutorials/ed-20.py @@ -223,7 +223,7 @@ # %% project = Project(name='beer') -project.save_as(dir_path='projects/beer_mcstas') +project.save_as(dir_path='projects/ed_20_beer_mcstas') # %% [markdown] # ### Add Structures diff --git a/docs/docs/tutorials/ed-23.py b/docs/docs/tutorials/ed-23.py index f7e01bc66..59ee1c6ef 100644 --- a/docs/docs/tutorials/ed-23.py +++ b/docs/docs/tutorials/ed-23.py @@ -102,3 +102,9 @@ # %% project.display.fit.series(versus=temperature) + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_23_cosio_d20_scan') diff --git a/docs/docs/tutorials/ed-24.py b/docs/docs/tutorials/ed-24.py index 95cfff7d7..984706474 100644 --- a/docs/docs/tutorials/ed-24.py +++ b/docs/docs/tutorials/ed-24.py @@ -94,3 +94,9 @@ # %% project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93) + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_24_lbco_hrpt_bumps_dream') diff --git a/docs/docs/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py index 6b1c06dc1..102be7af3 100644 --- a/docs/docs/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -58,7 +58,7 @@ # directory path. # %% -project.save_as(dir_path='projects/lbco_hrpt') +project.save_as(dir_path='projects/ed_3_lbco_hrpt') # %% [markdown] # ## 🧩 Define Structure diff --git a/docs/docs/tutorials/ed-4.py b/docs/docs/tutorials/ed-4.py index 0e974893a..633a93b7f 100644 --- a/docs/docs/tutorials/ed-4.py +++ b/docs/docs/tutorials/ed-4.py @@ -320,3 +320,9 @@ # %% project.display.pattern(expt_name='xrd', x_min=29.0, x_max=30.4) + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_4_pbso4_joint') diff --git a/docs/docs/tutorials/ed-5.py b/docs/docs/tutorials/ed-5.py index 942444d25..35a1bf765 100644 --- a/docs/docs/tutorials/ed-5.py +++ b/docs/docs/tutorials/ed-5.py @@ -174,7 +174,7 @@ project = Project() # %% -project.save_as('projects/cosio_d20') +project.save_as(dir_path='projects/ed_5_cosio_d20') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-6.py b/docs/docs/tutorials/ed-6.py index f2968b774..20068a7ef 100644 --- a/docs/docs/tutorials/ed-6.py +++ b/docs/docs/tutorials/ed-6.py @@ -344,3 +344,9 @@ # # The HTML report is written automatically when the project is saved; # enable `project.report.pdf` as well for a PDF version. + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_6_hs_hrpt') diff --git a/docs/docs/tutorials/ed-7.py b/docs/docs/tutorials/ed-7.py index a8d568805..b7ce6ec44 100644 --- a/docs/docs/tutorials/ed-7.py +++ b/docs/docs/tutorials/ed-7.py @@ -356,3 +356,9 @@ # %% project.display.pattern(expt_name='sepd', x='d_spacing') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_7_si_sepd') diff --git a/docs/docs/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py index d875c3998..f049bea2b 100644 --- a/docs/docs/tutorials/ed-8.py +++ b/docs/docs/tutorials/ed-8.py @@ -360,3 +360,9 @@ # # The HTML report is written automatically when the project is saved; # enable `project.report.pdf` as well for a PDF version. + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_8_ncaf_wish') diff --git a/docs/docs/tutorials/ed-9.py b/docs/docs/tutorials/ed-9.py index 4fe451af2..6308a0b8c 100644 --- a/docs/docs/tutorials/ed-9.py +++ b/docs/docs/tutorials/ed-9.py @@ -304,3 +304,9 @@ # %% project.display.pattern(expt_name='mcstas') + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_9_lbco_si_mcstas') From 32b3151289c24ceb866679653720eecbd7e2f971 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 17:11:06 +0200 Subject: [PATCH 02/57] Seed and standardize save paths for Bayesian tutorials --- docs/docs/tutorials/ed-21.py | 3 ++- docs/docs/tutorials/ed-22.py | 3 ++- docs/docs/tutorials/ed-25.py | 3 ++- docs/docs/tutorials/ed-26.py | 7 +++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index efce4cacd..2676f9d7f 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -41,7 +41,7 @@ project = ed.Project() # %% -project.save_as('projects/lbco_hrpt_bumps-dream') +project.save_as(dir_path='projects/ed_21_lbco_hrpt_bumps_dream') # %% [markdown] # ## 🧩 Define Structure @@ -306,6 +306,7 @@ # %% project.analysis.minimizer.sampling_steps = 100 # lower than the default 3000 project.analysis.minimizer.burn_in_steps = 20 # lower than the default 600 +project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output # %% project.analysis.fit() diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index e1b3f8abe..79cd24163 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -38,7 +38,7 @@ project = ed.Project() # %% -project.save_as('projects/tbti_heidi_emcee') +project.save_as(dir_path='projects/ed_22_tbti_heidi_emcee') # %% [markdown] # ## 🧩 Define Structure @@ -229,6 +229,7 @@ project.analysis.minimizer.sampling_steps = 500 # lower than the default 3000 project.analysis.minimizer.burn_in_steps = 100 # lower than the default 600 project.analysis.minimizer.population_size = 16 # lower than the default 32 +project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output # %% project.analysis.fit() diff --git a/docs/docs/tutorials/ed-25.py b/docs/docs/tutorials/ed-25.py index 8806d05cc..746f5c13b 100644 --- a/docs/docs/tutorials/ed-25.py +++ b/docs/docs/tutorials/ed-25.py @@ -41,7 +41,7 @@ project = ed.Project() # %% -project.save_as('projects/lbco_hrpt_emcee') +project.save_as(dir_path='projects/ed_25_lbco_hrpt_emcee') # %% [markdown] # ## 🧩 Define Structure @@ -299,6 +299,7 @@ project.analysis.minimizer.sampling_steps = 100 # lower than the default 5000 project.analysis.minimizer.burn_in_steps = 20 # lower than the default 1000 project.analysis.minimizer.population_size = 16 # lower than the default 32 +project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output # %% project.analysis.fit() diff --git a/docs/docs/tutorials/ed-26.py b/docs/docs/tutorials/ed-26.py index c3d994d62..d3d9be817 100644 --- a/docs/docs/tutorials/ed-26.py +++ b/docs/docs/tutorials/ed-26.py @@ -124,6 +124,7 @@ # convergence and better posterior resolution. # %% +project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output project.analysis.fit(resume=True, extra_steps=100) # %% @@ -142,3 +143,9 @@ # %% project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93) + +# %% [markdown] +# ## 💾 Save Project + +# %% +project.save_as(dir_path='projects/ed_26_lbco_hrpt_emcee') From eb9ff5e9e9234537845c5c51ab4debc42a492eb5 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 17:11:48 +0200 Subject: [PATCH 03/57] Regenerate tutorial notebooks --- docs/docs/tutorials/ed-1.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-10.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-11.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-12.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-13.ipynb | 4 ++-- docs/docs/tutorials/ed-14.ipynb | 2 +- docs/docs/tutorials/ed-15.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-16.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-17.ipynb | 2 +- docs/docs/tutorials/ed-18.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-2.ipynb | 2 +- docs/docs/tutorials/ed-20.ipynb | 2 +- docs/docs/tutorials/ed-21.ipynb | 5 +++-- docs/docs/tutorials/ed-22.ipynb | 5 +++-- docs/docs/tutorials/ed-23.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-24.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-25.ipynb | 5 +++-- docs/docs/tutorials/ed-26.ipynb | 19 +++++++++++++++++++ docs/docs/tutorials/ed-3.ipynb | 2 +- docs/docs/tutorials/ed-4.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-5.ipynb | 2 +- docs/docs/tutorials/ed-6.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-7.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-8.ipynb | 18 ++++++++++++++++++ docs/docs/tutorials/ed-9.ipynb | 18 ++++++++++++++++++ 25 files changed, 288 insertions(+), 14 deletions(-) diff --git a/docs/docs/tutorials/ed-1.ipynb b/docs/docs/tutorials/ed-1.ipynb index 39de4d787..17a9083d2 100644 --- a/docs/docs/tutorials/ed-1.ipynb +++ b/docs/docs/tutorials/ed-1.ipynb @@ -288,6 +288,24 @@ "# Plot measured vs. calculated diffraction patterns\n", "project.display.pattern(expt_name='hrpt')" ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_1_lbco_hrpt')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-10.ipynb b/docs/docs/tutorials/ed-10.ipynb index 3815f24ea..84197148d 100644 --- a/docs/docs/tutorials/ed-10.ipynb +++ b/docs/docs/tutorials/ed-10.ipynb @@ -261,6 +261,24 @@ "source": [ "project.display.pattern(expt_name='pdf')" ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_10_ni_pdf')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-11.ipynb b/docs/docs/tutorials/ed-11.ipynb index b5d03cf56..582c1ae85 100644 --- a/docs/docs/tutorials/ed-11.ipynb +++ b/docs/docs/tutorials/ed-11.ipynb @@ -291,6 +291,24 @@ "source": [ "project.display.pattern(expt_name='nomad', include=('measured', 'calculated'))" ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_11_si_nomad_pdf')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-12.ipynb b/docs/docs/tutorials/ed-12.ipynb index ed2c0a0b5..be3925044 100644 --- a/docs/docs/tutorials/ed-12.ipynb +++ b/docs/docs/tutorials/ed-12.ipynb @@ -331,6 +331,24 @@ "source": [ "project.display.pattern(expt_name='xray_pdf')" ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_12_nacl_xray_pdf')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index 9a84c90cd..d78d659d9 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -1199,7 +1199,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.save_as(dir_path='data/powder_diffraction_Si')" + "project_1.save_as(dir_path='projects/ed_13_si')" ] }, { @@ -2631,7 +2631,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.save_as(dir_path='data/powder_diffraction_LBCO_Si')" + "project_2.save_as(dir_path='projects/ed_13_lbco_si')" ] }, { diff --git a/docs/docs/tutorials/ed-14.ipynb b/docs/docs/tutorials/ed-14.ipynb index 2c744b0da..b8b6f4b9a 100644 --- a/docs/docs/tutorials/ed-14.ipynb +++ b/docs/docs/tutorials/ed-14.ipynb @@ -70,7 +70,7 @@ "refinement of the crystal structure of Tb2Ti2O7 using single crystal \n", "neutron diffraction data from HEiDi at FRM II.\"\"\"\n", "\n", - "project.save_as('projects/tbti_heidi')" + "project.save_as(dir_path='projects/ed_14_tbti_heidi')" ] }, { diff --git a/docs/docs/tutorials/ed-15.ipynb b/docs/docs/tutorials/ed-15.ipynb index 821429bd9..672c429f1 100644 --- a/docs/docs/tutorials/ed-15.ipynb +++ b/docs/docs/tutorials/ed-15.ipynb @@ -425,6 +425,24 @@ "source": [ "structure.show_as_cif()" ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_15_taurine_senju')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb index 09c816f05..9bf0988d1 100644 --- a/docs/docs/tutorials/ed-16.ipynb +++ b/docs/docs/tutorials/ed-16.ipynb @@ -618,6 +618,24 @@ "source": [ "project.display.pattern(expt_name='nomad')" ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_16_si_bragg_pdf')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb index 351290746..8bc7ab0e7 100644 --- a/docs/docs/tutorials/ed-17.ipynb +++ b/docs/docs/tutorials/ed-17.ipynb @@ -90,7 +90,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as(dir_path='projects/cosio_d20_scan')" + "project.save_as(dir_path='projects/ed_17_cosio_d20_scan')" ] }, { diff --git a/docs/docs/tutorials/ed-18.ipynb b/docs/docs/tutorials/ed-18.ipynb index 86d6cb69d..0299b4487 100644 --- a/docs/docs/tutorials/ed-18.ipynb +++ b/docs/docs/tutorials/ed-18.ipynb @@ -181,6 +181,24 @@ "source": [ "project.display.pattern(expt_name='hrpt')" ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_18_lbco_hrpt')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb index 9b1872b5b..f4973448c 100644 --- a/docs/docs/tutorials/ed-2.ipynb +++ b/docs/docs/tutorials/ed-2.ipynb @@ -549,7 +549,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('projects/lbco_hrpt')" + "project.save_as(dir_path='projects/ed_2_lbco_hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb index 6dc22803e..f27b9c986 100644 --- a/docs/docs/tutorials/ed-20.ipynb +++ b/docs/docs/tutorials/ed-20.ipynb @@ -457,7 +457,7 @@ "outputs": [], "source": [ "project = Project(name='beer')\n", - "project.save_as(dir_path='projects/beer_mcstas')" + "project.save_as(dir_path='projects/ed_20_beer_mcstas')" ] }, { diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index 697851df6..a6616b381 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -97,7 +97,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('projects/lbco_hrpt_bumps-dream')" + "project.save_as(dir_path='projects/ed_21_lbco_hrpt_bumps_dream')" ] }, { @@ -637,7 +637,8 @@ "outputs": [], "source": [ "project.analysis.minimizer.sampling_steps = 100 # lower than the default 3000\n", - "project.analysis.minimizer.burn_in_steps = 20 # lower than the default 600" + "project.analysis.minimizer.burn_in_steps = 20 # lower than the default 600\n", + "project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output" ] }, { diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb index 1a70e189e..a97ddebaa 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -94,7 +94,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('projects/tbti_heidi_emcee')" + "project.save_as(dir_path='projects/ed_22_tbti_heidi_emcee')" ] }, { @@ -510,7 +510,8 @@ "source": [ "project.analysis.minimizer.sampling_steps = 500 # lower than the default 3000\n", "project.analysis.minimizer.burn_in_steps = 100 # lower than the default 600\n", - "project.analysis.minimizer.population_size = 16 # lower than the default 32" + "project.analysis.minimizer.population_size = 16 # lower than the default 32\n", + "project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output" ] }, { diff --git a/docs/docs/tutorials/ed-23.ipynb b/docs/docs/tutorials/ed-23.ipynb index 368150181..fcca74451 100644 --- a/docs/docs/tutorials/ed-23.ipynb +++ b/docs/docs/tutorials/ed-23.ipynb @@ -258,6 +258,24 @@ "source": [ "project.display.fit.series(versus=temperature)" ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_23_cosio_d20_scan')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-24.ipynb b/docs/docs/tutorials/ed-24.ipynb index e21fd7ed6..b4dbb1fee 100644 --- a/docs/docs/tutorials/ed-24.ipynb +++ b/docs/docs/tutorials/ed-24.ipynb @@ -245,6 +245,24 @@ "source": [ "project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93)" ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_24_lbco_hrpt_bumps_dream')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-25.ipynb b/docs/docs/tutorials/ed-25.ipynb index 7f94ff8d9..373a68287 100644 --- a/docs/docs/tutorials/ed-25.ipynb +++ b/docs/docs/tutorials/ed-25.ipynb @@ -97,7 +97,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('projects/lbco_hrpt_emcee')" + "project.save_as(dir_path='projects/ed_25_lbco_hrpt_emcee')" ] }, { @@ -623,7 +623,8 @@ "source": [ "project.analysis.minimizer.sampling_steps = 100 # lower than the default 5000\n", "project.analysis.minimizer.burn_in_steps = 20 # lower than the default 1000\n", - "project.analysis.minimizer.population_size = 16 # lower than the default 32" + "project.analysis.minimizer.population_size = 16 # lower than the default 32\n", + "project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output" ] }, { diff --git a/docs/docs/tutorials/ed-26.ipynb b/docs/docs/tutorials/ed-26.ipynb index f9a778aa3..1e4460336 100644 --- a/docs/docs/tutorials/ed-26.ipynb +++ b/docs/docs/tutorials/ed-26.ipynb @@ -291,6 +291,7 @@ "metadata": {}, "outputs": [], "source": [ + "project.analysis.minimizer.random_seed = 42 # fixed seed for reproducible output\n", "project.analysis.fit(resume=True, extra_steps=100)" ] }, @@ -343,6 +344,24 @@ "source": [ "project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93)" ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_26_lbco_hrpt_emcee')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index cce439297..d97820d47 100644 --- a/docs/docs/tutorials/ed-3.ipynb +++ b/docs/docs/tutorials/ed-3.ipynb @@ -148,7 +148,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as(dir_path='projects/lbco_hrpt')" + "project.save_as(dir_path='projects/ed_3_lbco_hrpt')" ] }, { diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb index c45ed29cf..4ee50ba35 100644 --- a/docs/docs/tutorials/ed-4.ipynb +++ b/docs/docs/tutorials/ed-4.ipynb @@ -712,6 +712,24 @@ "source": [ "project.display.pattern(expt_name='xrd', x_min=29.0, x_max=30.4)" ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_4_pbso4_joint')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-5.ipynb b/docs/docs/tutorials/ed-5.ipynb index 391ca3651..3c2eeffd9 100644 --- a/docs/docs/tutorials/ed-5.ipynb +++ b/docs/docs/tutorials/ed-5.ipynb @@ -371,7 +371,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('projects/cosio_d20')" + "project.save_as(dir_path='projects/ed_5_cosio_d20')" ] }, { diff --git a/docs/docs/tutorials/ed-6.ipynb b/docs/docs/tutorials/ed-6.ipynb index 561afb750..b763ff164 100644 --- a/docs/docs/tutorials/ed-6.ipynb +++ b/docs/docs/tutorials/ed-6.ipynb @@ -830,6 +830,24 @@ "The HTML report is written automatically when the project is saved;\n", "enable `project.report.pdf` as well for a PDF version." ] + }, + { + "cell_type": "markdown", + "id": "77", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_6_hs_hrpt')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-7.ipynb b/docs/docs/tutorials/ed-7.ipynb index e0902a8e7..66bd528d9 100644 --- a/docs/docs/tutorials/ed-7.ipynb +++ b/docs/docs/tutorials/ed-7.ipynb @@ -926,6 +926,24 @@ "source": [ "project.display.pattern(expt_name='sepd', x='d_spacing')" ] + }, + { + "cell_type": "markdown", + "id": "91", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_7_si_sepd')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb index 7e02e5770..70d147f0f 100644 --- a/docs/docs/tutorials/ed-8.ipynb +++ b/docs/docs/tutorials/ed-8.ipynb @@ -713,6 +713,24 @@ "The HTML report is written automatically when the project is saved;\n", "enable `project.report.pdf` as well for a PDF version." ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_8_ncaf_wish')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-9.ipynb b/docs/docs/tutorials/ed-9.ipynb index 8d1ecdfee..4daf26a2a 100644 --- a/docs/docs/tutorials/ed-9.ipynb +++ b/docs/docs/tutorials/ed-9.ipynb @@ -695,6 +695,24 @@ "source": [ "project.display.pattern(expt_name='mcstas')" ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "## 💾 Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='projects/ed_9_lbco_si_mcstas')" + ] } ], "metadata": { From f40054e1578e231d376793fe708d98d753c502b9 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 17:27:49 +0200 Subject: [PATCH 04/57] Add tutorial-output regression test against fit baseline --- pixi.toml | 9 + tests/tutorials/analysis_cif_reader.py | 127 +++++++++++ tests/tutorials/baseline.json | 265 +++++++++++++++++++++++ tests/tutorials/conftest.py | 15 ++ tests/tutorials/generate_baseline.py | 117 ++++++++++ tests/tutorials/test_tutorial_outputs.py | 94 ++++++++ 6 files changed, 627 insertions(+) create mode 100644 tests/tutorials/analysis_cif_reader.py create mode 100644 tests/tutorials/baseline.json create mode 100644 tests/tutorials/conftest.py create mode 100644 tests/tutorials/generate_baseline.py create mode 100644 tests/tutorials/test_tutorial_outputs.py diff --git a/pixi.toml b/pixi.toml index bb0f0b629..698b29223 100644 --- a/pixi.toml +++ b/pixi.toml @@ -109,6 +109,15 @@ integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' script-tests = { cmd = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } notebook-tests = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +# Parse the analysis.cif of every saved tutorial project and assert its +# refined fit results against tests/tutorials/baseline.json. Run only after +# the tutorials have been executed (script-tests or notebook-tests), so the +# saved projects exist under tmp/tutorials/projects/. +tutorial-output-tests = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +# Run the tutorials and then verify their saved outputs in a single step. +script-tests-checked = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', depends-on = ['script-tests'], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +notebook-tests-checked = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', depends-on = ['notebook-tests'], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } + test = { depends-on = ['unit-tests', 'functional-tests'] } test-all = { depends-on = [ 'unit-tests', diff --git a/tests/tutorials/analysis_cif_reader.py b/tests/tutorials/analysis_cif_reader.py new file mode 100644 index 000000000..9fcd98bd0 --- /dev/null +++ b/tests/tutorials/analysis_cif_reader.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Minimal reader for the fit results stored in a project ``analysis.cif``. + +The tutorial-output tests read the persisted ``analysis/analysis.cif`` of +each saved tutorial project and compare a few fit-quality metrics and +refined parameter values against a committed baseline. Only the two +blocks needed for that comparison are parsed: the scalar +``_fit_result.*`` entries and the ``_fit_parameter`` loop. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +_FIT_RESULT_PREFIX = '_fit_result.' +_FIT_PARAMETER_NAME_TAG = '_fit_parameter.param_unique_name' +_BAYESIAN_VALUE_COLUMN = 'posterior_median' +_DETERMINISTIC_VALUE_COLUMN = 'start_value' + + +def _unquote(value: str) -> str: + """Strip a single pair of surrounding single or double quotes.""" + if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: + return value[1:-1] + return value + + +@dataclass(frozen=True) +class AnalysisCif: + """Parsed fit results from a project ``analysis.cif`` file.""" + + fit_result: dict[str, str] + fit_parameters: dict[str, dict[str, str]] + + @property + def result_kind(self) -> str | None: + """Return ``'deterministic'`` or ``'bayesian'`` when recorded.""" + return self.fit_result.get('result_kind') + + def scalar(self, name: str) -> float | None: + """Return a ``_fit_result`` scalar as a float, or ``None``.""" + raw = self.fit_result.get(name) + if raw is None: + return None + try: + return float(raw) + except ValueError: + return None + + def parameter_value(self, name: str) -> float: + """Return the refined value of a named parameter. + + Bayesian fits use ``posterior_median``; deterministic fits use + ``start_value`` (the value held after refinement). + """ + columns = self.fit_parameters[name] + if self.result_kind == 'bayesian' and _BAYESIAN_VALUE_COLUMN in columns: + return float(columns[_BAYESIAN_VALUE_COLUMN]) + return float(columns[_DETERMINISTIC_VALUE_COLUMN]) + + +def _read_loop(lines: list[str], start: int) -> tuple[int, list[str], list[str]]: + """Read one ``loop_`` block, returning the next index, tags, rows.""" + index = start + count = len(lines) + tags: list[str] = [] + while index < count and lines[index].strip().startswith('_'): + tags.append(lines[index].strip()) + index += 1 + rows: list[str] = [] + while index < count: + stripped = lines[index].strip() + if not stripped or stripped == 'loop_' or stripped.startswith('_'): + break + rows.append(stripped) + index += 1 + return index, tags, rows + + +def _store_fit_parameters( + tags: list[str], + rows: list[str], + fit_parameters: dict[str, dict[str, str]], +) -> None: + """Populate ``fit_parameters`` from a ``_fit_parameter`` loop.""" + column_names = [tag.split('.', 1)[1] for tag in tags] + for row in rows: + tokens = row.split() + if len(tokens) != len(column_names): + continue + fit_parameters[tokens[0]] = dict(zip(column_names, tokens, strict=True)) + + +def parse_analysis_cif(text: str) -> AnalysisCif: + """Parse ``_fit_result`` scalars and the ``_fit_parameter`` loop.""" + fit_result: dict[str, str] = {} + fit_parameters: dict[str, dict[str, str]] = {} + + lines = text.splitlines() + index = 0 + count = len(lines) + while index < count: + stripped = lines[index].strip() + if not stripped: + index += 1 + continue + if stripped == 'loop_': + index, tags, rows = _read_loop(lines, index + 1) + if tags and tags[0] == _FIT_PARAMETER_NAME_TAG: + _store_fit_parameters(tags, rows, fit_parameters) + continue + if stripped.startswith(_FIT_RESULT_PREFIX): + key, _, value = stripped.partition(' ') + fit_result[key.removeprefix(_FIT_RESULT_PREFIX)] = _unquote(value.strip()) + index += 1 + + return AnalysisCif(fit_result=fit_result, fit_parameters=fit_parameters) + + +def read_analysis_cif(path: Path) -> AnalysisCif: + """Parse the ``analysis.cif`` file at *path*.""" + return parse_analysis_cif(path.read_text(encoding='utf-8')) diff --git a/tests/tutorials/baseline.json b/tests/tutorials/baseline.json new file mode 100644 index 000000000..2b5f9b9b3 --- /dev/null +++ b/tests/tutorials/baseline.json @@ -0,0 +1,265 @@ +{ + "ed_10_ni_pdf": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 207.102333, + "R_factor_all": 0.098321, + "wR_factor_all": 0.09479, + "parameters": { + "ni.cell.length_a": 3.52387, + "pdf.linked_phases.ni.scale": 1.0 + } + }, + "ed_11_si_nomad_pdf": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 170.543161, + "R_factor_all": 0.084011, + "wR_factor_all": 0.082977, + "parameters": { + "si.cell.length_a": 5.43146, + "nomad.linked_phases.si.scale": 1.0 + } + }, + "ed_12_nacl_xray_pdf": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.476045, + "R_factor_all": 0.110217, + "wR_factor_all": 0.113817, + "parameters": { + "nacl.cell.length_a": 5.62, + "xray_pdf.linked_phases.nacl.scale": 0.5 + } + }, + "ed_13_lbco_si": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.537656, + "R_factor_all": 0.046203, + "wR_factor_all": 0.05534, + "parameters": { + "lbco.cell.length_a": 3.891469, + "sim_lbco.linked_phases.lbco.scale": 4.829531, + "sim_lbco.linked_phases.si.scale": 1.0 + } + }, + "ed_13_si": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 2.30651, + "R_factor_all": 0.068706, + "wR_factor_all": 0.091774, + "parameters": { + "sim_si.linked_phases.si.scale": 1.0, + "sim_si.peak.rise_alpha_0": -0.0055 + } + }, + "ed_14_tbti_heidi": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 2.944513, + "R_factor_all": 0.042052, + "wR_factor_all": 0.042544, + "parameters": { + "tbti.atom_site.Ti.occupancy": 0.972996, + "heidi.linked_crystal.scale": 2.932881 + } + }, + "ed_15_taurine_senju": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 12.192189, + "R_factor_all": 0.133438, + "wR_factor_all": 0.084877, + "parameters": { + "taurine.atom_site.S1.fract_x": 0.201623, + "senju.linked_crystal.scale": 1.348755 + } + }, + "ed_16_si_bragg_pdf": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 51.874863, + "R_factor_all": 0.104928, + "wR_factor_all": 0.082876, + "parameters": { + "si.cell.length_a": 5.42, + "sepd.linked_phases.si.scale": 13.0, + "nomad.linked_phases.si.scale": 1.0 + } + }, + "ed_18_lbco_hrpt": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.289805, + "R_factor_all": 0.056345, + "wR_factor_all": 0.072095, + "parameters": { + "lbco.cell.length_a": 3.890868, + "hrpt.linked_phases.lbco.scale": 9.135 + } + }, + "ed_1_lbco_hrpt": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.290457, + "R_factor_all": 0.056249, + "wR_factor_all": 0.072055, + "parameters": { + "lbco.cell.length_a": 3.890868, + "hrpt.linked_phases.lbco.scale": 9.134916 + } + }, + "ed_20_beer_mcstas": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 13.661849, + "R_factor_all": 0.049609, + "wR_factor_all": 0.071233, + "parameters": { + "expt_s2.linked_phases.ferrite.scale": 50.358734, + "expt_s2.linked_phases.austenite.scale": 11.985173 + } + }, + "ed_21_lbco_hrpt_bumps_dream": { + "result_kind": "bayesian", + "rtol": 0.1, + "reduced_chi_square": 1.289863, + "parameters": { + "lbco.cell.length_a": 3.891316, + "hrpt.linked_phases.lbco.scale": 9.134207 + } + }, + "ed_22_tbti_heidi_emcee": { + "result_kind": "bayesian", + "rtol": 0.1, + "reduced_chi_square": 12.715296, + "parameters": { + "tbti.atom_site.Tb.adp_iso": 0.532568, + "heidi.linked_crystal.scale": 2.924634 + } + }, + "ed_23_cosio_d20_scan": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 53.079845, + "R_factor_all": 0.078802, + "wR_factor_all": 0.137043, + "parameters": { + "cosio.cell.length_a": 10.30627, + "cosio.cell.length_b": 6.00142, + "cosio.cell.length_c": 4.78691, + "d20.linked_phases.cosio.scale": 1.359 + } + }, + "ed_24_lbco_hrpt_bumps_dream": { + "result_kind": "bayesian", + "rtol": 0.1, + "reduced_chi_square": 1.289855, + "parameters": { + "lbco.cell.length_a": 3.891321, + "hrpt.linked_phases.lbco.scale": 9.132917 + } + }, + "ed_25_lbco_hrpt_emcee": { + "result_kind": "bayesian", + "rtol": 0.1, + "reduced_chi_square": 1.289948, + "parameters": { + "lbco.cell.length_a": 3.891324, + "hrpt.linked_phases.lbco.scale": 9.139069 + } + }, + "ed_26_lbco_hrpt_emcee": { + "result_kind": "bayesian", + "rtol": 0.1, + "reduced_chi_square": 1.289891, + "parameters": { + "lbco.cell.length_a": 3.89132, + "hrpt.linked_phases.lbco.scale": 9.132873 + } + }, + "ed_2_lbco_hrpt": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.285348, + "R_factor_all": 0.056189, + "wR_factor_all": 0.07197, + "parameters": { + "lbco.cell.length_a": 3.890868, + "hrpt.linked_phases.lbco.scale": 9.13509 + } + }, + "ed_3_lbco_hrpt": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.28275, + "R_factor_all": 0.056132, + "wR_factor_all": 0.071886, + "parameters": { + "lbco.cell.length_a": 3.890868, + "hrpt.linked_phases.lbco.scale": 9.13509 + } + }, + "ed_5_cosio_d20": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 4.380305, + "R_factor_all": 0.029898, + "wR_factor_all": 0.03934, + "parameters": { + "cosio.cell.length_a": 10.3, + "cosio.cell.length_b": 6.0, + "cosio.cell.length_c": 4.8, + "d20.linked_phases.cosio.scale": 1.0 + } + }, + "ed_6_hs_hrpt": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 1.970653, + "R_factor_all": 0.0403, + "wR_factor_all": 0.050734, + "parameters": { + "hs.cell.length_a": 6.862265, + "hs.cell.length_c": 14.135746, + "hrpt.linked_phases.hs.scale": 0.398847 + } + }, + "ed_7_si_sepd": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 2.663955, + "R_factor_all": 0.08004, + "wR_factor_all": 0.059687, + "parameters": { + "si.cell.length_a": 5.432483, + "sepd.linked_phases.si.scale": 14.969272 + } + }, + "ed_8_ncaf_wish": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 15.486737, + "R_factor_all": 0.069775, + "wR_factor_all": 0.081356, + "parameters": { + "wish_5_6.linked_phases.ncaf.scale": 1.0, + "wish_4_7.linked_phases.ncaf.scale": 2.0 + } + }, + "ed_9_lbco_si_mcstas": { + "result_kind": "deterministic", + "rtol": 0.02, + "reduced_chi_square": 9.532167, + "R_factor_all": 0.058084, + "wR_factor_all": 0.076338, + "parameters": { + "lbco.cell.length_a": 3.8909, + "si.cell.length_a": 5.43146, + "mcstas.linked_phases.lbco.scale": 4.0, + "mcstas.linked_phases.si.scale": 0.2 + } + } +} diff --git a/tests/tutorials/conftest.py b/tests/tutorials/conftest.py new file mode 100644 index 000000000..011ed51ec --- /dev/null +++ b/tests/tutorials/conftest.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Test configuration for the tutorial-output checks. + +Pytest runs with ``--import-mode=importlib``, which does not add the +test directory to ``sys.path``. Insert it here so the test module can +import the sibling ``analysis_cif_reader`` helper. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) diff --git a/tests/tutorials/generate_baseline.py b/tests/tutorials/generate_baseline.py new file mode 100644 index 000000000..46d96eab7 --- /dev/null +++ b/tests/tutorials/generate_baseline.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Generate the committed baseline for the tutorial-output tests. + +Run the tutorials first (``pixi run script-tests`` or +``pixi run notebook-tests``) so each saved project exists under +``/projects/``. Then run this script to (re)write +``baseline.json`` from the freshly produced ``analysis.cif`` files:: + + pixi run python tests/tutorials/generate_baseline.py + +Review the resulting diff before committing; tweak per-tutorial +tolerances or tracked parameters by editing ``baseline.json`` directly. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +from analysis_cif_reader import AnalysisCif +from analysis_cif_reader import read_analysis_cif + +# Relative tolerances used when comparing against the baseline. Bayesian +# (MCMC) fits are seeded but still vary slightly more than deterministic +# refinements, so they get a looser bound. +DETERMINISTIC_RTOL = 0.02 +BAYESIAN_RTOL = 0.10 + +# Optional deterministic fit-quality scalars to track when present. +OPTIONAL_SCALARS = ('R_factor_all', 'wR_factor_all') + +# Number of refined parameters to track per tutorial (cell lengths and +# phase scales preferred, topped up from the front of the loop). +KEY_PARAMETER_COUNT = 2 +ROUND_DIGITS = 6 + +REPO_ROOT = Path(__file__).resolve().parents[2] +BASELINE_PATH = Path(__file__).resolve().parent / 'baseline.json' + + +def artifact_root() -> Path: + """Return the configured tutorial artifact root directory.""" + configured = os.environ.get('EASYDIFFRACTION_ARTIFACT_ROOT') + if configured: + return Path(configured) if Path(configured).is_absolute() else REPO_ROOT / configured + return REPO_ROOT / 'tmp' / 'tutorials' + + +def _is_key_parameter(name: str) -> bool: + """Return whether *name* is a lattice length or a phase scale.""" + return '.cell.length_' in name or name.endswith('.scale') + + +def select_key_parameters(cif: AnalysisCif) -> dict[str, float]: + """Return the tracked refined parameter values for one project.""" + names = list(cif.fit_parameters) + selected = [name for name in names if _is_key_parameter(name)] + for name in names: + if len(selected) >= KEY_PARAMETER_COUNT: + break + if name not in selected: + selected.append(name) + ordered = [name for name in names if name in set(selected)] + return {name: round(cif.parameter_value(name), ROUND_DIGITS) for name in ordered} + + +def build_entry(cif: AnalysisCif) -> dict | None: + """Build a baseline entry, or ``None`` if the project has no fit.""" + reduced_chi_square = cif.scalar('reduced_chi_square') + if reduced_chi_square is None or reduced_chi_square <= 0: + return None + + kind = cif.result_kind or 'deterministic' + entry: dict = { + 'result_kind': kind, + 'rtol': BAYESIAN_RTOL if kind == 'bayesian' else DETERMINISTIC_RTOL, + 'reduced_chi_square': round(reduced_chi_square, ROUND_DIGITS), + } + for scalar_name in OPTIONAL_SCALARS: + value = cif.scalar(scalar_name) + if value is not None: + entry[scalar_name] = round(value, ROUND_DIGITS) + entry['parameters'] = select_key_parameters(cif) + return entry + + +def collect_baseline(root: Path) -> dict[str, dict]: + """Build baseline entries for every saved project under *root*.""" + projects_dir = root / 'projects' + baseline: dict[str, dict] = {} + for cif_path in sorted(projects_dir.glob('*/analysis/analysis.cif')): + name = cif_path.parents[1].name + if not name.startswith('ed_'): + continue + entry = build_entry(read_analysis_cif(cif_path)) + if entry is not None: + baseline[name] = entry + return baseline + + +def main() -> None: + """Write ``baseline.json`` from the current tutorial artifacts.""" + root = artifact_root() + baseline = collect_baseline(root) + if not baseline: + msg = f'No tutorial projects with fit results found under {root / "projects"}.' + raise SystemExit(msg) + + ordered = {name: baseline[name] for name in sorted(baseline)} + BASELINE_PATH.write_text(json.dumps(ordered, indent=2) + '\n', encoding='utf-8') + print(f'Wrote {len(ordered)} tutorial baselines to {BASELINE_PATH}') + + +if __name__ == '__main__': + main() diff --git a/tests/tutorials/test_tutorial_outputs.py b/tests/tutorials/test_tutorial_outputs.py new file mode 100644 index 000000000..6ee533045 --- /dev/null +++ b/tests/tutorials/test_tutorial_outputs.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Assert that saved tutorial projects reproduce expected fit results. + +This module runs *after* the tutorials have been executed (as scripts +via ``pixi run script-tests`` or as notebooks via +``pixi run notebook-tests``). Each tutorial saves its project under +``/projects/ed__/``; here we parse every +``analysis/analysis.cif`` and compare its fit-quality metrics and a few +refined parameter values against the committed ``baseline.json``. + +If no tutorial artifacts are present the whole module is skipped, so the +file is safe to collect in a plain ``pytest`` run that did not first +execute the tutorials. +""" + +from __future__ import annotations + +import json +import math +from pathlib import Path + +import pytest + +from analysis_cif_reader import read_analysis_cif +from generate_baseline import artifact_root + +BASELINE = json.loads((Path(__file__).parent / 'baseline.json').read_text(encoding='utf-8')) + +# Absolute tolerance floor, relevant only for values close to zero. +ABS_TOL = 1e-8 + + +def _analysis_cif_path(name: str) -> Path: + """Return the ``analysis.cif`` path for a saved tutorial project.""" + return artifact_root() / 'projects' / name / 'analysis' / 'analysis.cif' + + +def _artifacts_present() -> bool: + """Return whether any tutorial project has been saved.""" + projects_dir = artifact_root() / 'projects' + return projects_dir.is_dir() and any(projects_dir.glob('ed_*/analysis/analysis.cif')) + + +pytestmark = pytest.mark.skipif( + not _artifacts_present(), + reason='No tutorial artifacts found; run script-tests or notebook-tests first.', +) + + +def _assert_close(actual: float | None, expected: float, rtol: float, label: str) -> None: + """Assert *actual* matches *expected* within a relative tolerance.""" + assert actual is not None, f'{label}: value missing from analysis.cif' + assert math.isclose(actual, expected, rel_tol=rtol, abs_tol=ABS_TOL), ( + f'{label}: {actual} != {expected} (rel_tol={rtol})' + ) + + +@pytest.mark.parametrize('name', sorted(BASELINE)) +def test_tutorial_output(name: str) -> None: + """Check one tutorial's saved analysis.cif against the baseline.""" + expected = BASELINE[name] + cif_path = _analysis_cif_path(name) + assert cif_path.is_file(), f"Missing {cif_path}; tutorial '{name}' did not save its project." + + cif = read_analysis_cif(cif_path) + rtol = expected['rtol'] + + _assert_close( + cif.scalar('reduced_chi_square'), + expected['reduced_chi_square'], + rtol, + f'{name}: reduced_chi_square', + ) + + for scalar_name in ('R_factor_all', 'wR_factor_all'): + if scalar_name in expected: + _assert_close( + cif.scalar(scalar_name), + expected[scalar_name], + rtol, + f'{name}: {scalar_name}', + ) + + for param_name, exp_value in expected['parameters'].items(): + assert param_name in cif.fit_parameters, ( + f"{name}: parameter '{param_name}' missing from analysis.cif" + ) + _assert_close( + cif.parameter_value(param_name), + exp_value, + rtol, + f'{name}: {param_name}', + ) From 0b4ae457a94f65117579d4cbdfedfdd9df2d9177 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 18:32:27 +0200 Subject: [PATCH 05/57] Apply pixi run fix auto-fixes --- pixi.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pixi.toml b/pixi.toml index 698b29223..1476f3ce0 100644 --- a/pixi.toml +++ b/pixi.toml @@ -115,8 +115,12 @@ notebook-tests = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmak # saved projects exist under tmp/tutorials/projects/. tutorial-output-tests = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } # Run the tutorials and then verify their saved outputs in a single step. -script-tests-checked = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', depends-on = ['script-tests'], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } -notebook-tests-checked = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', depends-on = ['notebook-tests'], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +script-tests-checked = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', depends-on = [ + 'script-tests', +], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +notebook-tests-checked = { cmd = 'python -m pytest tests/tutorials/ --color=yes -v', depends-on = [ + 'notebook-tests', +], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } test = { depends-on = ['unit-tests', 'functional-tests'] } test-all = { depends-on = [ From 3d5fe64e3a3a21efbbc1aebc4143dbc2f5cf7e5e Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 19:40:56 +0200 Subject: [PATCH 06/57] Assert persisted result_kind in tutorial-output test --- tests/tutorials/test_tutorial_outputs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/tutorials/test_tutorial_outputs.py b/tests/tutorials/test_tutorial_outputs.py index 6ee533045..a044157a3 100644 --- a/tests/tutorials/test_tutorial_outputs.py +++ b/tests/tutorials/test_tutorial_outputs.py @@ -66,6 +66,10 @@ def test_tutorial_output(name: str) -> None: cif = read_analysis_cif(cif_path) rtol = expected['rtol'] + assert cif.result_kind == expected['result_kind'], ( + f"{name}: result_kind '{cif.result_kind}' != expected '{expected['result_kind']}'" + ) + _assert_close( cif.scalar('reduced_chi_square'), expected['reduced_chi_square'], From 7a02030b19e365d3a5bdeb2d19fe25e303014f5d Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 19:44:37 +0200 Subject: [PATCH 07/57] Run tutorial-output checks in CI --- .github/workflows/tutorial-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tutorial-tests.yml b/.github/workflows/tutorial-tests.yml index f924a31d3..4e93a6383 100644 --- a/.github/workflows/tutorial-tests.yml +++ b/.github/workflows/tutorial-tests.yml @@ -47,7 +47,7 @@ jobs: - name: Test tutorials as python scripts shell: bash - run: pixi run script-tests + run: pixi run script-tests-checked - name: Prepare notebooks shell: bash @@ -55,7 +55,7 @@ jobs: - name: Test tutorials as notebooks shell: bash - run: pixi run notebook-tests + run: pixi run notebook-tests-checked # Job 2: Build and publish dashboard (reusable workflow) run-reusable-workflows: From 6be99da137188603c296c67f08819c9ca8319f6a Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 19:54:47 +0200 Subject: [PATCH 08/57] Clean saved tutorial projects before tutorial tests --- pixi.toml | 13 ++++++++-- tools/clean_tutorial_projects.py | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tools/clean_tutorial_projects.py diff --git a/pixi.toml b/pixi.toml index 1476f3ce0..928c8bb22 100644 --- a/pixi.toml +++ b/pixi.toml @@ -106,8 +106,17 @@ user = { features = ['py-max', 'user'] } unit-tests = 'python -m pytest tests/unit/ --color=yes -v' functional-tests = 'python -m pytest tests/functional/ --color=yes -v' integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' -script-tests = { cmd = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } -notebook-tests = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } + +# Remove previously saved tutorial output projects (projects/ed_*) so the +# tutorial-output checks cannot pass against a stale artifact from an earlier +# run. Downloaded input projects (projects/ed-NN) are preserved. +clean-tutorial-projects = { cmd = 'python tools/clean_tutorial_projects.py', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +script-tests = { cmd = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v', depends-on = [ + 'clean-tutorial-projects', +], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +notebook-tests = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v', depends-on = [ + 'clean-tutorial-projects', +], env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } # Parse the analysis.cif of every saved tutorial project and assert its # refined fit results against tests/tutorials/baseline.json. Run only after diff --git a/tools/clean_tutorial_projects.py b/tools/clean_tutorial_projects.py new file mode 100644 index 000000000..047cc418c --- /dev/null +++ b/tools/clean_tutorial_projects.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Remove saved tutorial output projects before regenerating them. + +The tutorials save their projects under ``/projects/`` +with names matching ``ed__``. Removing them before the +tutorial tests guarantees the tutorial-output checks see only freshly +written projects, so a stale artifact cannot mask a tutorial that no +longer saves its project. + +Only the ``ed_*`` output directories are removed; downloaded input +projects (e.g. ``ed-36`` from ``download_data``) keep their hyphenated +names and are preserved so they need not be re-downloaded. +""" + +from __future__ import annotations + +import os +import shutil +from pathlib import Path + +_DEFAULT_ARTIFACT_ROOT = Path('tmp') / 'tutorials' + + +def main() -> None: + """Remove every ``projects/ed_*`` directory under the artifact root.""" + configured = os.environ.get('EASYDIFFRACTION_ARTIFACT_ROOT') + root = Path(configured) if configured else _DEFAULT_ARTIFACT_ROOT + projects_dir = root / 'projects' + + removed = 0 + if projects_dir.is_dir(): + for path in sorted(projects_dir.glob('ed_*')): + if path.is_dir(): + shutil.rmtree(path) + removed += 1 + + print(f'Removed {removed} saved tutorial project(s) under {projects_dir}') + + +if __name__ == '__main__': + main() From 32a4a074e2038be2f87c8980649d27718e9e47cb Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 21:52:01 +0200 Subject: [PATCH 09/57] Skip numeric baseline for platform-sensitive tutorials --- tests/tutorials/baseline.json | 1 + tests/tutorials/generate_baseline.py | 14 ++++++++++++-- tests/tutorials/test_tutorial_outputs.py | 11 ++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/tutorials/baseline.json b/tests/tutorials/baseline.json index 2b5f9b9b3..25aa4b3a5 100644 --- a/tests/tutorials/baseline.json +++ b/tests/tutorials/baseline.json @@ -231,6 +231,7 @@ "result_kind": "deterministic", "rtol": 0.02, "reduced_chi_square": 2.663955, + "platform_sensitive": true, "R_factor_all": 0.08004, "wR_factor_all": 0.059687, "parameters": { diff --git a/tests/tutorials/generate_baseline.py b/tests/tutorials/generate_baseline.py index 46d96eab7..fc1b09659 100644 --- a/tests/tutorials/generate_baseline.py +++ b/tests/tutorials/generate_baseline.py @@ -31,6 +31,14 @@ # Optional deterministic fit-quality scalars to track when present. OPTIONAL_SCALARS = ('R_factor_all', 'wR_factor_all') +# Tutorials whose fit metrics are not reproducible across platforms, +# so they are exempted from the numeric baseline comparison. ed-7 +# fits on the compiled crysfml backend, whose reduced_chi_square +# differs between arm64 macOS and x86-64 Linux/Windows. They still +# run in script-/notebook-tests and are checked for result_kind; +# only their numeric metrics are skipped. Add a name here to exempt. +PLATFORM_SENSITIVE = frozenset({'ed_7_si_sepd'}) + # Number of refined parameters to track per tutorial (cell lengths and # phase scales preferred, topped up from the front of the loop). KEY_PARAMETER_COUNT = 2 @@ -66,7 +74,7 @@ def select_key_parameters(cif: AnalysisCif) -> dict[str, float]: return {name: round(cif.parameter_value(name), ROUND_DIGITS) for name in ordered} -def build_entry(cif: AnalysisCif) -> dict | None: +def build_entry(name: str, cif: AnalysisCif) -> dict | None: """Build a baseline entry, or ``None`` if the project has no fit.""" reduced_chi_square = cif.scalar('reduced_chi_square') if reduced_chi_square is None or reduced_chi_square <= 0: @@ -78,6 +86,8 @@ def build_entry(cif: AnalysisCif) -> dict | None: 'rtol': BAYESIAN_RTOL if kind == 'bayesian' else DETERMINISTIC_RTOL, 'reduced_chi_square': round(reduced_chi_square, ROUND_DIGITS), } + if name in PLATFORM_SENSITIVE: + entry['platform_sensitive'] = True for scalar_name in OPTIONAL_SCALARS: value = cif.scalar(scalar_name) if value is not None: @@ -94,7 +104,7 @@ def collect_baseline(root: Path) -> dict[str, dict]: name = cif_path.parents[1].name if not name.startswith('ed_'): continue - entry = build_entry(read_analysis_cif(cif_path)) + entry = build_entry(name, read_analysis_cif(cif_path)) if entry is not None: baseline[name] = entry return baseline diff --git a/tests/tutorials/test_tutorial_outputs.py b/tests/tutorials/test_tutorial_outputs.py index a044157a3..9c9609897 100644 --- a/tests/tutorials/test_tutorial_outputs.py +++ b/tests/tutorials/test_tutorial_outputs.py @@ -23,6 +23,7 @@ import pytest from analysis_cif_reader import read_analysis_cif +from generate_baseline import PLATFORM_SENSITIVE from generate_baseline import artifact_root BASELINE = json.loads((Path(__file__).parent / 'baseline.json').read_text(encoding='utf-8')) @@ -64,12 +65,20 @@ def test_tutorial_output(name: str) -> None: assert cif_path.is_file(), f"Missing {cif_path}; tutorial '{name}' did not save its project." cif = read_analysis_cif(cif_path) - rtol = expected['rtol'] + # result_kind reflects the minimizer type; it is reproducible + # across platforms, so it is always checked. assert cif.result_kind == expected['result_kind'], ( f"{name}: result_kind '{cif.result_kind}' != expected '{expected['result_kind']}'" ) + # Some tutorials (e.g. ed-7 on the compiled crysfml backend) + # produce fit metrics that are not reproducible across CPU arch + # or BLAS; confirm they ran and saved, but skip the numbers. + if name in PLATFORM_SENSITIVE: + pytest.skip(f'{name}: platform-sensitive fit metrics not compared') + + rtol = expected['rtol'] _assert_close( cif.scalar('reduced_chi_square'), expected['reduced_chi_square'], From ca981580e841e67b5b7a7188ca7be95c066cec81 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 21:52:13 +0200 Subject: [PATCH 10/57] Set non-zero defaults for TOF profiles --- .../experiment/categories/peak/tof_mixins.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py b/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py index d540d6bb2..0ebd66b16 100644 --- a/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py +++ b/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py @@ -23,7 +23,12 @@ class TofGaussianBroadeningMixin: - """TOF Gaussian broadening parameters σ₀, σ₁, σ₂.""" + """ + TOF Gaussian broadening parameters σ₀, σ₁, σ₂. + + The constant term σ₀ defaults nonzero so every TOF profile has a + finite peak width out of the box; σ₁ and σ₂ default to 0. + """ def __init__(self) -> None: super().__init__() @@ -37,7 +42,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}^2$', ), value_spec=AttributeSpec( - default=0.0, + default=7.0, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -230,6 +235,9 @@ class TofBackToBackExponentialMixin: Rise parameters α₀, α₁ and decay parameters β₀, β₁ follow Von Dreele, Jorgensen & Windsor, J. Appl. Cryst. 15, 581 (1982). + + The rise α₁ and decay β₀ default nonzero so the profile is + normalisable and the peak is visible; refine per instrument. """ def __init__(self) -> None: @@ -244,7 +252,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}$', ), value_spec=AttributeSpec( - default=0.01, + default=0.0, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -261,7 +269,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}/\mathrm{\AA}$', ), value_spec=AttributeSpec( - default=0.02, + default=0.2, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -278,7 +286,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}$', ), value_spec=AttributeSpec( - default=0.0, + default=0.04, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -368,6 +376,9 @@ class TofDoubleExponentialMixin: Rise parameters α₁, α₂, decay parameters β₀₀, β₀₁, β₁₀ for two exponential regimes, and switching-function parameters r₀₁, r₀₂, r₀₃. + + α₁, β₀₀, β₁₀ and r₀₁ default nonzero so both regimes stay finite and + blended; an all-zero set produces NaN. Refine per instrument. """ def __init__(self) -> None: @@ -382,7 +393,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}$', ), value_spec=AttributeSpec( - default=0.0, + default=0.25, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -416,7 +427,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}$', ), value_spec=AttributeSpec( - default=0.0, + default=4.0, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -450,7 +461,7 @@ def __init__(self) -> None: latex_units=r'$\mu\mathrm{s}$', ), value_spec=AttributeSpec( - default=0.0, + default=2.0, validator=RangeValidator(), ), cif_handler=CifHandler( @@ -463,7 +474,7 @@ def __init__(self) -> None: description='Double-exp switching function r₀₁', units='none', value_spec=AttributeSpec( - default=0.0, + default=0.5, validator=RangeValidator(), ), cif_handler=CifHandler( From fb764d1fe774c3be5110f9f04eb1b5bb2a8db4ff Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 21:56:26 +0200 Subject: [PATCH 11/57] Trim platform-sensitive skip message --- tests/tutorials/test_tutorial_outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tutorials/test_tutorial_outputs.py b/tests/tutorials/test_tutorial_outputs.py index 9c9609897..1cb4f1cec 100644 --- a/tests/tutorials/test_tutorial_outputs.py +++ b/tests/tutorials/test_tutorial_outputs.py @@ -76,7 +76,7 @@ def test_tutorial_output(name: str) -> None: # produce fit metrics that are not reproducible across CPU arch # or BLAS; confirm they ran and saved, but skip the numbers. if name in PLATFORM_SENSITIVE: - pytest.skip(f'{name}: platform-sensitive fit metrics not compared') + pytest.skip('platform-sensitive') rtol = expected['rtol'] _assert_close( From 06daaf1d504e41130fd39e8b69c4783017ac1666 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:30:46 +0200 Subject: [PATCH 12/57] Update tutorial Si structure and doc URLs --- docs/docs/tutorials/ed-13.py | 84 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index 416e79999..8ed16fa13 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -29,7 +29,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/first-steps/#importing-easydiffraction) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/first-steps/#importing-easydiffraction) # for more details about importing the EasyDiffraction library and its # components. @@ -60,7 +60,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/project/) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/project/) # for more details about creating a project and its purpose in the # analysis workflow. @@ -87,7 +87,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/) # for more details about experiments and their purpose in the analysis # workflow. @@ -116,7 +116,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#defining-an-experiment-manually) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#defining-an-experiment-manually) # for more details about different types of experiments. # %% @@ -141,7 +141,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#measured-data-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#measured-data-category) # for more details about the measured data and its format. # # To visualize the measured data, we can use the `pattern` method of @@ -170,7 +170,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#excluded-regions-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#excluded-regions-category) # for more details about excluding regions from the measured data. # %% @@ -205,7 +205,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#instrument-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#instrument-category) # for more details about the instrument parameters. # %% @@ -251,7 +251,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/parameters/) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/parameters/) # for more details about parameters in EasyDiffraction and their # attributes. @@ -308,7 +308,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#peak-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#peak-category) # for more details about the peak profile types. # %% @@ -355,7 +355,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#background-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#background-category) # for more details about the background and its types. # %% @@ -393,15 +393,16 @@ # which is the length of the unit cell edge. The Si crystal structure # has a single atom in the unit cell, which is located at the origin (0, # 0, 0) of the unit cell. The symmetry of this site is defined by the -# Wyckoff letter 'a'. The atomic displacement parameter defines the -# thermal vibrations of the atoms in the unit cell and is presented as -# an isotropic parameter (B_iso). +# Wyckoff letter 'a', which is assigned automatically based on the space +# group and the atomic coordinates. The atomic displacement parameter +# defines the thermal vibrations of the atoms in the unit cell and is +# presented as an isotropic parameter (B_iso). # # Sometimes, the initial crystal structure parameters can be obtained # from one of the crystallographic databases, like for example the # Crystallography Open Database (COD). In this case, we use the COD # entry for silicon as a reference for the initial crystal structure -# model: https://www.crystallography.net/cod/4507226.html +# model: https://www.crystallography.net/cod/9008565.html # # Usually, the crystal structure parameters are provided in a CIF file # format, which is a standard format for crystallographic data. An @@ -411,7 +412,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/data-format/) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/data-format/) # for more details about the CIF format and its use in EasyDiffraction. # %% [markdown] @@ -419,11 +420,11 @@ # data_si # # _space_group.name_H-M_alt "F d -3 m" -# _space_group.IT_coordinate_system_code 2 +# _space_group.IT_coordinate_system_code 1 # -# _cell.length_a 5.43 -# _cell.length_b 5.43 -# _cell.length_c 5.43 +# _cell.length_a 5.4307 +# _cell.length_b 5.4307 +# _cell.length_c 5.4307 # _cell.angle_alpha 90.0 # _cell.angle_beta 90.0 # _cell.angle_gamma 90.0 @@ -434,11 +435,10 @@ # _atom_site.fract_x # _atom_site.fract_y # _atom_site.fract_z -# _atom_site.wyckoff_letter # _atom_site.occupancy # _atom_site.ADP_type # _atom_site.B_iso_or_equiv -# Si Si 0.125 0.125 0.125 a 1.0 Biso 0.89 +# Si Si 0 0 0 1.0 Biso 0.89 # ``` # %% [markdown] @@ -448,7 +448,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/) # for more details about structures and their purpose in the data # analysis workflow. @@ -463,39 +463,40 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#space-group-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/#space-group-category) # for more details about the space group. # %% project_1.structures['si'].space_group.name_h_m = 'F d -3 m' -project_1.structures['si'].space_group.it_coordinate_system_code = '2' +project_1.structures['si'].space_group.it_coordinate_system_code = '1' # %% [markdown] # #### Set Unit Cell # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#cell-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/#cell-category) # for more details about the unit cell parameters. # %% -project_1.structures['si'].cell.length_a = 5.43 +project_1.structures['si'].cell.length_a = 5.4307 # %% [markdown] # #### Set Atom Sites # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#atom-sites-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/#atom-sites-category) # for more details about the atom sites category. # %% project_1.structures['si'].atom_sites.create( label='Si', type_symbol='Si', - fract_x=0.125, - fract_y=0.125, - fract_z=0.125, + fract_x=0.0, + fract_y=0.0, + fract_z=0.0, + adp_type='Biso', adp_iso=0.89, ) @@ -518,7 +519,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#linked-phases-category) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#linked-phases-category) # for more details about linking a structure to an experiment. # %% @@ -553,7 +554,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/analysis/#minimization-optimization) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/analysis/#minimization-optimization) # for more details about the fitting process in EasyDiffraction. # %% [markdown] @@ -625,7 +626,7 @@ # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/analysis/#perform-fit) +# [documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/analysis/#perform-fit) # for more details about the fitting process. # %% @@ -916,14 +917,13 @@ # _atom_site.fract_x # _atom_site.fract_y # _atom_site.fract_z -# _atom_site.wyckoff_letter # _atom_site.occupancy # _atom_site.ADP_type # _atom_site.B_iso_or_equiv -# La La 0.0 0.0 0.0 a 0.5 Biso 0.95 -# Ba Ba 0.0 0.0 0.0 a 0.5 Biso 0.95 -# Co Co 0.5 0.5 0.5 b 1.0 Biso 0.80 -# O O 0.0 0.5 0.5 c 1.0 Biso 1.66 +# La La 0.0 0.0 0.0 0.5 Biso 0.95 +# Ba Ba 0.0 0.0 0.0 0.5 Biso 0.95 +# Co Co 0.5 0.5 0.5 1.0 Biso 0.80 +# O O 0.0 0.5 0.5 1.0 Biso 1.66 # ``` # %% [markdown] @@ -1029,8 +1029,9 @@ fract_x=0, fract_y=0, fract_z=0, - adp_iso=0.95, occupancy=0.5, + adp_type='Biso', + adp_iso=0.95, ) project_2.structures['lbco'].atom_sites.create( label='Ba', @@ -1038,8 +1039,9 @@ fract_x=0, fract_y=0, fract_z=0, - adp_iso=0.95, occupancy=0.5, + adp_type='Biso', + adp_iso=0.95, ) project_2.structures['lbco'].atom_sites.create( label='Co', @@ -1047,6 +1049,7 @@ fract_x=0.5, fract_y=0.5, fract_z=0.5, + adp_type='Biso', adp_iso=0.80, ) project_2.structures['lbco'].atom_sites.create( @@ -1055,6 +1058,7 @@ fract_x=0, fract_y=0.5, fract_z=0.5, + adp_type='Biso', adp_iso=1.66, ) From ff76e4e55c3396e3d60e87b837f0ec7401593040 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:45:17 +0200 Subject: [PATCH 13/57] Add pattern-display-unification implementation plan --- docs/dev/plans/pattern-display-unification.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/dev/plans/pattern-display-unification.md diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md new file mode 100644 index 000000000..dddd8a247 --- /dev/null +++ b/docs/dev/plans/pattern-display-unification.md @@ -0,0 +1,154 @@ +# Plan: Pattern Display Unification + +Follows [`AGENTS.md`](../../../AGENTS.md). No deliberate exceptions. + +## ADR + +Amends the accepted ADR +[`display-ux.md`](../adrs/accepted/display-ux.md): the `pattern()` API +drops the `include` parameter and `show_pattern_options()`, and always +renders every kind of data the project state supports. Relates to +[`plotting-docs-performance.md`](../adrs/accepted/plotting-docs-performance.md) +(the lazy figure skeleton that exposed the height bug) and open issue +#93 (future of `show_residual`). No new ADR file is required. + +## Branch / PR + +Feature slug `pattern-display-unification`. PR targets `develop`. +**Open:** confirm whether to branch off the current +`tutorial-project-outputs` working branch or start a fresh +`pattern-display-unification` branch before committing. + +## Background + +Four user-reported issues, all rooted in the split between the +single-panel path (`plot_meas`/`plot_calc` → `plot_powder`) and the +composite path (`build_powder_meas_vs_calc_figure`): + +1. **HTML height** — the single-panel Plotly figure sets no explicit + height, so the docs' lazy skeleton falls back to + `DEFAULT_HEIGHT * PLOTLY_HEIGHT_PER_UNIT = 600px` (the full + 3-panel height) instead of the ~458px main panel. +2. **X-axis offsets** — the single-panel path sets no x-range, so + Plotly auto-pads; the composite pins `(min, max)`. +3. **`include=('measured','excluded')` redundancy** — excluded + regions are a property of the experiment, not a viewing choice. +4. **Structure top margin** — the 3D scene sits flush under its + header line. + +## Decisions + +- **Drop `include` entirely.** `pattern(expt_name, x_min=None, + x_max=None, *, x=None)` always renders all available content (the + former `include='auto'` behaviour, which is already proven). The + cost — losing strict-subset views such as measured-only once a + calc exists — is accepted; `x_min`/`x_max` and `x` still work. Can + be re-added later only on a concrete need. +- **Excluded regions always shade** when defined on the experiment, + in every view; skipped only when a custom `x` axis is selected + (the overlay can't be mapped). The `'excluded'` token is removed. +- **Common rendering base via shared primitives.** The single-panel + `plot_powder` derives its figure height from + `_single_main_panel_height_pixels(...)` and its x-range from + `_composite_x_range(...)` — the same helpers the composite uses. + Because `_get_layout` already uses the composite margins + (`r:30, t:40, b:45`), a 1-panel view becomes the composite's main + row pixel-for-pixel. No builder merge (the composite hard-requires + `y_calc`); the shared helpers are the single source of truth. +- **Keep the internal availability engine** (`_pattern_option_statuses`, + `_auto_include`, `PatternOptionStatus`); only the user-facing + selection/validation/discovery layer is removed. +- The residual-fraction default moves to `plotters/base.py` so both + the facade (`plotting.py`) and backend (`plotly.py`) can import it + without a circular dependency. + +## Open questions + +- Branch choice (above). +- Should `pattern()` keep accepting a custom `x` axis variable? + Assumed **yes** (separate from `include`). + +## Concrete files likely to change + +- `src/easydiffraction/display/plotters/base.py` — add + `DEFAULT_RESIDUAL_HEIGHT_FRACTION`. +- `src/easydiffraction/display/plotters/plotly.py` — `plot_powder` + explicit height + tight x-range. +- `src/easydiffraction/display/plotting.py` — import the moved + constant. +- `src/easydiffraction/project/display.py` — rewrite `pattern()`; + remove `include`, `_normalize_include`, + `_validate_requested_include`, `show_pattern_options`; simplify + `_show_point_estimate_pattern`. +- `src/easydiffraction/display/structure/templates/structure.html.j2` + — scene top margin (done). +- `docs/dev/adrs/accepted/display-ux.md` — amend Pattern Display. +- `docs/dev/adrs/accepted/crysview-structure-visualization.md`, + `docs/docs/quick-reference/index.md` — drop stale + `show_pattern_options` references. +- `docs/docs/tutorials/ed-3.py`, `ed-9.py`, `ed-11.py`, `ed-13.py` + (+ regenerated `.ipynb`) — remove `include=`. +- Phase 2: `tests/unit/easydiffraction/project/test_display.py`, + `tests/integration/fitting/test_plotting.py`. + +## Implementation steps (Phase 1) + +- [ ] **P1.1 — Unify single-panel sizing/x-range.** Move + `DEFAULT_RESIDUAL_HEIGHT_FRACTION` to `base.py`; `plot_powder` sets + explicit main-panel height and tight `(min,max)` x-range. Fixes + issues #1 and #2. + Commit: `Match single-panel pattern height and x-range to composite` +- [ ] **P1.2 — Drop `include`; always render available content.** + Rewrite `pattern()`; remove the selection/validation/discovery + layer; always-shade excluded. Fixes issue #3. + Commit: `Always render available pattern content; drop include` +- [x] **P1.3 — Structure scene top margin.** Fixes issue #4. + Commit: `Add top margin above structure scene rectangle` +- [ ] **P1.4 — Amend ADR and docs.** Update `display-ux.md`; drop + stale `show_pattern_options` references. + Commit: `Amend display-ux ADR for always-on pattern view` +- [ ] **P1.5 — Update tutorials.** Remove `include=` from tutorial + sources; `pixi run notebook-prepare`. + Commit: `Drop include= from tutorials for unified pattern view` +- [ ] **P1.6 — Phase 1 review gate.** + Commit: `Reach Phase 1 review gate` + +Each completed P1 step is staged with explicit paths and committed +locally before the next step (per `AGENTS.md` Commits). Stop after +Phase 1 for review before Phase 2. + +## Phase 2 — Verification + +```bash +pixi run fix +pixi run check > /tmp/easydiffraction-check.log 2>&1; check_exit_code=$?; tail -n 200 /tmp/easydiffraction-check.log; exit $check_exit_code +pixi run unit-tests > /tmp/easydiffraction-unit.log 2>&1; unit_tests_exit_code=$?; tail -n 200 /tmp/easydiffraction-unit.log; exit $unit_tests_exit_code +pixi run integration-tests +pixi run script-tests +``` + +Test updates: remove `include=`/`show_pattern_options` tests; add +coverage for always-shown excluded regions, single-panel height + +tight x-range matching the composite main row, and the +nothing-to-plot error. + +## Status checklist + +- [ ] Phase 1 implementation complete +- [ ] Phase 1 reviewed +- [ ] Phase 2 verification complete + +## Suggested Pull Request + +**Title:** Simplify and unify the experiment pattern view + +**Description:** `project.display.pattern()` now always shows +everything the data supports — measured and calculated curves, the +residual, Bragg ticks, background, excluded regions, and uncertainty +bands — so you no longer pass `include=...` to assemble a view; just +call `pattern(expt_name=...)` and zoom with `x_min`/`x_max`. Excluded +regions are always shaded when defined. Single-panel and full +three-panel charts now share one layout, so a measured-only plot is +exactly the top panel of the full view — fixing the oversized height +in the HTML docs and the stray left/right margins. The 3D structure +view also gains a little breathing room below its title. From 42ad5448c87ae4bca33837034485479fc1d669f9 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:45:17 +0200 Subject: [PATCH 14/57] Add top margin above structure scene rectangle --- .../display/structure/templates/structure.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/easydiffraction/display/structure/templates/structure.html.j2 b/src/easydiffraction/display/structure/templates/structure.html.j2 index 51e8b9aa5..348225dfb 100644 --- a/src/easydiffraction/display/structure/templates/structure.html.j2 +++ b/src/easydiffraction/display/structure/templates/structure.html.j2 @@ -1,4 +1,4 @@ -
From ba6a0f8323d6f07ab0aab1c65912f503783f1f11 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:45:28 +0200 Subject: [PATCH 15/57] Match single-panel pattern height and x-range to composite --- docs/dev/plans/pattern-display-unification.md | 2 +- src/easydiffraction/display/plotters/base.py | 3 +++ src/easydiffraction/display/plotters/plotly.py | 16 +++++++++++++++- src/easydiffraction/display/plotting.py | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index dddd8a247..be67d5f13 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -93,7 +93,7 @@ composite path (`build_powder_meas_vs_calc_figure`): ## Implementation steps (Phase 1) -- [ ] **P1.1 — Unify single-panel sizing/x-range.** Move +- [x] **P1.1 — Unify single-panel sizing/x-range.** Move `DEFAULT_RESIDUAL_HEIGHT_FRACTION` to `base.py`; `plot_powder` sets explicit main-panel height and tight `(min,max)` x-range. Fixes issues #1 and #2. diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index 5a02c37b8..5e2d38eca 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -16,6 +16,9 @@ from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum DEFAULT_HEIGHT = 25 +# Residual-to-main row height ratio shared by the composite figure and +# the single-panel figure so both derive the same main-panel height. +DEFAULT_RESIDUAL_HEIGHT_FRACTION = 0.25 DEFAULT_MIN = -np.inf DEFAULT_MAX = np.inf diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index 8bfed31a4..00e66dbf8 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -30,6 +30,7 @@ HTML = None from easydiffraction.display.plotters.base import DEFAULT_HEIGHT +from easydiffraction.display.plotters.base import DEFAULT_RESIDUAL_HEIGHT_FRACTION from easydiffraction.display.plotters.base import SERIES_CONFIG from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PlotterBase @@ -2114,7 +2115,8 @@ def plot_powder( excluded_ranges : tuple[tuple[float, float], ...], default=() Excluded x-ranges to shade on the figure. """ - # Intentionally unused; accepted for API compatibility + # The passed height is an ASCII row count; the Plotly single + # panel is sized to the composite main row below instead. del height data = [] @@ -2129,6 +2131,18 @@ def plot_powder( ) fig = self._get_figure(data, layout) + # Share the composite's sizing and range primitives so a single + # panel is its main row by construction: the same explicit + # height (otherwise the docs skeleton falls back to the full + # three-panel height) and the same tight x-range with no + # autoscale padding. ``_get_layout`` already uses the composite + # margins, so the drawable area matches pixel-for-pixel. + fig.update_layout( + height=self._single_main_panel_height_pixels(DEFAULT_RESIDUAL_HEIGHT_FRACTION), + ) + x_min, x_max = self._composite_x_range(np.asarray(x)) + if x_min is not None and x_max is not None: + fig.update_xaxes(range=[x_min, x_max]) self._add_excluded_region_vrects(fig=fig, excluded_ranges=excluded_ranges) self._show_figure(fig) diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 90b0c8aca..dcda2cf33 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -30,6 +30,7 @@ from easydiffraction.display.plotters.base import DEFAULT_HEIGHT from easydiffraction.display.plotters.base import DEFAULT_MAX from easydiffraction.display.plotters.base import DEFAULT_MIN +from easydiffraction.display.plotters.base import DEFAULT_RESIDUAL_HEIGHT_FRACTION from easydiffraction.display.plotters.base import DEFAULT_X_AXIS from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec @@ -82,7 +83,6 @@ class PosteriorPairPlotStyleEnum(StrEnum): DEFAULT_CORRELATION_THRESHOLD: float | None = None DEFAULT_CORRELATION_MAX_PARAMETERS = 6 EXPECTED_COVAR_NDIM = 2 -DEFAULT_RESIDUAL_HEIGHT_FRACTION = 0.25 DEFAULT_BRAGG_PEAKS_HEIGHT_FRACTION = 0.10 DEFAULT_RESID_HEIGHT = DEFAULT_RESIDUAL_HEIGHT_FRACTION DEFAULT_BRAGG_ROW = DEFAULT_BRAGG_PEAKS_HEIGHT_FRACTION From bb5724284e8a8b1d09cd854caed81875d4526afb Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:45:36 +0200 Subject: [PATCH 16/57] Always render available pattern content; drop include --- docs/dev/plans/pattern-display-unification.md | 2 +- src/easydiffraction/project/display.py | 167 ++++-------------- 2 files changed, 32 insertions(+), 137 deletions(-) diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index be67d5f13..52c2c708e 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -98,7 +98,7 @@ composite path (`build_powder_meas_vs_calc_figure`): explicit main-panel height and tight `(min,max)` x-range. Fixes issues #1 and #2. Commit: `Match single-panel pattern height and x-range to composite` -- [ ] **P1.2 — Drop `include`; always render available content.** +- [x] **P1.2 — Drop `include`; always render available content.** Rewrite `pattern()`; remove the selection/validation/discovery layer; always-shade excluded. Fixes issue #3. Commit: `Always render available pattern content; drop include` diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 3c07970fb..67426fcd6 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -359,65 +359,37 @@ def pattern( expt_name: str, x_min: float | None = None, x_max: float | None = None, - include: str | tuple[str, ...] = 'auto', *, x: object | None = None, ) -> None: - """Show a pattern view for one experiment.""" - normalized_include = self._normalize_include(include) - statuses = self._pattern_option_statuses(expt_name) + """ + Show the experiment's diffraction pattern. - if normalized_include == ('auto',): - auto_include = self._auto_include(statuses) - if x is not None: - auto_include = tuple(option for option in auto_include if option != 'excluded') - if not auto_include: - msg = self._status_by_name(statuses, 'auto').reason - raise ValueError(msg) - if 'uncertainty' in auto_include: - indicator_context = ( - activity_indicator( - ACTIVITY_LABEL_PROCESSING, - verbosity=VerbosityEnum(self._project.verbosity.fit.value), - ) - if self._posterior._predictive_needs_processing_indicator( - expt_name=expt_name, - style='band', - x=x, - ) - else nullcontext() - ) - with indicator_context: - self._project.rendering_plot.plotter._plot_posterior_predictive_request( - expt_name=expt_name, - style='band', - plot_options=_MeasVsCalcPlotOptions( - x_min=x_min, - x_max=x_max, - show_residual=True if 'residual' in auto_include else None, - show_background='background' in auto_include, - show_bragg='bragg' in auto_include, - show_excluded='excluded' in auto_include, - x=x, - ), - ) - return - self._show_point_estimate_pattern( - expt_name=expt_name, - x_min=x_min, - x_max=x_max, - include=auto_include, - statuses=statuses, - x=x, - ) - return + Renders every kind of data the project state supports for the + experiment: measured and calculated intensities, the residual, + Bragg ticks, background, excluded regions, and posterior + predictive uncertainty bands, each shown when available. - self._validate_requested_include(statuses, normalized_include) - if x is not None and 'excluded' in normalized_include: - msg = "Excluded-region overlays currently require the experiment's default x-axis." - raise ValueError(msg) + Parameters + ---------- + expt_name : str + Name of the experiment to plot. + x_min : float | None, default=None + Lower bound for the x-axis range. + x_max : float | None, default=None + Upper bound for the x-axis range. + x : object | None, default=None + Optional x-axis variable overriding the experiment default + (excluded-region overlays are skipped for a custom axis). + """ + statuses = self._pattern_option_statuses(expt_name) + content = self._auto_include(statuses) + if x is not None: + content = tuple(option for option in content if option != 'excluded') + if not content: + raise ValueError(self._status_by_name(statuses, 'auto').reason) - if 'uncertainty' in normalized_include: + if 'uncertainty' in content: indicator_context = ( activity_indicator( ACTIVITY_LABEL_PROCESSING, @@ -437,10 +409,10 @@ def pattern( plot_options=_MeasVsCalcPlotOptions( x_min=x_min, x_max=x_max, - show_residual=True if 'residual' in normalized_include else None, - show_background='background' in normalized_include, - show_bragg='bragg' in normalized_include, - show_excluded='excluded' in normalized_include, + show_residual=True if 'residual' in content else None, + show_background='background' in content, + show_bragg='bragg' in content, + show_excluded='excluded' in content, x=x, ), ) @@ -450,29 +422,10 @@ def pattern( expt_name=expt_name, x_min=x_min, x_max=x_max, - include=normalized_include, - statuses=statuses, + include=content, x=x, ) - def show_pattern_options(self, expt_name: str) -> None: - """Show available ``pattern(include=...)`` options.""" - statuses = self._pattern_option_statuses(expt_name) - render_table( - columns_headers=['Option', 'Description', 'Available', 'Auto', 'Reason'], - columns_alignment=['left', 'left', 'center', 'center', 'left'], - columns_data=[ - [ - status.name, - status.description, - 'yes' if status.available else 'no', - 'yes' if status.auto_included else 'no', - status.reason or '-', - ] - for status in statuses - ], - ) - def structure( self, struct_name: str, @@ -629,24 +582,6 @@ def _emit_structure_output(self, output: str) -> None: "or set project.rendering_structure.type = 'ascii' for a terminal view.", ) - @staticmethod - def _normalize_include(include: str | tuple[str, ...]) -> tuple[str, ...]: - """Validate and normalize a ``pattern(include=...)`` value.""" - values = (include,) if isinstance(include, str) else include - if not values: - msg = 'include must contain at least one option.' - raise ValueError(msg) - - normalized = tuple(dict.fromkeys(values)) - unknown = [value for value in normalized if value not in _PATTERN_OPTION_DESCRIPTIONS] - if unknown: - msg = f'Unknown pattern include option(s): {unknown}.' - raise ValueError(msg) - if 'auto' in normalized and len(normalized) > 1: - msg = "include='auto' cannot be combined with other options." - raise ValueError(msg) - return normalized - @staticmethod def _status_by_name( statuses: list[PatternOptionStatus], @@ -677,7 +612,7 @@ def _auto_include( cls, statuses: list[PatternOptionStatus], ) -> tuple[str, ...]: - """Return the effective include tuple for ``include='auto'``.""" + """Return the kinds of pattern content to render by availability.""" status_by_name = {status.name: status for status in statuses} optional_point_estimate = ('background', 'residual', 'bragg', 'excluded') @@ -707,44 +642,6 @@ def _auto_include( ) return () - @classmethod - def _validate_requested_include( - cls, - statuses: list[PatternOptionStatus], - include: tuple[str, ...], - ) -> None: - """ - Raise a clear error when a requested include is unavailable. - """ - status_by_name = {status.name: status for status in statuses} - unavailable = [ - option_name - for option_name in include - if option_name != 'auto' and not status_by_name[option_name].available - ] - if unavailable: - option_name = unavailable[0] - msg = status_by_name[option_name].reason - raise ValueError(msg) - - include_set = set(include) - if 'background' in include_set and not {'measured', 'calculated'}.issubset(include_set): - msg = 'background requires both measured and calculated data in the same view.' - raise ValueError(msg) - if 'bragg' in include_set and not {'measured', 'calculated'}.issubset(include_set): - msg = 'bragg requires both measured and calculated data in the same view.' - raise ValueError(msg) - if 'residual' in include_set and not {'measured', 'calculated'}.issubset(include_set): - msg = 'residual requires both measured and calculated data in the same view.' - raise ValueError(msg) - if 'excluded' in include_set and not include_set.intersection({ - 'measured', - 'calculated', - 'uncertainty', - }): - msg = 'excluded requires measured, calculated, or uncertainty data in the same view.' - raise ValueError(msg) - def _show_point_estimate_pattern( self, *, @@ -752,13 +649,11 @@ def _show_point_estimate_pattern( x_min: float | None, x_max: float | None, include: tuple[str, ...], - statuses: list[PatternOptionStatus], x: object | None, ) -> None: """ Dispatch a point-estimate pattern view to the live plotter. """ - self._validate_requested_include(statuses, include) include_set = set(include) if include_set == {'measured'}: self._project.rendering_plot.plotter.plot_meas( From 757246204805ba42a472e2d698c5c23d3e1577b1 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:45:46 +0200 Subject: [PATCH 17/57] Amend display-ux ADR for always-on pattern view --- .../crysview-structure-visualization.md | 23 +++-- docs/dev/adrs/accepted/display-ux.md | 86 ++++++------------- docs/dev/plans/pattern-display-unification.md | 2 +- docs/docs/quick-reference/index.md | 4 +- 4 files changed, 36 insertions(+), 79 deletions(-) diff --git a/docs/dev/adrs/accepted/crysview-structure-visualization.md b/docs/dev/adrs/accepted/crysview-structure-visualization.md index b5fc2281a..0852726f3 100644 --- a/docs/dev/adrs/accepted/crysview-structure-visualization.md +++ b/docs/dev/adrs/accepted/crysview-structure-visualization.md @@ -158,8 +158,7 @@ representation); like the HTML report it can also write a standalone HTML file to a path. The exact return and save signature is left to the implementation plan. -Content selection mirrors `pattern(include=...)` rather than inventing a -new vocabulary: +Content selection uses an `include=` argument: ```python project.display.structure(struct_name='lbco') @@ -181,13 +180,11 @@ toggles the same features after the initial view is drawn, so `include` sets the starting state and the modebar refines it. A companion `project.display.show_structure_options(struct_name=...)` -mirrors the existing `show_pattern_options(expt_name=...)`: it lists -each `include=` option with whether the active engine and the current -structure state support it, and the reason when they do not — for -example `moments` is unavailable until the structure model carries +lists each `include=` option with whether the active engine and the +current structure state support it, and the reason when they do not — +for example `moments` is unavailable until the structure model carries moment fields, and the `ascii` engine reports the features only the 3D -engines draw. This gives the structure view the same per-option -discoverability the pattern view already offers. +engines draw. This gives the structure view per-option discoverability. The view also has a spatial extent: which symmetry-equivalent atoms the scene contains. The scene builder takes the unique (asymmetric-unit) @@ -613,11 +610,11 @@ a per-call request behave predictably: ## Consequences - `project.display` gains a spatial view (`structure()`) that - complements the 1D `pattern()` view and reuses the `include=` - vocabulary. -- `project.display` also gains `show_structure_options()`, parallel to - `show_pattern_options()`, so the supported content for a given - structure and engine is discoverable with reasons. + complements the 1D `pattern()` view with an `include=` feature + selector. +- `project.display` also gains `show_structure_options()`, so the + supported content for a given structure and engine is discoverable + with reasons. - Keeping crystallography in the scene builder and out of renderers lets several front-ends (Three.js now, Qt Quick 3D later) share one model. - A switchable `rendering_structure` category diff --git a/docs/dev/adrs/accepted/display-ux.md b/docs/dev/adrs/accepted/display-ux.md index 40ee2c0d1..06004ce7d 100644 --- a/docs/dev/adrs/accepted/display-ux.md +++ b/docs/dev/adrs/accepted/display-ux.md @@ -82,8 +82,6 @@ project.display.fit.series(param, versus='diffrn.ambient_temperature') project.display.posterior.pairs() project.display.posterior.distribution(param) project.display.posterior.predictive(expt_name='hrpt') - -project.display.show_pattern_options(expt_name='hrpt') ``` `project.analysis.display` is removed from the primary public API. Its @@ -116,8 +114,8 @@ project.display.pattern(expt_name='hrpt') project.display.pattern(expt_name='hrpt', x_min=40, x_max=55) ``` -By default, `pattern()` uses `include='auto'` and displays as much -useful information as the project state supports: +`pattern()` always displays as much useful information as the project +state supports; there is no view-selection argument: - measured data if present - calculated data if linked structure state and calculated intensities @@ -128,65 +126,28 @@ useful information as the project state supports: reflection rows are available - residual if both measured and calculated data are available and the experiment type supports a residual panel -- excluded regions if available on the experiment +- excluded regions whenever defined on the experiment - uncertainty bands where posterior predictive data exists and the chart engine supports them -Specific subsets are selected with `include`: - -```python -project.display.pattern(expt_name='hrpt', include='auto') -project.display.pattern(expt_name='hrpt', include='measured') -project.display.pattern(expt_name='hrpt', include='calculated') -project.display.pattern( - expt_name='hrpt', - include=('measured', 'calculated', 'background', 'residual', 'bragg'), -) -``` - -`include` was chosen over alternatives: - -| Name | Reason not selected | -| ------------- | ----------------------------------------------- | -| `layers` | Sounds graphical rather than user intent. | -| `components` | Precise, but longer. | -| `content` | Too broad. | -| `view` | Better for presets than arbitrary combinations. | -| `series` | Does not fit residual rows or Bragg ticks well. | -| boolean flags | Explicit, but scales poorly. | - -Add discovery for supported pattern content: - -```python -project.display.show_pattern_options(expt_name='hrpt') -``` - -The table shows option name, description, availability for the -experiment, whether `include='auto'` includes it, and the reason an -option is unavailable. - -Pattern option names: - -- `auto` -- `measured` -- `calculated` -- `background` -- `residual` -- `bragg` -- `excluded` -- `uncertainty` - -`uncertainty` is available where posterior predictive data exists for a -supported experiment and the active chart engine can render bands. It is -unavailable, with a clear reason, when no posterior predictive data is -present. - -Explicit combinations are validated against the same project state used -by `include='auto'`. `background`, `bragg`, and `residual` require both -measured and calculated data in the same view. `excluded` requires -measured, calculated, or uncertainty content in the same view, and -excluded-region overlays currently require the experiment's default -x-axis. +Excluded regions are a property of the experiment, not a viewing +choice, so they are always shaded when present, skipped only when a +custom `x` axis variable is selected (the overlay cannot be mapped onto +it). `uncertainty` is shown where posterior predictive data exists for +a supported experiment and the active chart engine can render bands. + +Single-panel views (for example measured-only, before a structure is +linked) and the full composite share one figure-sizing and x-range +core, so a one-row chart is the top row of the multi-row chart pixel +for pixel: the same height and the same tick-to-tick x-range with no +autoscale padding. + +This supersedes the earlier design, which assembled a view from an +`include=('measured', 'calculated', ...)` argument and exposed a +`show_pattern_options()` discovery table. Selecting a strict subset of +the available data is no longer supported; the project is in beta, so +this simplification replaces the previous API rather than carrying a +compatibility shim. ## Deterministic And Bayesian Consistency @@ -230,8 +191,9 @@ users should not need to decide the output type before asking for information. Some outputs may render as a chart or a table depending on backend and state. -Separate `measured()` and `calculated()` methods were rejected because -they duplicate `pattern(..., include=...)`. +Separate `measured()` and `calculated()` methods are unnecessary: +`pattern()` shows every available kind of data directly, so there is no +subset for them to select. ## Consequences diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index 52c2c708e..b2aad9af9 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -104,7 +104,7 @@ composite path (`build_powder_meas_vs_calc_figure`): Commit: `Always render available pattern content; drop include` - [x] **P1.3 — Structure scene top margin.** Fixes issue #4. Commit: `Add top margin above structure scene rectangle` -- [ ] **P1.4 — Amend ADR and docs.** Update `display-ux.md`; drop +- [x] **P1.4 — Amend ADR and docs.** Update `display-ux.md`; drop stale `show_pattern_options` references. Commit: `Amend display-ux ADR for always-on pattern view` - [ ] **P1.5 — Update tutorials.** Remove `include=` from tutorial diff --git a/docs/docs/quick-reference/index.md b/docs/docs/quick-reference/index.md index 6fee6b1f5..b246586d1 100644 --- a/docs/docs/quick-reference/index.md +++ b/docs/docs/quick-reference/index.md @@ -136,7 +136,7 @@ experiment.linked_phases.create(id='lbco', scale=10.0) ## Inspect the Project -Show names, CIF text, and plotting options: +Show names and CIF text: ```python project.structures.show_names() @@ -144,8 +144,6 @@ project.experiments.show_names() structure.show_as_cif() experiment.show_as_cif() - -project.display.show_pattern_options(expt_name='hrpt') ``` Open the main display views: From b12e7620e17286a1cfc18c486df39e5314fe91ea Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:49:30 +0200 Subject: [PATCH 18/57] Apply formatter to pattern docstring and docs --- docs/dev/adrs/accepted/display-ux.md | 16 +-- docs/dev/plans/pattern-display-unification.md | 127 +++++++++--------- src/easydiffraction/project/display.py | 4 +- 3 files changed, 73 insertions(+), 74 deletions(-) diff --git a/docs/dev/adrs/accepted/display-ux.md b/docs/dev/adrs/accepted/display-ux.md index 06004ce7d..31604c944 100644 --- a/docs/dev/adrs/accepted/display-ux.md +++ b/docs/dev/adrs/accepted/display-ux.md @@ -130,16 +130,16 @@ state supports; there is no view-selection argument: - uncertainty bands where posterior predictive data exists and the chart engine supports them -Excluded regions are a property of the experiment, not a viewing -choice, so they are always shaded when present, skipped only when a -custom `x` axis variable is selected (the overlay cannot be mapped onto -it). `uncertainty` is shown where posterior predictive data exists for -a supported experiment and the active chart engine can render bands. +Excluded regions are a property of the experiment, not a viewing choice, +so they are always shaded when present, skipped only when a custom `x` +axis variable is selected (the overlay cannot be mapped onto it). +`uncertainty` is shown where posterior predictive data exists for a +supported experiment and the active chart engine can render bands. Single-panel views (for example measured-only, before a structure is -linked) and the full composite share one figure-sizing and x-range -core, so a one-row chart is the top row of the multi-row chart pixel -for pixel: the same height and the same tick-to-tick x-range with no +linked) and the full composite share one figure-sizing and x-range core, +so a one-row chart is the top row of the multi-row chart pixel for +pixel: the same height and the same tick-to-tick x-range with no autoscale padding. This supersedes the earlier design, which assembled a view from an diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index b2aad9af9..20d15f595 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -27,46 +27,46 @@ composite path (`build_powder_meas_vs_calc_figure`): 1. **HTML height** — the single-panel Plotly figure sets no explicit height, so the docs' lazy skeleton falls back to - `DEFAULT_HEIGHT * PLOTLY_HEIGHT_PER_UNIT = 600px` (the full - 3-panel height) instead of the ~458px main panel. -2. **X-axis offsets** — the single-panel path sets no x-range, so - Plotly auto-pads; the composite pins `(min, max)`. -3. **`include=('measured','excluded')` redundancy** — excluded - regions are a property of the experiment, not a viewing choice. -4. **Structure top margin** — the 3D scene sits flush under its - header line. + `DEFAULT_HEIGHT * PLOTLY_HEIGHT_PER_UNIT = 600px` (the full 3-panel + height) instead of the ~458px main panel. +2. **X-axis offsets** — the single-panel path sets no x-range, so Plotly + auto-pads; the composite pins `(min, max)`. +3. **`include=('measured','excluded')` redundancy** — excluded regions + are a property of the experiment, not a viewing choice. +4. **Structure top margin** — the 3D scene sits flush under its header + line. ## Decisions -- **Drop `include` entirely.** `pattern(expt_name, x_min=None, - x_max=None, *, x=None)` always renders all available content (the - former `include='auto'` behaviour, which is already proven). The - cost — losing strict-subset views such as measured-only once a - calc exists — is accepted; `x_min`/`x_max` and `x` still work. Can - be re-added later only on a concrete need. -- **Excluded regions always shade** when defined on the experiment, - in every view; skipped only when a custom `x` axis is selected - (the overlay can't be mapped). The `'excluded'` token is removed. +- **Drop `include` entirely.** + `pattern(expt_name, x_min=None, x_max=None, *, x=None)` always renders + all available content (the former `include='auto'` behaviour, which is + already proven). The cost — losing strict-subset views such as + measured-only once a calc exists — is accepted; `x_min`/`x_max` and + `x` still work. Can be re-added later only on a concrete need. +- **Excluded regions always shade** when defined on the experiment, in + every view; skipped only when a custom `x` axis is selected (the + overlay can't be mapped). The `'excluded'` token is removed. - **Common rendering base via shared primitives.** The single-panel `plot_powder` derives its figure height from `_single_main_panel_height_pixels(...)` and its x-range from `_composite_x_range(...)` — the same helpers the composite uses. Because `_get_layout` already uses the composite margins - (`r:30, t:40, b:45`), a 1-panel view becomes the composite's main - row pixel-for-pixel. No builder merge (the composite hard-requires + (`r:30, t:40, b:45`), a 1-panel view becomes the composite's main row + pixel-for-pixel. No builder merge (the composite hard-requires `y_calc`); the shared helpers are the single source of truth. - **Keep the internal availability engine** (`_pattern_option_statuses`, `_auto_include`, `PatternOptionStatus`); only the user-facing selection/validation/discovery layer is removed. -- The residual-fraction default moves to `plotters/base.py` so both - the facade (`plotting.py`) and backend (`plotly.py`) can import it - without a circular dependency. +- The residual-fraction default moves to `plotters/base.py` so both the + facade (`plotting.py`) and backend (`plotly.py`) can import it without + a circular dependency. ## Open questions - Branch choice (above). -- Should `pattern()` keep accepting a custom `x` axis variable? - Assumed **yes** (separate from `include`). +- Should `pattern()` keep accepting a custom `x` axis variable? Assumed + **yes** (separate from `include`). ## Concrete files likely to change @@ -74,48 +74,46 @@ composite path (`build_powder_meas_vs_calc_figure`): `DEFAULT_RESIDUAL_HEIGHT_FRACTION`. - `src/easydiffraction/display/plotters/plotly.py` — `plot_powder` explicit height + tight x-range. -- `src/easydiffraction/display/plotting.py` — import the moved - constant. -- `src/easydiffraction/project/display.py` — rewrite `pattern()`; - remove `include`, `_normalize_include`, - `_validate_requested_include`, `show_pattern_options`; simplify - `_show_point_estimate_pattern`. -- `src/easydiffraction/display/structure/templates/structure.html.j2` - — scene top margin (done). +- `src/easydiffraction/display/plotting.py` — import the moved constant. +- `src/easydiffraction/project/display.py` — rewrite `pattern()`; remove + `include`, `_normalize_include`, `_validate_requested_include`, + `show_pattern_options`; simplify `_show_point_estimate_pattern`. +- `src/easydiffraction/display/structure/templates/structure.html.j2` — + scene top margin (done). - `docs/dev/adrs/accepted/display-ux.md` — amend Pattern Display. - `docs/dev/adrs/accepted/crysview-structure-visualization.md`, `docs/docs/quick-reference/index.md` — drop stale `show_pattern_options` references. -- `docs/docs/tutorials/ed-3.py`, `ed-9.py`, `ed-11.py`, `ed-13.py` - (+ regenerated `.ipynb`) — remove `include=`. +- `docs/docs/tutorials/ed-3.py`, `ed-9.py`, `ed-11.py`, `ed-13.py` (+ + regenerated `.ipynb`) — remove `include=`. - Phase 2: `tests/unit/easydiffraction/project/test_display.py`, `tests/integration/fitting/test_plotting.py`. ## Implementation steps (Phase 1) - [x] **P1.1 — Unify single-panel sizing/x-range.** Move - `DEFAULT_RESIDUAL_HEIGHT_FRACTION` to `base.py`; `plot_powder` sets - explicit main-panel height and tight `(min,max)` x-range. Fixes - issues #1 and #2. - Commit: `Match single-panel pattern height and x-range to composite` + `DEFAULT_RESIDUAL_HEIGHT_FRACTION` to `base.py`; `plot_powder` + sets explicit main-panel height and tight `(min,max)` x-range. + Fixes issues #1 and #2. Commit: + `Match single-panel pattern height and x-range to composite` - [x] **P1.2 — Drop `include`; always render available content.** - Rewrite `pattern()`; remove the selection/validation/discovery - layer; always-shade excluded. Fixes issue #3. - Commit: `Always render available pattern content; drop include` -- [x] **P1.3 — Structure scene top margin.** Fixes issue #4. - Commit: `Add top margin above structure scene rectangle` -- [x] **P1.4 — Amend ADR and docs.** Update `display-ux.md`; drop - stale `show_pattern_options` references. - Commit: `Amend display-ux ADR for always-on pattern view` + Rewrite `pattern()`; remove the selection/validation/discovery + layer; always-shade excluded. Fixes issue #3. Commit: + `Always render available pattern content; drop include` +- [x] **P1.3 — Structure scene top margin.** Fixes issue #4. Commit: + `Add top margin above structure scene rectangle` +- [x] **P1.4 — Amend ADR and docs.** Update `display-ux.md`; drop stale + `show_pattern_options` references. Commit: + `Amend display-ux ADR for always-on pattern view` - [ ] **P1.5 — Update tutorials.** Remove `include=` from tutorial - sources; `pixi run notebook-prepare`. - Commit: `Drop include= from tutorials for unified pattern view` -- [ ] **P1.6 — Phase 1 review gate.** - Commit: `Reach Phase 1 review gate` + sources; `pixi run notebook-prepare`. Commit: + `Drop include= from tutorials for unified pattern view` +- [ ] **P1.6 — Phase 1 review gate.** Commit: + `Reach Phase 1 review gate` Each completed P1 step is staged with explicit paths and committed -locally before the next step (per `AGENTS.md` Commits). Stop after -Phase 1 for review before Phase 2. +locally before the next step (per `AGENTS.md` Commits). Stop after Phase +1 for review before Phase 2. ## Phase 2 — Verification @@ -128,9 +126,8 @@ pixi run script-tests ``` Test updates: remove `include=`/`show_pattern_options` tests; add -coverage for always-shown excluded regions, single-panel height + -tight x-range matching the composite main row, and the -nothing-to-plot error. +coverage for always-shown excluded regions, single-panel height + tight +x-range matching the composite main row, and the nothing-to-plot error. ## Status checklist @@ -142,13 +139,13 @@ nothing-to-plot error. **Title:** Simplify and unify the experiment pattern view -**Description:** `project.display.pattern()` now always shows -everything the data supports — measured and calculated curves, the -residual, Bragg ticks, background, excluded regions, and uncertainty -bands — so you no longer pass `include=...` to assemble a view; just -call `pattern(expt_name=...)` and zoom with `x_min`/`x_max`. Excluded -regions are always shaded when defined. Single-panel and full -three-panel charts now share one layout, so a measured-only plot is -exactly the top panel of the full view — fixing the oversized height -in the HTML docs and the stray left/right margins. The 3D structure -view also gains a little breathing room below its title. +**Description:** `project.display.pattern()` now always shows everything +the data supports — measured and calculated curves, the residual, Bragg +ticks, background, excluded regions, and uncertainty bands — so you no +longer pass `include=...` to assemble a view; just call +`pattern(expt_name=...)` and zoom with `x_min`/`x_max`. Excluded regions +are always shaded when defined. Single-panel and full three-panel charts +now share one layout, so a measured-only plot is exactly the top panel +of the full view — fixing the oversized height in the HTML docs and the +stray left/right margins. The 3D structure view also gains a little +breathing room below its title. diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 67426fcd6..d7b3176f6 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -612,7 +612,9 @@ def _auto_include( cls, statuses: list[PatternOptionStatus], ) -> tuple[str, ...]: - """Return the kinds of pattern content to render by availability.""" + """ + Return the kinds of pattern content to render by availability. + """ status_by_name = {status.name: status for status in statuses} optional_point_estimate = ('background', 'residual', 'bragg', 'excluded') From 9d4a80d912f3a32652258d2cf155ae3ecd039665 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:54:38 +0200 Subject: [PATCH 19/57] Drop include= from tutorials for unified pattern view --- docs/dev/plans/pattern-display-unification.md | 2 +- docs/docs/tutorials/ed-11.ipynb | 2 +- docs/docs/tutorials/ed-11.py | 2 +- docs/docs/tutorials/ed-13.ipynb | 94 ++++----- docs/docs/tutorials/ed-13.py | 10 +- docs/docs/tutorials/ed-3.ipynb | 184 ++++++++---------- docs/docs/tutorials/ed-3.py | 8 +- docs/docs/tutorials/ed-9.ipynb | 4 +- docs/docs/tutorials/ed-9.py | 4 +- 9 files changed, 145 insertions(+), 165 deletions(-) diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index 20d15f595..b6ef099d4 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -105,7 +105,7 @@ composite path (`build_powder_meas_vs_calc_figure`): - [x] **P1.4 — Amend ADR and docs.** Update `display-ux.md`; drop stale `show_pattern_options` references. Commit: `Amend display-ux ADR for always-on pattern view` -- [ ] **P1.5 — Update tutorials.** Remove `include=` from tutorial +- [x] **P1.5 — Update tutorials.** Remove `include=` from tutorial sources; `pixi run notebook-prepare`. Commit: `Drop include= from tutorials for unified pattern view` - [ ] **P1.6 — Phase 1 review gate.** Commit: diff --git a/docs/docs/tutorials/ed-11.ipynb b/docs/docs/tutorials/ed-11.ipynb index 582c1ae85..47c4d0df0 100644 --- a/docs/docs/tutorials/ed-11.ipynb +++ b/docs/docs/tutorials/ed-11.ipynb @@ -289,7 +289,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.pattern(expt_name='nomad', include=('measured', 'calculated'))" + "project.display.pattern(expt_name='nomad')" ] }, { diff --git a/docs/docs/tutorials/ed-11.py b/docs/docs/tutorials/ed-11.py index 18aedca0e..2d000d892 100644 --- a/docs/docs/tutorials/ed-11.py +++ b/docs/docs/tutorials/ed-11.py @@ -111,7 +111,7 @@ # ### Display Pattern # %% -project.display.pattern(expt_name='nomad', include=('measured', 'calculated')) +project.display.pattern(expt_name='nomad') # %% [markdown] # ## 💾 Save Project diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index d78d659d9..f147d3228 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -59,7 +59,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/first-steps/#importing-easydiffraction)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/first-steps/#importing-easydiffraction)\n", "for more details about importing the EasyDiffraction library and its\n", "components." ] @@ -107,7 +107,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/project/)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/project/)\n", "for more details about creating a project and its purpose in the\n", "analysis workflow." ] @@ -163,7 +163,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/)\n", "for more details about experiments and their purpose in the analysis\n", "workflow." ] @@ -221,7 +221,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#defining-an-experiment-manually)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#defining-an-experiment-manually)\n", "for more details about different types of experiments." ] }, @@ -263,11 +263,11 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#measured-data-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#measured-data-category)\n", "for more details about the measured data and its format.\n", "\n", "To visualize the measured data, we can use the `pattern` method of\n", - "the project's `display` facade with `include='measured'`." + "the project's `display` facade." ] }, { @@ -277,7 +277,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.pattern(expt_name='sim_si', include='measured')" + "project_1.display.pattern(expt_name='sim_si')" ] }, { @@ -309,7 +309,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#excluded-regions-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#excluded-regions-category)\n", "for more details about excluding regions from the measured data." ] }, @@ -341,7 +341,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.pattern(expt_name='sim_si', include=('measured', 'excluded'))" + "project_1.display.pattern(expt_name='sim_si')" ] }, { @@ -373,7 +373,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#instrument-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#instrument-category)\n", "for more details about the instrument parameters." ] }, @@ -460,7 +460,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/parameters/)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/parameters/)\n", "for more details about parameters in EasyDiffraction and their\n", "attributes." ] @@ -527,7 +527,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#peak-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#peak-category)\n", "for more details about the peak profile types." ] }, @@ -598,7 +598,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#background-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#background-category)\n", "for more details about the background and its types." ] }, @@ -655,15 +655,16 @@ "which is the length of the unit cell edge. The Si crystal structure\n", "has a single atom in the unit cell, which is located at the origin (0,\n", "0, 0) of the unit cell. The symmetry of this site is defined by the\n", - "Wyckoff letter 'a'. The atomic displacement parameter defines the\n", - "thermal vibrations of the atoms in the unit cell and is presented as\n", - "an isotropic parameter (B_iso).\n", + "Wyckoff letter 'a', which is assigned automatically based on the space\n", + "group and the atomic coordinates. The atomic displacement parameter\n", + "defines the thermal vibrations of the atoms in the unit cell and is\n", + "presented as an isotropic parameter (B_iso).\n", "\n", "Sometimes, the initial crystal structure parameters can be obtained\n", "from one of the crystallographic databases, like for example the\n", "Crystallography Open Database (COD). In this case, we use the COD\n", "entry for silicon as a reference for the initial crystal structure\n", - "model: https://www.crystallography.net/cod/4507226.html\n", + "model: https://www.crystallography.net/cod/9008565.html\n", "\n", "Usually, the crystal structure parameters are provided in a CIF file\n", "format, which is a standard format for crystallographic data. An\n", @@ -678,7 +679,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/data-format/)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/data-format/)\n", "for more details about the CIF format and its use in EasyDiffraction." ] }, @@ -691,11 +692,11 @@ "data_si\n", "\n", "_space_group.name_H-M_alt \"F d -3 m\"\n", - "_space_group.IT_coordinate_system_code 2\n", + "_space_group.IT_coordinate_system_code 1\n", "\n", - "_cell.length_a 5.43\n", - "_cell.length_b 5.43\n", - "_cell.length_c 5.43\n", + "_cell.length_a 5.4307\n", + "_cell.length_b 5.4307\n", + "_cell.length_c 5.4307\n", "_cell.angle_alpha 90.0\n", "_cell.angle_beta 90.0\n", "_cell.angle_gamma 90.0\n", @@ -706,11 +707,10 @@ "_atom_site.fract_x\n", "_atom_site.fract_y\n", "_atom_site.fract_z\n", - "_atom_site.wyckoff_letter\n", "_atom_site.occupancy\n", "_atom_site.ADP_type\n", "_atom_site.B_iso_or_equiv\n", - "Si Si 0.125 0.125 0.125 a 1.0 Biso 0.89\n", + "Si Si 0 0 0 1.0 Biso 0.89\n", "```" ] }, @@ -730,7 +730,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/)\n", "for more details about structures and their purpose in the data\n", "analysis workflow." ] @@ -767,7 +767,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#space-group-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/#space-group-category)\n", "for more details about the space group." ] }, @@ -779,7 +779,7 @@ "outputs": [], "source": [ "project_1.structures['si'].space_group.name_h_m = 'F d -3 m'\n", - "project_1.structures['si'].space_group.it_coordinate_system_code = '2'" + "project_1.structures['si'].space_group.it_coordinate_system_code = '1'" ] }, { @@ -796,7 +796,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#cell-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/#cell-category)\n", "for more details about the unit cell parameters." ] }, @@ -807,7 +807,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.structures['si'].cell.length_a = 5.43" + "project_1.structures['si'].cell.length_a = 5.4307" ] }, { @@ -824,7 +824,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#atom-sites-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/structure/#atom-sites-category)\n", "for more details about the atom sites category." ] }, @@ -838,9 +838,10 @@ "project_1.structures['si'].atom_sites.create(\n", " label='Si',\n", " type_symbol='Si',\n", - " fract_x=0.125,\n", - " fract_y=0.125,\n", - " fract_z=0.125,\n", + " fract_x=0.0,\n", + " fract_y=0.0,\n", + " fract_z=0.0,\n", + " adp_type='Biso',\n", " adp_iso=0.89,\n", ")" ] @@ -885,7 +886,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#linked-phases-category)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/experiment/#linked-phases-category)\n", "for more details about linking a structure to an experiment." ] }, @@ -944,7 +945,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/analysis/#minimization-optimization)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/analysis/#minimization-optimization)\n", "for more details about the fitting process in EasyDiffraction." ] }, @@ -1067,7 +1068,7 @@ "metadata": {}, "source": [ "📖 See\n", - "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/analysis/#perform-fit)\n", + "[documentation](https://docs.easydiffraction.org/lib/latest/user-guide/analysis-workflow/analysis/#perform-fit)\n", "for more details about the fitting process." ] }, @@ -1368,12 +1369,12 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.pattern(expt_name='sim_lbco', include='measured')\n", + "project_2.display.pattern(expt_name='sim_lbco')\n", "\n", "project_2.experiments['sim_lbco'].excluded_regions.create(id='1', start=0, end=55000)\n", "project_2.experiments['sim_lbco'].excluded_regions.create(id='2', start=105500, end=200000)\n", "\n", - "project_2.display.pattern(expt_name='sim_lbco', include=('measured', 'excluded'))" + "project_2.display.pattern(expt_name='sim_lbco')" ] }, { @@ -1578,14 +1579,13 @@ "_atom_site.fract_x\n", "_atom_site.fract_y\n", "_atom_site.fract_z\n", - "_atom_site.wyckoff_letter\n", "_atom_site.occupancy\n", "_atom_site.ADP_type\n", "_atom_site.B_iso_or_equiv\n", - "La La 0.0 0.0 0.0 a 0.5 Biso 0.95\n", - "Ba Ba 0.0 0.0 0.0 a 0.5 Biso 0.95\n", - "Co Co 0.5 0.5 0.5 b 1.0 Biso 0.80\n", - "O O 0.0 0.5 0.5 c 1.0 Biso 1.66\n", + "La La 0.0 0.0 0.0 0.5 Biso 0.95\n", + "Ba Ba 0.0 0.0 0.0 0.5 Biso 0.95\n", + "Co Co 0.5 0.5 0.5 1.0 Biso 0.80\n", + "O O 0.0 0.5 0.5 1.0 Biso 1.66\n", "```" ] }, @@ -1804,8 +1804,9 @@ " fract_x=0,\n", " fract_y=0,\n", " fract_z=0,\n", - " adp_iso=0.95,\n", " occupancy=0.5,\n", + " adp_type='Biso',\n", + " adp_iso=0.95,\n", ")\n", "project_2.structures['lbco'].atom_sites.create(\n", " label='Ba',\n", @@ -1813,8 +1814,9 @@ " fract_x=0,\n", " fract_y=0,\n", " fract_z=0,\n", - " adp_iso=0.95,\n", " occupancy=0.5,\n", + " adp_type='Biso',\n", + " adp_iso=0.95,\n", ")\n", "project_2.structures['lbco'].atom_sites.create(\n", " label='Co',\n", @@ -1822,6 +1824,7 @@ " fract_x=0.5,\n", " fract_y=0.5,\n", " fract_z=0.5,\n", + " adp_type='Biso',\n", " adp_iso=0.80,\n", ")\n", "project_2.structures['lbco'].atom_sites.create(\n", @@ -1830,6 +1833,7 @@ " fract_x=0,\n", " fract_y=0.5,\n", " fract_z=0.5,\n", + " adp_type='Biso',\n", " adp_iso=1.66,\n", ")" ] diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index 8ed16fa13..10b26e35b 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -145,10 +145,10 @@ # for more details about the measured data and its format. # # To visualize the measured data, we can use the `pattern` method of -# the project's `display` facade with `include='measured'`. +# the project's `display` facade. # %% -project_1.display.pattern(expt_name='sim_si', include='measured') +project_1.display.pattern(expt_name='sim_si') # %% [markdown] # If you zoom in on the highest TOF peak (around 120,000 μs), you will @@ -183,7 +183,7 @@ # the plot and is not used in the fitting process. # %% -project_1.display.pattern(expt_name='sim_si', include=('measured', 'excluded')) +project_1.display.pattern(expt_name='sim_si') # %% [markdown] # #### Set Instrument @@ -798,12 +798,12 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.pattern(expt_name='sim_lbco', include='measured') +project_2.display.pattern(expt_name='sim_lbco') project_2.experiments['sim_lbco'].excluded_regions.create(id='1', start=0, end=55000) project_2.experiments['sim_lbco'].excluded_regions.create(id='2', start=105500, end=200000) -project_2.display.pattern(expt_name='sim_lbco', include=('measured', 'excluded')) +project_2.display.pattern(expt_name='sim_lbco') # %% [markdown] # #### Exercise 2.2: Set Instrument diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index d97820d47..e12fede2d 100644 --- a/docs/docs/tutorials/ed-3.ipynb +++ b/docs/docs/tutorials/ed-3.ipynb @@ -594,7 +594,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.pattern(expt_name='hrpt', include='measured')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -907,24 +907,6 @@ "cell_type": "markdown", "id": "85", "metadata": {}, - "source": [ - "### Show Calculated Data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "86", - "metadata": {}, - "outputs": [], - "source": [ - "project.display.pattern(expt_name='hrpt', include='calculated')" - ] - }, - { - "cell_type": "markdown", - "id": "87", - "metadata": {}, "source": [ "### Display Pattern" ] @@ -932,7 +914,7 @@ { "cell_type": "code", "execution_count": null, - "id": "88", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -942,7 +924,7 @@ { "cell_type": "code", "execution_count": null, - "id": "89", + "id": "87", "metadata": {}, "outputs": [], "source": [ @@ -951,7 +933,7 @@ }, { "cell_type": "markdown", - "id": "90", + "id": "88", "metadata": {}, "source": [ "### Display Parameters\n", @@ -962,7 +944,7 @@ { "cell_type": "code", "execution_count": null, - "id": "91", + "id": "89", "metadata": {}, "outputs": [], "source": [ @@ -971,7 +953,7 @@ }, { "cell_type": "markdown", - "id": "92", + "id": "90", "metadata": {}, "source": [ "Show all fittable parameters." @@ -980,7 +962,7 @@ { "cell_type": "code", "execution_count": null, - "id": "93", + "id": "91", "metadata": {}, "outputs": [], "source": [ @@ -989,7 +971,7 @@ }, { "cell_type": "markdown", - "id": "94", + "id": "92", "metadata": {}, "source": [ "Show only free parameters." @@ -998,7 +980,7 @@ { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "93", "metadata": {}, "outputs": [], "source": [ @@ -1007,7 +989,7 @@ }, { "cell_type": "markdown", - "id": "96", + "id": "94", "metadata": {}, "source": [ "Show how to access parameters in the code." @@ -1016,7 +998,7 @@ { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "95", "metadata": {}, "outputs": [], "source": [ @@ -1025,7 +1007,7 @@ }, { "cell_type": "markdown", - "id": "98", + "id": "96", "metadata": {}, "source": [ "### Set Fit Mode\n", @@ -1036,7 +1018,7 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "97", "metadata": {}, "outputs": [], "source": [ @@ -1045,7 +1027,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "98", "metadata": {}, "source": [ "Select desired fit mode." @@ -1054,7 +1036,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "99", "metadata": {}, "outputs": [], "source": [ @@ -1063,7 +1045,7 @@ }, { "cell_type": "markdown", - "id": "102", + "id": "100", "metadata": {}, "source": [ "### Set Minimizer\n", @@ -1074,7 +1056,7 @@ { "cell_type": "code", "execution_count": null, - "id": "103", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1083,7 +1065,7 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "102", "metadata": {}, "source": [ "Select desired fitting engine." @@ -1092,7 +1074,7 @@ { "cell_type": "code", "execution_count": null, - "id": "105", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1101,7 +1083,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "104", "metadata": {}, "source": [ "### Perform Fit 1/5\n", @@ -1112,7 +1094,7 @@ { "cell_type": "code", "execution_count": null, - "id": "107", + "id": "105", "metadata": {}, "outputs": [], "source": [ @@ -1121,7 +1103,7 @@ }, { "cell_type": "markdown", - "id": "108", + "id": "106", "metadata": {}, "source": [ "Set experiment parameters to be refined." @@ -1130,7 +1112,7 @@ { "cell_type": "code", "execution_count": null, - "id": "109", + "id": "107", "metadata": {}, "outputs": [], "source": [ @@ -1145,7 +1127,7 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "108", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1154,7 +1136,7 @@ { "cell_type": "code", "execution_count": null, - "id": "111", + "id": "109", "metadata": {}, "outputs": [], "source": [ @@ -1163,7 +1145,7 @@ }, { "cell_type": "markdown", - "id": "112", + "id": "110", "metadata": {}, "source": [ "#### Run Fitting" @@ -1172,7 +1154,7 @@ { "cell_type": "code", "execution_count": null, - "id": "113", + "id": "111", "metadata": {}, "outputs": [], "source": [ @@ -1182,7 +1164,7 @@ }, { "cell_type": "markdown", - "id": "114", + "id": "112", "metadata": {}, "source": [ "#### Display Pattern" @@ -1191,7 +1173,7 @@ { "cell_type": "code", "execution_count": null, - "id": "115", + "id": "113", "metadata": {}, "outputs": [], "source": [ @@ -1201,7 +1183,7 @@ { "cell_type": "code", "execution_count": null, - "id": "116", + "id": "114", "metadata": {}, "outputs": [], "source": [ @@ -1210,7 +1192,7 @@ }, { "cell_type": "markdown", - "id": "117", + "id": "115", "metadata": {}, "source": [ "### Perform Fit 2/5\n", @@ -1221,7 +1203,7 @@ { "cell_type": "code", "execution_count": null, - "id": "118", + "id": "116", "metadata": {}, "outputs": [], "source": [ @@ -1233,7 +1215,7 @@ }, { "cell_type": "markdown", - "id": "119", + "id": "117", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1242,7 +1224,7 @@ { "cell_type": "code", "execution_count": null, - "id": "120", + "id": "118", "metadata": {}, "outputs": [], "source": [ @@ -1251,7 +1233,7 @@ }, { "cell_type": "markdown", - "id": "121", + "id": "119", "metadata": {}, "source": [ "#### Run Fitting" @@ -1260,7 +1242,7 @@ { "cell_type": "code", "execution_count": null, - "id": "122", + "id": "120", "metadata": {}, "outputs": [], "source": [ @@ -1270,7 +1252,7 @@ }, { "cell_type": "markdown", - "id": "123", + "id": "121", "metadata": {}, "source": [ "#### Display Pattern" @@ -1279,7 +1261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "124", + "id": "122", "metadata": {}, "outputs": [], "source": [ @@ -1289,7 +1271,7 @@ { "cell_type": "code", "execution_count": null, - "id": "125", + "id": "123", "metadata": {}, "outputs": [], "source": [ @@ -1298,7 +1280,7 @@ }, { "cell_type": "markdown", - "id": "126", + "id": "124", "metadata": {}, "source": [ "#### Save Project State" @@ -1307,7 +1289,7 @@ { "cell_type": "code", "execution_count": null, - "id": "127", + "id": "125", "metadata": {}, "outputs": [], "source": [ @@ -1316,7 +1298,7 @@ }, { "cell_type": "markdown", - "id": "128", + "id": "126", "metadata": {}, "source": [ "### Perform Fit 3/5\n", @@ -1327,7 +1309,7 @@ { "cell_type": "code", "execution_count": null, - "id": "129", + "id": "127", "metadata": {}, "outputs": [], "source": [ @@ -1339,7 +1321,7 @@ }, { "cell_type": "markdown", - "id": "130", + "id": "128", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1348,7 +1330,7 @@ { "cell_type": "code", "execution_count": null, - "id": "131", + "id": "129", "metadata": {}, "outputs": [], "source": [ @@ -1357,7 +1339,7 @@ }, { "cell_type": "markdown", - "id": "132", + "id": "130", "metadata": {}, "source": [ "#### Run Fitting" @@ -1366,7 +1348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "133", + "id": "131", "metadata": {}, "outputs": [], "source": [ @@ -1376,7 +1358,7 @@ }, { "cell_type": "markdown", - "id": "134", + "id": "132", "metadata": {}, "source": [ "#### Display Pattern" @@ -1385,7 +1367,7 @@ { "cell_type": "code", "execution_count": null, - "id": "135", + "id": "133", "metadata": {}, "outputs": [], "source": [ @@ -1395,7 +1377,7 @@ { "cell_type": "code", "execution_count": null, - "id": "136", + "id": "134", "metadata": {}, "outputs": [], "source": [ @@ -1404,7 +1386,7 @@ }, { "cell_type": "markdown", - "id": "137", + "id": "135", "metadata": {}, "source": [ "### Perform Fit 4/5\n", @@ -1417,7 +1399,7 @@ { "cell_type": "code", "execution_count": null, - "id": "138", + "id": "136", "metadata": {}, "outputs": [], "source": [ @@ -1433,7 +1415,7 @@ }, { "cell_type": "markdown", - "id": "139", + "id": "137", "metadata": {}, "source": [ "Set constraints." @@ -1442,7 +1424,7 @@ { "cell_type": "code", "execution_count": null, - "id": "140", + "id": "138", "metadata": {}, "outputs": [], "source": [ @@ -1451,7 +1433,7 @@ }, { "cell_type": "markdown", - "id": "141", + "id": "139", "metadata": {}, "source": [ "Show defined constraints." @@ -1460,7 +1442,7 @@ { "cell_type": "code", "execution_count": null, - "id": "142", + "id": "140", "metadata": {}, "outputs": [], "source": [ @@ -1469,7 +1451,7 @@ }, { "cell_type": "markdown", - "id": "143", + "id": "141", "metadata": {}, "source": [ "Show free parameters." @@ -1478,7 +1460,7 @@ { "cell_type": "code", "execution_count": null, - "id": "144", + "id": "142", "metadata": {}, "outputs": [], "source": [ @@ -1487,7 +1469,7 @@ }, { "cell_type": "markdown", - "id": "145", + "id": "143", "metadata": {}, "source": [ "#### Run Fitting" @@ -1496,7 +1478,7 @@ { "cell_type": "code", "execution_count": null, - "id": "146", + "id": "144", "metadata": {}, "outputs": [], "source": [ @@ -1506,7 +1488,7 @@ }, { "cell_type": "markdown", - "id": "147", + "id": "145", "metadata": {}, "source": [ "#### Display Pattern" @@ -1515,7 +1497,7 @@ { "cell_type": "code", "execution_count": null, - "id": "148", + "id": "146", "metadata": {}, "outputs": [], "source": [ @@ -1525,7 +1507,7 @@ { "cell_type": "code", "execution_count": null, - "id": "149", + "id": "147", "metadata": {}, "outputs": [], "source": [ @@ -1534,7 +1516,7 @@ }, { "cell_type": "markdown", - "id": "150", + "id": "148", "metadata": {}, "source": [ "### Perform Fit 5/5\n", @@ -1547,7 +1529,7 @@ { "cell_type": "code", "execution_count": null, - "id": "151", + "id": "149", "metadata": {}, "outputs": [], "source": [ @@ -1563,7 +1545,7 @@ }, { "cell_type": "markdown", - "id": "152", + "id": "150", "metadata": {}, "source": [ "Set more constraints." @@ -1572,7 +1554,7 @@ { "cell_type": "code", "execution_count": null, - "id": "153", + "id": "151", "metadata": {}, "outputs": [], "source": [ @@ -1583,7 +1565,7 @@ }, { "cell_type": "markdown", - "id": "154", + "id": "152", "metadata": {}, "source": [ "Show defined constraints." @@ -1592,7 +1574,7 @@ { "cell_type": "code", "execution_count": null, - "id": "155", + "id": "153", "metadata": { "lines_to_next_cell": 2 }, @@ -1603,7 +1585,7 @@ }, { "cell_type": "markdown", - "id": "156", + "id": "154", "metadata": {}, "source": [ "Set structure parameters to be refined." @@ -1612,7 +1594,7 @@ { "cell_type": "code", "execution_count": null, - "id": "157", + "id": "155", "metadata": {}, "outputs": [], "source": [ @@ -1621,7 +1603,7 @@ }, { "cell_type": "markdown", - "id": "158", + "id": "156", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1630,7 +1612,7 @@ { "cell_type": "code", "execution_count": null, - "id": "159", + "id": "157", "metadata": {}, "outputs": [], "source": [ @@ -1639,7 +1621,7 @@ }, { "cell_type": "markdown", - "id": "160", + "id": "158", "metadata": {}, "source": [ "#### Run Fitting" @@ -1648,7 +1630,7 @@ { "cell_type": "code", "execution_count": null, - "id": "161", + "id": "159", "metadata": {}, "outputs": [], "source": [ @@ -1659,7 +1641,7 @@ }, { "cell_type": "markdown", - "id": "162", + "id": "160", "metadata": {}, "source": [ "#### Display Pattern" @@ -1668,7 +1650,7 @@ { "cell_type": "code", "execution_count": null, - "id": "163", + "id": "161", "metadata": {}, "outputs": [], "source": [ @@ -1678,7 +1660,7 @@ { "cell_type": "code", "execution_count": null, - "id": "164", + "id": "162", "metadata": {}, "outputs": [], "source": [ @@ -1688,7 +1670,7 @@ { "cell_type": "code", "execution_count": null, - "id": "165", + "id": "163", "metadata": {}, "outputs": [], "source": [ @@ -1697,7 +1679,7 @@ }, { "cell_type": "markdown", - "id": "166", + "id": "164", "metadata": {}, "source": [ "#### Display Structure" @@ -1706,7 +1688,7 @@ { "cell_type": "code", "execution_count": null, - "id": "167", + "id": "165", "metadata": {}, "outputs": [], "source": [ @@ -1715,7 +1697,7 @@ }, { "cell_type": "markdown", - "id": "168", + "id": "166", "metadata": {}, "source": [ "## 📊 Report\n", @@ -1735,7 +1717,7 @@ { "cell_type": "code", "execution_count": null, - "id": "169", + "id": "167", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py index 102be7af3..ca82d131e 100644 --- a/docs/docs/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -257,7 +257,7 @@ # ### Show Measured Data # %% -project.display.pattern(expt_name='hrpt', include='measured') +project.display.pattern(expt_name='hrpt') # %% [markdown] # ### Set Instrument @@ -380,12 +380,6 @@ # %% project.rendering_plot.help() -# %% [markdown] -# ### Show Calculated Data - -# %% -project.display.pattern(expt_name='hrpt', include='calculated') - # %% [markdown] # ### Display Pattern diff --git a/docs/docs/tutorials/ed-9.ipynb b/docs/docs/tutorials/ed-9.ipynb index 4daf26a2a..23e128637 100644 --- a/docs/docs/tutorials/ed-9.ipynb +++ b/docs/docs/tutorials/ed-9.ipynb @@ -523,7 +523,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.pattern(expt_name='mcstas', include='measured')" + "project.display.pattern(expt_name='mcstas')" ] }, { @@ -578,7 +578,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.pattern(expt_name='mcstas', include=('measured', 'excluded'))" + "project.display.pattern(expt_name='mcstas')" ] }, { diff --git a/docs/docs/tutorials/ed-9.py b/docs/docs/tutorials/ed-9.py index 6308a0b8c..21a12bc16 100644 --- a/docs/docs/tutorials/ed-9.py +++ b/docs/docs/tutorials/ed-9.py @@ -229,7 +229,7 @@ # Show measured data as loaded from the file. # %% -project.display.pattern(expt_name='mcstas', include='measured') +project.display.pattern(expt_name='mcstas') # %% [markdown] # Add excluded regions. @@ -248,7 +248,7 @@ # Show measured data after adding excluded regions. # %% -project.display.pattern(expt_name='mcstas', include=('measured', 'excluded')) +project.display.pattern(expt_name='mcstas') # %% [markdown] # Show experiment as CIF. From b1a952c44c41f11033ae73d7341a858060eeaf3f Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:54:53 +0200 Subject: [PATCH 20/57] Reach Phase 1 review gate --- docs/dev/plans/pattern-display-unification.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index b6ef099d4..5cf7e2a1b 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -108,7 +108,7 @@ composite path (`build_powder_meas_vs_calc_figure`): - [x] **P1.5 — Update tutorials.** Remove `include=` from tutorial sources; `pixi run notebook-prepare`. Commit: `Drop include= from tutorials for unified pattern view` -- [ ] **P1.6 — Phase 1 review gate.** Commit: +- [x] **P1.6 — Phase 1 review gate.** Commit: `Reach Phase 1 review gate` Each completed P1 step is staged with explicit paths and committed @@ -131,7 +131,7 @@ x-range matching the composite main row, and the nothing-to-plot error. ## Status checklist -- [ ] Phase 1 implementation complete +- [x] Phase 1 implementation complete - [ ] Phase 1 reviewed - [ ] Phase 2 verification complete From e964aaf8d70f8fc1154a1b628ba942ac2cf5d9bd Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 3 Jun 2026 23:57:27 +0200 Subject: [PATCH 21/57] Add Raises section to pattern docstring --- src/easydiffraction/project/display.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index d7b3176f6..bb0d1774b 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -381,6 +381,11 @@ def pattern( x : object | None, default=None Optional x-axis variable overriding the experiment default (excluded-region overlays are skipped for a custom axis). + + Raises + ------ + ValueError + If no pattern content is available for the experiment. """ statuses = self._pattern_option_statuses(expt_name) content = self._auto_include(statuses) From ece8f906ca4212caaf0e4adb604bfedf613a32a0 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 00:18:48 +0200 Subject: [PATCH 22/57] Set single-panel height via layout construction --- src/easydiffraction/display/plotters/plotly.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index 00e66dbf8..70916c032 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -1999,6 +1999,7 @@ def _get_layout( *, axis_range: tuple[float, float] | None = None, axis_dtick: float | None = None, + height: int | None = None, ) -> object: """ Create a Plotly layout configuration. @@ -2016,6 +2017,8 @@ def _get_layout( axis_dtick : float | None, default=None When given, the same tick step applied to both axes, so the x and y ticks match. + height : int | None, default=None + Explicit figure height in pixels; ``None`` auto-sizes. Returns ------- @@ -2077,6 +2080,7 @@ def _get_layout( 'yanchor': 'top', 'y': 0.99, }, + height=height, xaxis=xaxis, yaxis=yaxis, shapes=shapes, @@ -2125,21 +2129,19 @@ def plot_powder( trace = self._get_powder_trace(x, y, label) data.append(trace) - layout = self._get_layout( - title, - axes_labels, - ) - - fig = self._get_figure(data, layout) # Share the composite's sizing and range primitives so a single # panel is its main row by construction: the same explicit # height (otherwise the docs skeleton falls back to the full # three-panel height) and the same tight x-range with no # autoscale padding. ``_get_layout`` already uses the composite # margins, so the drawable area matches pixel-for-pixel. - fig.update_layout( + layout = self._get_layout( + title, + axes_labels, height=self._single_main_panel_height_pixels(DEFAULT_RESIDUAL_HEIGHT_FRACTION), ) + + fig = self._get_figure(data, layout) x_min, x_max = self._composite_x_range(np.asarray(x)) if x_min is not None and x_max is not None: fig.update_xaxes(range=[x_min, x_max]) From 0b1cb28834ebd0e568d80bf1d2e098eb5379ae1a Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 00:18:48 +0200 Subject: [PATCH 23/57] Update unit tests for unified pattern view --- .../display/plotters/test_plotly.py | 21 +++++ .../easydiffraction/project/test_display.py | 80 ++++++++++++------- 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py index 53200f98a..d98316118 100644 --- a/tests/unit/easydiffraction/display/plotters/test_plotly.py +++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py @@ -164,6 +164,27 @@ def __init__(self, html): assert dummy_display_calls['count'] == 1 or shown['count'] == 1 +def test_single_panel_height_matches_composite_main_row(): + import easydiffraction.display.plotters.plotly as pp + + full_height = pp.DEFAULT_HEIGHT * pp.PLOTLY_HEIGHT_PER_UNIT + main_panel = pp.PlotlyPlotter._single_main_panel_height_pixels( + pp.DEFAULT_RESIDUAL_HEIGHT_FRACTION + ) + # A single-panel view is sized to the composite main row, not the + # full three-panel height. + assert 0 < main_panel < full_height + + +def test_composite_x_range_is_tight(): + import numpy as np + + import easydiffraction.display.plotters.plotly as pp + + assert pp.PlotlyPlotter._composite_x_range(np.array([10.0, 20.0, 30.0])) == (10.0, 30.0) + assert pp.PlotlyPlotter._composite_x_range(np.array([])) == (None, None) + + def test_show_figure_adds_legend_toggle_script_to_html_output(monkeypatch): import easydiffraction.display.plotters.plotly as pp diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index 45d30efee..1dfa513b8 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -107,9 +107,11 @@ def _make_statuses( PatternOptionStatus( name='auto', description='auto', - available=True, + available=measured or calculated or uncertainty, auto_included=True, - reason='', + reason='' + if (measured or calculated or uncertainty) + else 'No supported pattern content is available.', ), PatternOptionStatus( name='measured', @@ -193,7 +195,6 @@ def test_project_display_help_lists_namespaces_and_methods(capsys): assert 'fit' in out assert 'posterior' in out assert 'pattern()' in out - assert 'show_pattern_options()' in out def test_nested_project_display_help_lists_methods(capsys): @@ -452,7 +453,6 @@ def fake_activity_indicator(label, *, verbosity): 'hrpt', x_min=1.0, x_max=2.0, - include=('measured', 'calculated', 'uncertainty', 'residual', 'excluded'), ) assert calls == [ @@ -477,17 +477,15 @@ def fake_activity_indicator(label, *, verbosity): assert indicator_calls == [(ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL)] -def test_pattern_measured_and_calculated_suppresses_background_and_bragg(): +def test_pattern_measured_and_calculated_only_shows_available(): project, calls = _make_project_stub() display = ProjectDisplay(project) display._pattern_option_statuses = lambda expt_name: _make_statuses( measured=True, calculated=True, - background=True, - bragg=True, ) - display.pattern('hrpt', include=('measured', 'calculated')) + display.pattern('hrpt') assert calls == [ ( @@ -509,7 +507,7 @@ def test_pattern_measured_and_calculated_suppresses_background_and_bragg(): ] -def test_pattern_measured_and_calculated_can_enable_background_and_bragg(): +def test_pattern_shows_all_available_content(): project, calls = _make_project_stub() display = ProjectDisplay(project) display._pattern_option_statuses = lambda expt_name: _make_statuses( @@ -521,10 +519,7 @@ def test_pattern_measured_and_calculated_can_enable_background_and_bragg(): excluded=True, ) - display.pattern( - 'hrpt', - include=('measured', 'calculated', 'background', 'residual', 'bragg', 'excluded'), - ) + display.pattern('hrpt') assert calls == [ ( @@ -650,40 +645,63 @@ def _recorder(*args, **kwargs): ] -def test_pattern_rejects_excluded_with_custom_x(): - project, _calls = _make_project_stub() +def test_pattern_with_custom_x_drops_excluded_overlay(): + project, calls = _make_project_stub() display = ProjectDisplay(project) display._pattern_option_statuses = lambda expt_name: _make_statuses( measured=True, excluded=True, ) - with pytest.raises(ValueError, match='default x-axis'): - display.pattern('hrpt', include=('measured', 'excluded'), x='d_spacing') + display.pattern('hrpt', x='d_spacing') + + assert calls == [ + ( + 'plot_meas', + (), + { + 'expt_name': 'hrpt', + 'x_min': None, + 'x_max': None, + 'x': 'd_spacing', + 'show_excluded': False, + }, + ) + ] -def test_show_pattern_options_renders_table(monkeypatch): - project, _calls = _make_project_stub() +def test_pattern_measured_only_shades_excluded_when_present(): + project, calls = _make_project_stub() display = ProjectDisplay(project) display._pattern_option_statuses = lambda expt_name: _make_statuses( measured=True, - calculated=True, + excluded=True, ) - captured: dict[str, object] = {} - def fake_render_table(*, columns_headers, columns_alignment, columns_data): - captured['columns_headers'] = columns_headers - captured['columns_alignment'] = columns_alignment - captured['columns_data'] = columns_data + display.pattern('hrpt') - monkeypatch.setattr('easydiffraction.project.display.render_table', fake_render_table) + assert calls == [ + ( + 'plot_meas', + (), + { + 'expt_name': 'hrpt', + 'x_min': None, + 'x_max': None, + 'x': None, + 'show_excluded': True, + }, + ) + ] - display.show_pattern_options('hrpt') - assert captured['columns_headers'] == ['Option', 'Description', 'Available', 'Auto', 'Reason'] - assert captured['columns_alignment'] == ['left', 'left', 'center', 'center', 'left'] - assert captured['columns_data'][0][0] == 'auto' - assert captured['columns_data'][1][0] == 'measured' +def test_pattern_raises_when_nothing_available(): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses() + + with pytest.raises(ValueError, match='No supported pattern content'): + display.pattern('hrpt') def test_structure_updates_categories_before_building_scene(monkeypatch, tmp_path): From 6be57ffe7ada4efe16875b41de61467fae00f6ad Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 00:20:14 +0200 Subject: [PATCH 24/57] Remove py3Dmol, relax pillow constraint --- pixi.lock | 12 +----------- pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/pixi.lock b/pixi.lock index 96e17870c..d4ab13b04 100644 --- a/pixi.lock +++ b/pixi.lock @@ -274,7 +274,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -598,7 +597,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -915,7 +913,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -1259,7 +1256,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -1581,7 +1577,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -1899,7 +1894,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -2239,7 +2233,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -2563,7 +2556,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -2880,7 +2872,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8e/5a/7fd1b784a87e96e0078f49a0a13a98b4c5f644ba5597a4a3b70a2ba3e613/py3dmol-2.5.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl @@ -8763,10 +8754,9 @@ packages: - lmfit - numpy - pandas - - pillow>=10.1 + - pillow - plotly - pooch - - py3dmol - rich - scipy - sympy diff --git a/pyproject.toml b/pyproject.toml index c8d57b976..a28986380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,7 @@ dependencies = [ 'darkdetect', # Detecting dark mode (system-level) 'pandas', # Displaying tables in Jupyter notebooks 'plotly', # Interactive plots - 'py3Dmol', # Visualisation of crystal structures - 'pillow>=10.1', # Rendering structure figures (labels, legend) for reports + 'pillow', # Rendering structure figures (labels, legend) for reports ] [project.optional-dependencies] From 0b68698c5350d865330214e252e570b02efb6a17 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 00:25:31 +0200 Subject: [PATCH 25/57] Update integration plotting tests for dropped include --- tests/integration/fitting/test_plotting.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/tests/integration/fitting/test_plotting.py b/tests/integration/fitting/test_plotting.py index 393e78c3d..374ab0d21 100644 --- a/tests/integration/fitting/test_plotting.py +++ b/tests/integration/fitting/test_plotting.py @@ -4,26 +4,11 @@ """Integration tests for ``project.display.pattern``.""" -def test_pattern_auto(lbco_fitted_project): +def test_pattern_default(lbco_fitted_project): project = lbco_fitted_project project.display.pattern(expt_name='hrpt') -def test_pattern_measured(lbco_fitted_project): - project = lbco_fitted_project - project.display.pattern(expt_name='hrpt', include='measured') - - -def test_pattern_measured_vs_calculated(lbco_fitted_project): - project = lbco_fitted_project - project.display.pattern(expt_name='hrpt', include=('measured', 'calculated')) - - def test_pattern_with_range(lbco_fitted_project): project = lbco_fitted_project project.display.pattern(expt_name='hrpt', x_min=20, x_max=80) - - -def test_show_pattern_options(lbco_fitted_project): - project = lbco_fitted_project - project.display.show_pattern_options(expt_name='hrpt') From e6f3d49d51257923d9ef6e5f28fd11d62665e8f3 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 00:29:07 +0200 Subject: [PATCH 26/57] Mark Phase 2 verification complete --- docs/dev/plans/pattern-display-unification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index 5cf7e2a1b..707c03c07 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -133,7 +133,7 @@ x-range matching the composite main row, and the nothing-to-plot error. - [x] Phase 1 implementation complete - [ ] Phase 1 reviewed -- [ ] Phase 2 verification complete +- [x] Phase 2 verification complete ## Suggested Pull Request From 15c7d9163a4fe7cf62652ffb079f2cdd550bda64 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 00:41:52 +0200 Subject: [PATCH 27/57] Record pattern unification as a dedicated ADR --- docs/dev/adrs/accepted/display-ux.md | 42 ++------- .../accepted/pattern-display-unification.md | 92 +++++++++++++++++++ docs/dev/adrs/index.md | 1 + docs/dev/plans/pattern-display-unification.md | 10 +- 4 files changed, 106 insertions(+), 39 deletions(-) create mode 100644 docs/dev/adrs/accepted/pattern-display-unification.md diff --git a/docs/dev/adrs/accepted/display-ux.md b/docs/dev/adrs/accepted/display-ux.md index 31604c944..edd55f3b3 100644 --- a/docs/dev/adrs/accepted/display-ux.md +++ b/docs/dev/adrs/accepted/display-ux.md @@ -114,40 +114,14 @@ project.display.pattern(expt_name='hrpt') project.display.pattern(expt_name='hrpt', x_min=40, x_max=55) ``` -`pattern()` always displays as much useful information as the project -state supports; there is no view-selection argument: - -- measured data if present -- calculated data if linked structure state and calculated intensities - are available -- background if powder Bragg measured and calculated data plus defined - background points are available -- Bragg ticks if powder Bragg measured and calculated data plus - reflection rows are available -- residual if both measured and calculated data are available and the - experiment type supports a residual panel -- excluded regions whenever defined on the experiment -- uncertainty bands where posterior predictive data exists and the chart - engine supports them - -Excluded regions are a property of the experiment, not a viewing choice, -so they are always shaded when present, skipped only when a custom `x` -axis variable is selected (the overlay cannot be mapped onto it). -`uncertainty` is shown where posterior predictive data exists for a -supported experiment and the active chart engine can render bands. - -Single-panel views (for example measured-only, before a structure is -linked) and the full composite share one figure-sizing and x-range core, -so a one-row chart is the top row of the multi-row chart pixel for -pixel: the same height and the same tick-to-tick x-range with no -autoscale padding. - -This supersedes the earlier design, which assembled a view from an -`include=('measured', 'calculated', ...)` argument and exposed a -`show_pattern_options()` discovery table. Selecting a strict subset of -the available data is no longer supported; the project is in beta, so -this simplification replaces the previous API rather than carrying a -compatibility shim. +`pattern()` renders every kind of data the project state supports — +measured, calculated, residual, Bragg ticks, background, excluded +regions, and posterior predictive uncertainty, each shown when +available. It takes no view-selection argument. The content rules, the +removed `include` / `show_pattern_options` design, and the shared +single- and multi-panel figure sizing are recorded in the +[Unified Pattern View](pattern-display-unification.md) ADR, which +supersedes the `include`-based pattern design once described here. ## Deterministic And Bayesian Consistency diff --git a/docs/dev/adrs/accepted/pattern-display-unification.md b/docs/dev/adrs/accepted/pattern-display-unification.md new file mode 100644 index 000000000..e4f057a00 --- /dev/null +++ b/docs/dev/adrs/accepted/pattern-display-unification.md @@ -0,0 +1,92 @@ +# ADR: Unified Pattern View + +## Status + +Accepted and implemented. + +## Date + +2026-06-04 + +## Context + +`project.display.pattern()` was originally specified by the +[Display UX Facade](display-ux.md) ADR with an `include=` argument that +assembled a view from named layers (`measured`, `calculated`, +`background`, `residual`, `bragg`, `excluded`, `uncertainty`), plus a +`show_pattern_options()` discovery table. `include='auto'` already chose +the most informative combination from project state. + +In practice this surfaced several problems: + +- The single-panel path (`plot_meas`/`plot_calc` → `plot_powder`) and + the composite path (`build_powder_meas_vs_calc_figure`) computed + figure height and x-range independently, so they drifted. A + measured-only view rendered at the full three-panel height in the lazy + docs runtime — the + [Plotting & Docs Performance](plotting-docs-performance.md) skeleton + fell back to `DEFAULT_HEIGHT * PLOTLY_HEIGHT_PER_UNIT` — and gained + stray left/right autoscale padding the composite did not have. +- `'excluded'` was an opt-in overlay, but excluded regions are a + property of the experiment, not a viewing choice; `include='measured'` + and `include=('measured', 'excluded')` produced different plots of the + same data, which read as redundant. +- For a scientist audience, choosing layers is friction. The project + state already determines what is meaningful to show, which is exactly + what `include='auto'` computed. + +## Decision + +`pattern(expt_name, x_min=None, x_max=None, *, x=None)` always renders +every kind of data the project state supports — the former +`include='auto'` behaviour is now the only behaviour. The `include` +parameter, the `show_pattern_options()` method, and the option-status +discovery table are removed. Strict-subset views (for example +measured-only once a calculation exists) are intentionally no longer +offered; zooming (`x_min`/`x_max`) and the x-axis variable (`x`) remain. + +Excluded regions are always shaded when defined on the experiment, +skipped only when a custom `x` axis variable is selected, because the +overlay cannot be mapped onto an arbitrary axis. + +Single-panel and composite charts share one figure-sizing and x-range +core. `plot_powder` builds its layout with the same +`_single_main_panel_height_pixels(...)` height and tight +`_composite_x_range(...)` as the composite main row, and `_get_layout` +already applies the composite margins. A one-row chart is therefore the +top row of the multi-row chart pixel for pixel, by construction, so the +two paths cannot diverge again. + +This supersedes the `include`-based pattern design in the +[Display UX Facade](display-ux.md) ADR. The remainder of that ADR — the +facade grouping, renderer categories, and naming rules — still stands. + +## Consequences + +- `pattern(expt_name=...)` is the whole pattern API surface; tutorials, + docs, and tests no longer pass `include=`. +- The project is in beta, so this replaces the previous API with no + compatibility shim; tutorials and tests are updated to the current + API. +- Sizing and range differences between one- and three-panel views are + prevented structurally, not patched per call. +- `structure(include=...)` and `show_structure_options()` are + unaffected: choosing which 3D features to draw remains a genuine + viewing choice (see + [Crystal Structure 3D Visualization](crysview-structure-visualization.md)). + +## Alternatives Considered + +Keeping `include` as the view-selection vocabulary (the original +design). `include` had been chosen over `layers`, `components`, +`content`, `view`, `series`, and boolean flags because it read as user +intent and fit residual rows and Bragg ticks. It is removed now because +the only combination users reached for in practice was the automatic +"show everything available" view; the subset combinations added API +surface and a discovery table without a matching workflow, and the +parallel single-panel rendering path was the source of the sizing and +range divergence. + +Keeping `'excluded'` as an opt-in overlay, or as a redundant no-op +token, was rejected: excluded regions belong to the experiment, so +shading them is automatic whenever they are present. diff --git a/docs/dev/adrs/index.md b/docs/dev/adrs/index.md index eea0d1af1..4156a5eb0 100644 --- a/docs/dev/adrs/index.md +++ b/docs/dev/adrs/index.md @@ -56,4 +56,5 @@ folders. | User-facing API | Accepted | String Paths and Live Descriptors | Separates persisted field selectors from references to live model parameters. | [`string-paths-and-live-descriptors.md`](accepted/string-paths-and-live-descriptors.md) | | User-facing API | Accepted | Switchable Category API | Places multi-type category selectors on the owner and omits public selectors for fixed or single-type categories. | [`switchable-category-api.md`](accepted/switchable-category-api.md) | | User-facing API | Accepted | Switchable Category Owned Selectors | Moves the writable `type` selector and `show_supported()` onto the category itself; collapses the CIF duplication. | [`switchable-category-owned-selectors.md`](accepted/switchable-category-owned-selectors.md) | +| User-facing API | Accepted | Unified Pattern View | `pattern()` always renders available data, drops `include`, and unifies single- and three-panel figure sizing. | [`pattern-display-unification.md`](accepted/pattern-display-unification.md) | | User-facing API | Accepted | Value-Selector Discovery | Gives enumerated value fields a per-descriptor `show_supported()`, beside the three category-level selector families. | [`value-selector-discovery.md`](accepted/value-selector-discovery.md) | diff --git a/docs/dev/plans/pattern-display-unification.md b/docs/dev/plans/pattern-display-unification.md index 707c03c07..35c3f2f08 100644 --- a/docs/dev/plans/pattern-display-unification.md +++ b/docs/dev/plans/pattern-display-unification.md @@ -4,13 +4,13 @@ Follows [`AGENTS.md`](../../../AGENTS.md). No deliberate exceptions. ## ADR -Amends the accepted ADR -[`display-ux.md`](../adrs/accepted/display-ux.md): the `pattern()` API -drops the `include` parameter and `show_pattern_options()`, and always -renders every kind of data the project state supports. Relates to +Recorded as the +[`pattern-display-unification`](../adrs/accepted/pattern-display-unification.md) +ADR (Accepted), which supersedes the `include`-based pattern design in +[`display-ux.md`](../adrs/accepted/display-ux.md). Relates to [`plotting-docs-performance.md`](../adrs/accepted/plotting-docs-performance.md) (the lazy figure skeleton that exposed the height bug) and open issue -#93 (future of `show_residual`). No new ADR file is required. +#93 (future of `show_residual`). ## Branch / PR From 2c8c6c7b26ccd1a0a12539f7177fa271e1b0b23c Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 06:30:33 +0200 Subject: [PATCH 28/57] Show pretty units in parameter repr --- src/easydiffraction/core/variable.py | 10 +++-- .../easydiffraction/core/test_parameters.py | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/easydiffraction/core/variable.py b/src/easydiffraction/core/variable.py index 03dac6b1f..8bcffb872 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -329,8 +329,9 @@ def __str__(self) -> str: """Return the string representation including units.""" s: str = super().__str__() s = s[1:-1] # strip <> - if self.units != 'none': - s += f' {self.units}' + units = self.resolve_display_units('gui') + if units: + s += f' {units}' return f'<{s}>' @property @@ -412,8 +413,9 @@ def __str__(self) -> str: s = s[1:-1] # strip <> if self.uncertainty is not None: s += f' ± {self.uncertainty}' - if self.units != 'none': - s += f' {self.units}' + units = self.resolve_display_units('gui') + if units: + s += f' {units}' s += f' (free={self.free})' return f'<{s}>' diff --git a/tests/unit/easydiffraction/core/test_parameters.py b/tests/unit/easydiffraction/core/test_parameters.py index a4a4979a3..8886c2f2d 100644 --- a/tests/unit/easydiffraction/core/test_parameters.py +++ b/tests/unit/easydiffraction/core/test_parameters.py @@ -45,6 +45,24 @@ def test_numeric_descriptor_str_includes_units(): assert 'w' in s +def test_numeric_descriptor_str_uses_pretty_display_units(): + from easydiffraction.core.display_handler import DisplayHandler + from easydiffraction.core.validation import AttributeSpec + from easydiffraction.core.variable import NumericDescriptor + from easydiffraction.io.cif.handler import CifHandler + + d = NumericDescriptor( + name='a', + value_spec=AttributeSpec(default=5.43), + units='angstroms', + cif_handler=CifHandler(names=['_cell.a']), + display_handler=DisplayHandler(display_units='Å'), + ) + s = str(d) + assert 'Å' in s # pretty symbol shown + assert 'angstroms' not in s # raw code suppressed + + def test_parameter_string_repr_and_as_cif_and_flags(): from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.variable import Parameter @@ -73,6 +91,30 @@ def test_parameter_string_repr_and_as_cif_and_flags(): assert p._cif_handler.uid == p.unique_name == 'a' +def test_parameter_str_uses_pretty_display_units(): + from easydiffraction.core.display_handler import DisplayHandler + from easydiffraction.core.validation import AttributeSpec + from easydiffraction.core.variable import Parameter + from easydiffraction.io.cif.handler import CifHandler + + p = Parameter( + name='a', + value_spec=AttributeSpec(default=0.0), + units='angstroms', + cif_handler=CifHandler(names=['_cell.a']), + display_handler=DisplayHandler(display_units='Å'), + ) + p.value = 5.43 + p.uncertainty = 0.01 + p.free = True + + s = str(p) + assert '± 0.01' in s + assert 'Å' in s # pretty symbol shown + assert 'angstroms' not in s # raw code suppressed + assert '(free=True)' in s + + def test_parameter_uncertainty_must_be_non_negative(): from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.variable import Parameter From 6e3a8c96434387988485b02e03fbe5dc3b021812 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 07:13:28 +0200 Subject: [PATCH 29/57] Omit space_group_Wyckoff loop from IUCr reports --- .../adrs/accepted/wyckoff-letter-detection.md | 26 ++++++---- src/easydiffraction/io/cif/iucr_writer.py | 49 ------------------- .../categories/test_space_group_wyckoff.py | 15 ++---- .../io/cif/test_iucr_writer.py | 22 +++++++++ 4 files changed, 43 insertions(+), 69 deletions(-) diff --git a/docs/dev/adrs/accepted/wyckoff-letter-detection.md b/docs/dev/adrs/accepted/wyckoff-letter-detection.md index 47df620bc..2ef1d2f10 100644 --- a/docs/dev/adrs/accepted/wyckoff-letter-detection.md +++ b/docs/dev/adrs/accepted/wyckoff-letter-detection.md @@ -2,6 +2,14 @@ **Status:** Accepted **Date:** 2026-06-01 +> **Amendment (2026-06-04):** the derived `space_group_Wyckoff` loop is +> now **code-only** — it is excluded from IUCr/report output as well as +> from project CIF. Wording below that calls the loop "report-facing" or +> says the report writer "emits" it (the §"`space_group_Wyckoff` loop" +> decision and the matching Consequences bullet) is superseded: the +> table is reachable in code via `structure.space_group_wyckoff` but is +> never serialized. + ## Group Structure model. @@ -447,13 +455,13 @@ which fall out naturally below. already emits the resolved `Wyckoff_symbol` for every atom and now also emits `_atom_site.site_symmetry_multiplicity`. - **`space_group_Wyckoff` loop.** The derived category is model-owned - and report-facing, but it is not persisted in project CIF. `Structure` - explicitly excludes it from project-save serialization via - `_serializable_categories()`, overriding `CategoryOwner`'s default of - serializing all owned categories. The IUCr/report writer emits the - `_space_group_Wyckoff.*` loop from the derived category because that - loop is useful report output even though it is redundant persisted - state. + and code-facing only; it is not serialized. `Structure` excludes it + from project-save serialization via `_serializable_categories()`, + overriding `CategoryOwner`'s default of serializing all owned + categories, and the IUCr/report writer likewise does **not** emit the + `_space_group_Wyckoff.*` loop — the table is redundant derived state, + reachable in code via `structure.space_group_wyckoff` but excluded + from both project CIF and report output (amended 2026-06-04). - **Derived values on read.** Multiplicity is recomputed from the letter, so any incoming `_atom_site.site_symmetry_multiplicity` is ignored rather than trusted; the `space_group_Wyckoff` category is @@ -677,8 +685,8 @@ may miss: structure's space group (each entry's letter / multiplicity / site_symmetry / coords match `SPACE_GROUPS`), rebuilds when the space group changes, refuses all public mutation paths, is empty for an - absent group, is omitted from project CIF, and is emitted in - IUCr/report output; + absent group, and is omitted from both project CIF and IUCr/report + output (code-only, reachable via `structure.space_group_wyckoff`); - CIF round-trip stability — a written letter reloads verbatim, an omitted one re-derives to the same value, and an unsupported-group row keeps `None` multiplicity whether its letter is empty, explicitly diff --git a/src/easydiffraction/io/cif/iucr_writer.py b/src/easydiffraction/io/cif/iucr_writer.py index 6db8aae8f..644f1e091 100644 --- a/src/easydiffraction/io/cif/iucr_writer.py +++ b/src/easydiffraction/io/cif/iucr_writer.py @@ -171,7 +171,6 @@ def _write_sc_block( _write_cell_section(lines, structure) _write_space_group_section(lines, structure) _write_symmetry_operations_section(lines, structure) - _write_space_group_wyckoff_section(lines, structure) _write_diffrn_section(lines, experiment) _write_wavelength_section(lines, experiment) _write_atom_site_sections(lines, structure) @@ -223,53 +222,6 @@ def _write_symmetry_operations_section(lines: list[str], structure: object) -> N _write_loop(lines, loop.tags, loop.rows) -def _write_space_group_wyckoff_section(lines: list[str], structure: object) -> None: - """ - Append the derived space-group Wyckoff-position loop. - - This loop is report-only: it summarises every Wyckoff position of - the structure's space group. ``coords_xyz`` reports the - representative orbit coordinate (the first orbit member) to keep - loop cells compact. - """ - positions = list(_collection_values(getattr(structure, 'space_group_wyckoff', None))) - if not positions: - return - - rows = [ - ( - _attribute_descriptor(position, 'id'), - _attribute_descriptor(position, 'letter'), - _attribute_descriptor(position, 'multiplicity'), - _attribute_descriptor(position, 'site_symmetry'), - _wyckoff_representative_coord(position), - ) - for position in positions - ] - _section(lines, 'Wyckoff positions') - _write_loop( - lines, - ( - '_space_group_Wyckoff.id', - '_space_group_Wyckoff.letter', - '_space_group_Wyckoff.multiplicity', - '_space_group_Wyckoff.site_symmetry', - '_space_group_Wyckoff.coords_xyz', - ), - rows, - ) - - -def _wyckoff_representative_coord(position: object) -> str: - """ - Return the representative (first) orbit coordinate of a position. - """ - coords = _attribute_value(position, 'coords_xyz') - if not coords: - return '?' - return str(coords).split()[0] - - def _write_diffrn_section(lines: list[str], experiment: object) -> None: """Append diffraction metadata.""" diffrn = getattr(experiment, 'diffrn', None) @@ -497,7 +449,6 @@ def _write_powder_phase_block(phase: _PowderPhase) -> str: _write_cell_section(lines, phase.structure) _write_space_group_section(lines, phase.structure) _write_symmetry_operations_section(lines, phase.structure) - _write_space_group_wyckoff_section(lines, phase.structure) _write_atom_site_sections(lines, phase.structure) _write_atom_site_aniso_sections(lines, phase.structure) _write_powder_phase_reference_section(lines, phase) diff --git a/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py index 16716343c..085e1a4d0 100644 --- a/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py +++ b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py @@ -123,14 +123,7 @@ def test_omitted_from_project_cif(self): structure = _cubic_structure() assert '_space_group_Wyckoff' not in structure.as_cif - def test_appears_in_report_output(self): - from easydiffraction.io.cif import iucr_writer - - structure = _cubic_structure() - lines: list[str] = [] - iucr_writer._write_space_group_wyckoff_section(lines, structure) - body = '\n'.join(lines) - assert '_space_group_Wyckoff.id' in body - assert '_space_group_Wyckoff.coords_xyz' in body - # Representative coordinate only (compact), never the full orbit. - assert max(len(line) for line in lines) < 80 + # Report omission (the IUCr writer never emits the loop) is guarded by + # test_write_iucr_cif_omits_space_group_wyckoff_loop in + # tests/unit/easydiffraction/io/cif/test_iucr_writer.py, where the + # project-level writer fixtures live. diff --git a/tests/unit/easydiffraction/io/cif/test_iucr_writer.py b/tests/unit/easydiffraction/io/cif/test_iucr_writer.py index 6ba55e93e..5aa2deb01 100644 --- a/tests/unit/easydiffraction/io/cif/test_iucr_writer.py +++ b/tests/unit/easydiffraction/io/cif/test_iucr_writer.py @@ -302,6 +302,28 @@ def test_write_iucr_cif_emits_single_crystal_block(tmp_path): assert '_easydiffraction_calculator.type' in text +def test_write_iucr_cif_omits_space_group_wyckoff_loop(tmp_path): + from easydiffraction.io.cif.iucr_writer import write_iucr_cif + + # The derived Wyckoff table is code-only; reports never emit it, even + # for a space group whose table is non-empty. + structure = _structure() + structure.space_group.name_h_m = 'P m -3 m' + structure._update_categories() + assert len(structure.space_group_wyckoff) > 0 + + project = _project( + 'wyckoff_omitted', + tmp_path, + _collection(structure), + _collection(_single_crystal_experiment()), + ) + + text = write_iucr_cif(project).read_text(encoding='utf-8') + + assert '_space_group_Wyckoff' not in text + + def test_write_iucr_cif_emits_powder_cwl_blocks(tmp_path): from easydiffraction.io.cif.iucr_writer import write_iucr_cif From 5fb2328a0decdd38ba4ba50de344207fc570eb3a Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 07:40:26 +0200 Subject: [PATCH 30/57] Hide space_group_Wyckoff via _skip_cif_serialization hook --- .../adrs/accepted/wyckoff-letter-detection.md | 18 +++++++++++------- .../categories/space_group_wyckoff/default.py | 19 +++++++++++++++++++ .../datablocks/structure/item/base.py | 10 ---------- .../categories/test_space_group_wyckoff.py | 15 +++++++++++---- .../report/test_data_context.py | 15 +++++++++++++++ 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/docs/dev/adrs/accepted/wyckoff-letter-detection.md b/docs/dev/adrs/accepted/wyckoff-letter-detection.md index 2ef1d2f10..c8b4340bb 100644 --- a/docs/dev/adrs/accepted/wyckoff-letter-detection.md +++ b/docs/dev/adrs/accepted/wyckoff-letter-detection.md @@ -455,13 +455,17 @@ which fall out naturally below. already emits the resolved `Wyckoff_symbol` for every atom and now also emits `_atom_site.site_symmetry_multiplicity`. - **`space_group_Wyckoff` loop.** The derived category is model-owned - and code-facing only; it is not serialized. `Structure` excludes it - from project-save serialization via `_serializable_categories()`, - overriding `CategoryOwner`'s default of serializing all owned - categories, and the IUCr/report writer likewise does **not** emit the - `_space_group_Wyckoff.*` loop — the table is redundant derived state, - reachable in code via `structure.space_group_wyckoff` but excluded - from both project CIF and report output (amended 2026-06-04). + and code-facing only; it is not serialized. The collection suppresses + its own output through `_skip_cif_serialization()` returning `True`, + the same collection-owned hook `atom_site_aniso` uses (conditionally); + every serialization path that consults it honours the suppression — + `category_collection_to_cif` (project CIF) and the report data context + — so the `_space_group_Wyckoff.*` loop never appears in project CIF or + HTML/TeX reports, and the hand-rolled IUCr writer does not emit it + either. The decision lives on the category, so `Structure` needs no + `_serializable_categories()` override. The table is redundant derived + state, reachable in code via `structure.space_group_wyckoff` (amended + 2026-06-04). - **Derived values on read.** Multiplicity is recomputed from the letter, so any incoming `_atom_site.site_symmetry_multiplicity` is ignored rather than trusted; the `space_group_Wyckoff` category is diff --git a/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py b/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py index 3c6256567..10a612162 100644 --- a/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py +++ b/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py @@ -125,6 +125,25 @@ def __init__(self) -> None: """Initialise an empty derived Wyckoff collection.""" super().__init__(item_type=SpaceGroupWyckoff) + def _skip_cif_serialization(self) -> bool: + """ + Always suppress serialized output for this derived category. + + The Wyckoff table is rebuilt from the structure's space group on + every update, so it is code-only: reachable via + ``structure.space_group_wyckoff`` but never written to project + CIF, the IUCr export, or HTML/TeX reports. Every serialization + path that consults this hook (``category_collection_to_cif`` and + the report data context) honours the suppression, so no + owner-level category filtering is required. + + Returns + ------- + bool + Always ``True``. + """ + return True + @override def add(self, item: object) -> None: """ diff --git a/src/easydiffraction/datablocks/structure/item/base.py b/src/easydiffraction/datablocks/structure/item/base.py index 704a98c9c..addc8552d 100644 --- a/src/easydiffraction/datablocks/structure/item/base.py +++ b/src/easydiffraction/datablocks/structure/item/base.py @@ -253,16 +253,6 @@ def _update_categories( self._need_categories_update = False - def _serializable_categories(self) -> list: - """ - Project-CIF categories (excludes the derived Wyckoff table). - """ - return [ - category - for category in self.categories - if not isinstance(category, SpaceGroupWyckoffCollection) - ] - # ------------------------------------------------------------------ # Public methods # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py index 085e1a4d0..0c8acf72d 100644 --- a/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py +++ b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group_wyckoff.py @@ -123,7 +123,14 @@ def test_omitted_from_project_cif(self): structure = _cubic_structure() assert '_space_group_Wyckoff' not in structure.as_cif - # Report omission (the IUCr writer never emits the loop) is guarded by - # test_write_iucr_cif_omits_space_group_wyckoff_loop in - # tests/unit/easydiffraction/io/cif/test_iucr_writer.py, where the - # project-level writer fixtures live. + def test_skip_cif_serialization_is_true(self): + # Derived/code-only: the collection suppresses its own output on + # every serialization path that consults the hook. + structure = _cubic_structure() + assert structure.space_group_wyckoff._skip_cif_serialization() is True + + # End-to-end report omission is guarded where each writer's fixtures + # live: test_write_iucr_cif_omits_space_group_wyckoff_loop in + # tests/unit/easydiffraction/io/cif/test_iucr_writer.py (IUCr export) + # and test_space_group_wyckoff_omitted_from_report_context in + # tests/unit/easydiffraction/report/test_data_context.py (HTML/TeX). diff --git a/tests/unit/easydiffraction/report/test_data_context.py b/tests/unit/easydiffraction/report/test_data_context.py index fe2e2c8fe..96bb30651 100644 --- a/tests/unit/easydiffraction/report/test_data_context.py +++ b/tests/unit/easydiffraction/report/test_data_context.py @@ -407,6 +407,21 @@ def test_report_atom_site_aniso_adp_column_uses_active_b_label(): assert adp_column['latex_label'] == r'$B_{11}$' +def test_space_group_wyckoff_omitted_from_report_context(): + from easydiffraction.datablocks.structure.item.base import Structure + from easydiffraction.report.data_context import _category_contexts + + # P m -3 m has a non-empty Wyckoff table; the derived category must + # still be omitted from report output (it is code-only). + structure = Structure(name='phase') + structure.space_group.name_h_m = 'P m -3 m' + structure._update_categories() + assert len(structure.space_group_wyckoff) > 0 + + codes = {context['code'] for context in _category_contexts(structure)} + assert 'space_group_Wyckoff' not in codes + + def test_report_number_parts_split_decimal_and_uncertainty_text(): from easydiffraction.report.data_context import _number_parts From 999002ddd5944ddc181b0907834c5b8521021028 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:13:49 +0200 Subject: [PATCH 31/57] Make Wyckoff skip-serialization hook a staticmethod --- .../structure/categories/space_group_wyckoff/default.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py b/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py index 10a612162..0478cfe02 100644 --- a/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py +++ b/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/default.py @@ -125,9 +125,10 @@ def __init__(self) -> None: """Initialise an empty derived Wyckoff collection.""" super().__init__(item_type=SpaceGroupWyckoff) - def _skip_cif_serialization(self) -> bool: + @staticmethod + def _skip_cif_serialization() -> bool: """ - Always suppress serialized output for this derived category. + Suppress all serialized output for this derived category. The Wyckoff table is rebuilt from the structure's space group on every update, so it is code-only: reachable via From 457413a9be0a7d73f07a183639a745dcdfee6d15 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:13:49 +0200 Subject: [PATCH 32/57] Update Wyckoff plan for serialization-hook refactor --- docs/dev/plans/wyckoff-letter-detection.md | 45 +++++++++++++++------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/dev/plans/wyckoff-letter-detection.md b/docs/dev/plans/wyckoff-letter-detection.md index 2bfcb4bc5..094111680 100644 --- a/docs/dev/plans/wyckoff-letter-detection.md +++ b/docs/dev/plans/wyckoff-letter-detection.md @@ -11,6 +11,18 @@ ADR. No deliberate exception to `AGENTS.md` is taken. - [x] Phase 1 review gate - [x] Phase 2 — Verification (tests + `pixi` checks) +> **Post-implementation amendment (2026-06-04).** Two follow-up changes +> landed after this plan completed: (1) the `_space_group_Wyckoff.*` +> loop is no longer emitted in IUCr/HTML/TeX report output — the derived +> table is code-only; and (2) its exclusion from every serialization +> path now comes from the collection's own `_skip_cif_serialization()` +> hook (returning `True`), not from a +> `Structure._serializable_categories()` override, which was removed. +> References to `_serializable_categories()` and to "report-facing" / +> "emits the loop" below describe the original implementation and are +> superseded by this note and by the +> [ADR amendment](../adrs/accepted/wyckoff-letter-detection.md). + ## ADR This plan implements the @@ -105,13 +117,15 @@ rotation/translation parser. `_replace_from_space_group()` path rebuilds the derived collection through internal adoption. 10. **`space_group_Wyckoff` serialization policy.** The category is - model-owned and report-facing, but not persisted in project CIF. - `Structure._serializable_categories()` excludes it from - `structure.as_cif` / project saves, the IUCr/report writer emits the - `_space_group_Wyckoff.*` loop from the derived category, and - incoming `_space_group_Wyckoff.*` values are ignored/overwritten on - project load because the category is re-derived from the space - group. + model-owned and code-only: it is excluded from every serialization + path. The collection's own `_skip_cif_serialization()` hook (returns + `True`) suppresses its output in `structure.as_cif` / project saves + and in the report data context alike, and the IUCr writer does not + emit the `_space_group_Wyckoff.*` loop; incoming + `_space_group_Wyckoff.*` values are ignored/overwritten on project + load because the category is re-derived from the space group. + (Superseded the original `_serializable_categories()` + + report-emission design; see the Post-implementation amendment.) 11. **Allowed letters come from the current space group.** `_wyckoff_letter_allowed_values` returns `['', *tabulated_letters]` for supported groups and `[]` for absent groups. @@ -196,8 +210,9 @@ Phase 1 (implementation): - `src/easydiffraction/datablocks/structure/item/base.py` — add the `space_group_wyckoff` sibling category, expose it read-only, rebuild it from the current `space_group` before ordinary category update - hooks, and exclude it from project CIF through - `_serializable_categories()`. + hooks. (Project-CIF exclusion originally lived here via + `_serializable_categories()`; it now lives on the collection through + `_skip_cif_serialization()` — see the Post-implementation amendment.) - `src/easydiffraction/analysis/calculators/cryspy.py` — `_update_atom_multiplicity` reads `atom_site.multiplicity.value`. - `src/easydiffraction/io/cif/serialize.py` — only if atom-site CIF read @@ -288,7 +303,9 @@ The ADR commit + design-phase review/reply cleanup are handled by a read-only sibling category on `Structure`, rebuild it when structure categories update so it tracks the current space group, keep it empty for absent groups, and exclude it from project CIF - by overriding `_serializable_categories()`. Rebuild it from + by overriding `_serializable_categories()` (later replaced by the + collection's `_skip_cif_serialization()` hook; see the + Post-implementation amendment). Rebuild it from `Structure._update_categories()` before ordinary category update hooks, with no special `_update_priority`; atom-site detection reads `SPACE_GROUPS` through the crystallography helpers rather @@ -391,9 +408,11 @@ The ADR commit + design-phase review/reply cleanup are handled by is already automatic: P1.4 added the `multiplicity` descriptor with that CIF handler, and it is part of `AtomSite.parameters`, so the atom-site loop emits it (value `?` for untabulated - sites). The `_space_group_Wyckoff` loop exclusion is already - provided by P1.3's `Structure._serializable_categories` - override. No new write-side code was needed in P1.8. + sites). The `_space_group_Wyckoff` loop exclusion was originally + provided by P1.3's `Structure._serializable_categories` override + (later moved to the collection's `_skip_cif_serialization()` + hook; see the Post-implementation amendment). No new write-side + code was needed in P1.8. - **Read ignore of incoming `_space_group_Wyckoff.*`** is done by a no-op `SpaceGroupWyckoffCollection.from_cif` override (the structure read loop iterates *all* categories, including the From 4271d2d41c1c1a12cc4bd268a6d75c2eb5f16607 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:22:05 +0200 Subject: [PATCH 33/57] Apply latest templates --- .copier-answers.yml | 2 +- .github/workflows/coverage.yml | 26 +------------------------- .prettierignore | 3 +++ docs/docs/assets/stylesheets/extra.css | 9 ++++++++- docs/mkdocs.yml | 13 ++++++++++++- pyproject.toml | 5 ++++- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 009e6acc4..32c8c49a5 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # WARNING: Do not edit this file manually. # Any changes will be overwritten by Copier. -_commit: v0.11.4 +_commit: v0.12.0 _src_path: gh:easyscience/templates app_docs_url: https://easyscience.github.io/diffraction-app app_doi: 10.5281/zenodo.18163581 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d96e5b87d..5208a3333 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -63,32 +63,8 @@ jobs: files: ./coverage-unit.xml token: ${{ secrets.CODECOV_TOKEN }} - # Job 2: Run integration tests with coverage and upload to Codecov - integration-tests-coverage: - runs-on: ubuntu-latest - - steps: - - name: Check-out repository - uses: actions/checkout@v6 - - - name: Set up pixi - uses: ./.github/actions/setup-pixi - - - name: Run integration tests with coverage - run: - pixi run integration-tests-coverage --cov-report=xml:coverage-integration.xml - - - name: Upload integration tests coverage to Codecov - if: ${{ !cancelled() }} - uses: ./.github/actions/upload-codecov - with: - name: integration-tests-job - flags: integration - files: ./coverage-integration.xml - token: ${{ secrets.CODECOV_TOKEN }} - # Job 4: Build and publish dashboard (reusable workflow) run-reusable-workflows: - needs: [docstring-coverage, unit-tests-coverage, integration-tests-coverage] # depend on the previous jobs + needs: [docstring-coverage, unit-tests-coverage] # depend on the previous jobs uses: ./.github/workflows/dashboard.yml secrets: inherit diff --git a/.prettierignore b/.prettierignore index b894e10fb..32b5970dc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -31,6 +31,9 @@ src/easydiffraction/report/templates/html/vendor/ src/easydiffraction/report/templates/tex/styles/ src/easydiffraction/utils/_vendored/jupyter_dark_detect/ +# Tox +.tox + # Misc .benchmarks .cache diff --git a/docs/docs/assets/stylesheets/extra.css b/docs/docs/assets/stylesheets/extra.css index 59e5b0929..c4c8c786c 100644 --- a/docs/docs/assets/stylesheets/extra.css +++ b/docs/docs/assets/stylesheets/extra.css @@ -202,7 +202,14 @@ label.md-nav__title[for="__drawer"] { /* Change the overall width of the page */ .md-grid { - max-width: 1280px; + max-width: 116em; +} + +/* Keep prose line length stable when sidebars are hidden at narrower widths */ +.md-main .md-content > .md-content__inner { + width: min(100%, 40.5em); + margin-left: auto !important; + margin-right: auto !important; } /* Needed for mkdocs-jupyter to show download and other buttons on top of the notebook */ diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 8dba57ab1..b7b7786fd 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -161,7 +161,18 @@ plugins: show_root_heading: true show_root_full_path: false show_submodules: true - show_source: true + show_source: false + show_bases: false + # Uncomment when the project depends on `easyscience` and inherited + # EasyScience base-class members should be shown in the API docs. + # preload_modules: + # - easyscience + inherited_members: true + show_category_heading: true + merge_init_into_class: true + docstring_options: + ignore_init_summary: true + summary: true - search # Determines additional directories to watch when running mkdocs serve diff --git a/pyproject.toml b/pyproject.toml index a28986380..a90af7ff2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,7 +224,7 @@ exclude = [ indent-width = 4 line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] preview = true # Enable new rules that are not yet stable, like DOC - +builtins = ['display'] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 # Formatting options for Ruff [tool.ruff.format] @@ -357,6 +357,9 @@ ignore = [ 'D', 'W', ] +'docs/docs/tutorials/**' = [ + 'E402', # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ +] # Specific options for certain rules From ec4865f0205015595f5f3ceb5b73b9fb169d69e0 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:35:46 +0200 Subject: [PATCH 34/57] Assert crysfml engine is loaded in switch-calculator test --- tests/integration/fitting/test_switch-calculator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/fitting/test_switch-calculator.py b/tests/integration/fitting/test_switch-calculator.py index e633b5c74..0d853442b 100644 --- a/tests/integration/fitting/test_switch-calculator.py +++ b/tests/integration/fitting/test_switch-calculator.py @@ -12,6 +12,11 @@ @pytest.mark.fast def test_neutron_pd_cwl_lbco_crysfml(tmp_path) -> None: import easydiffraction as ed + from easydiffraction.analysis.calculators.crysfml import CrysfmlCalculator + + # Fail clearly if the crysfml backend is not importable, rather than + # raising a generic "unsupported calculator" error further down. + assert CrysfmlCalculator.engine_imported is True # Create a project from CIF files project = ed.Project() From 8ee6f1bac85b473bacfbf923fd668cc707dd84c6 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:36:55 +0200 Subject: [PATCH 35/57] Reformat builtins as multiline list --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a90af7ff2..6e5b27250 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,7 +224,9 @@ exclude = [ indent-width = 4 line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] preview = true # Enable new rules that are not yet stable, like DOC -builtins = ['display'] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 +builtins = [ + 'display', +] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 # Formatting options for Ruff [tool.ruff.format] From 2929b2e7be4e60f5edd9c11ca703f477737f2d3d Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:38:27 +0200 Subject: [PATCH 36/57] Increase page max-width to 118em --- docs/docs/assets/stylesheets/extra.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/assets/stylesheets/extra.css b/docs/docs/assets/stylesheets/extra.css index c4c8c786c..2f8ca55fc 100644 --- a/docs/docs/assets/stylesheets/extra.css +++ b/docs/docs/assets/stylesheets/extra.css @@ -202,7 +202,7 @@ label.md-nav__title[for="__drawer"] { /* Change the overall width of the page */ .md-grid { - max-width: 116em; + max-width: 118em; } /* Keep prose line length stable when sidebars are hidden at narrower widths */ From e25e200ad24e5240802d2c39c67dab00a90453ee Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 08:55:08 +0200 Subject: [PATCH 37/57] Point report API page at easydiffraction.report package --- docs/docs/api-reference/report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/api-reference/report.md b/docs/docs/api-reference/report.md index c33824c37..7a3ae20f9 100644 --- a/docs/docs/api-reference/report.md +++ b/docs/docs/api-reference/report.md @@ -1 +1 @@ -::: easydiffraction.project.categories.report.default.Report +::: easydiffraction.report From e2b12be297e0d2027a8120f3fa904fdaff6a3aa3 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:05:59 +0200 Subject: [PATCH 38/57] Track host theme in 3D-view loading box, relabel to plot --- .../display/structure/templates/structure.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/easydiffraction/display/structure/templates/structure.html.j2 b/src/easydiffraction/display/structure/templates/structure.html.j2 index 348225dfb..752cb018f 100644 --- a/src/easydiffraction/display/structure/templates/structure.html.j2 +++ b/src/easydiffraction/display/structure/templates/structure.html.j2 @@ -3,7 +3,7 @@ border-radius:0;box-sizing:border-box;overflow:hidden;isolation:isolate;z-index:0; font-family:system-ui,-apple-system,'Segoe UI',sans-serif;">
-
Loading 3D view…
+
Loading plot…
From 0f2e6ea1b4e0f4cb890b88a38701869a9ad5a287 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:14:47 +0200 Subject: [PATCH 39/57] Size fit-series scatter plot to the pattern top panel --- .../display/plotters/plotly.py | 6 +++- .../display/plotters/test_plotly.py | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index 70916c032..a20a8dd82 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -2994,7 +2994,10 @@ def plot_scatter( height: int | None = None, ) -> None: """Render a scatter plot with error bars via Plotly.""" - _ = height # not used by Plotly backend + # The passed height is an ASCII row count; the Plotly scatter + # panel is sized to the composite main row instead, so it + # matches the pattern plot's top panel. + del height trace = go.Scatter( x=x, @@ -3021,6 +3024,7 @@ def plot_scatter( layout = self._get_layout( title, axes_labels, + height=self._single_main_panel_height_pixels(DEFAULT_RESIDUAL_HEIGHT_FRACTION), ) fig = self._get_figure(trace, layout) diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py index d98316118..4f34a4ef9 100644 --- a/tests/unit/easydiffraction/display/plotters/test_plotly.py +++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py @@ -176,6 +176,35 @@ def test_single_panel_height_matches_composite_main_row(): assert 0 < main_panel < full_height +def test_plot_scatter_matches_single_main_panel_height(monkeypatch): + """Fit-series scatter matches the pattern plot's top panel.""" + import easydiffraction.display.plotters.plotly as pp + + captured = {} + + def fake_show_figure(self, fig): + captured['fig'] = fig + + monkeypatch.setattr(pp.PlotlyPlotter, '_show_figure', fake_show_figure) + + plotter = pp.PlotlyPlotter() + # The facade passes an ASCII row count here; the Plotly backend + # ignores it and sizes the panel to the composite main row. + plotter.plot_scatter( + x=[1.0, 2.0, 3.0], + y=[10.0, 12.0, 11.0], + sy=[0.5, 0.4, 0.6], + axes_labels=['Experiment No.', 'Parameter value'], + title='Series', + height=25, + ) + + expected = pp.PlotlyPlotter._single_main_panel_height_pixels( + pp.DEFAULT_RESIDUAL_HEIGHT_FRACTION + ) + assert captured['fig'].layout.height == expected + + def test_composite_x_range_is_tight(): import numpy as np From 7e6ab310b128b684f750e98ad968ea033f99ae4f Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:16:50 +0200 Subject: [PATCH 40/57] Increase page and content max-widths --- docs/docs/assets/stylesheets/extra.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/assets/stylesheets/extra.css b/docs/docs/assets/stylesheets/extra.css index 2f8ca55fc..a5a2f73e5 100644 --- a/docs/docs/assets/stylesheets/extra.css +++ b/docs/docs/assets/stylesheets/extra.css @@ -202,12 +202,12 @@ label.md-nav__title[for="__drawer"] { /* Change the overall width of the page */ .md-grid { - max-width: 118em; + max-width: 125em; } /* Keep prose line length stable when sidebars are hidden at narrower widths */ .md-main .md-content > .md-content__inner { - width: min(100%, 40.5em); + width: min(100%, 45em); margin-left: auto !important; margin-right: auto !important; } From 768534b7b40d591b7795779fa9433d0491a7d72a Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:44:42 +0200 Subject: [PATCH 41/57] Hide space_group_Wyckoff from parameter tables --- src/easydiffraction/analysis/analysis.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 41ffc5443..c09dfa3a6 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -76,7 +76,15 @@ from easydiffraction.analysis.categories.minimizer.base import MinimizerCategoryBase from easydiffraction.core.posterior import PosteriorParameterSummary -_SUMMARY_HIDDEN_PARAMETER_CATEGORIES = frozenset({'pd_data', 'total_data', 'refln'}) +# Categories hidden from the parameter summary tables: bulky measured +# data and derived, read-only tables that would only add noise. The +# space_group_Wyckoff table also carries unreadably long coords_xyz. +_SUMMARY_HIDDEN_PARAMETER_CATEGORIES = frozenset({ + 'pd_data', + 'total_data', + 'refln', + 'space_group_Wyckoff', +}) _POSTERIOR_SAMPLE_NDIM = 3 From f24875b0396b9da12226ad999ac1da684909548e Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:44:49 +0200 Subject: [PATCH 42/57] Render integer descriptors in parameter table --- src/easydiffraction/analysis/analysis.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index c09dfa3a6..af37ff812 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -51,6 +51,7 @@ from easydiffraction.core.category_owner import CategoryOwner from easydiffraction.core.guard import _apply_help_filter from easydiffraction.core.singleton import ConstraintsHandler +from easydiffraction.core.variable import GenericNumericDescriptor from easydiffraction.core.variable import NumericDescriptor from easydiffraction.core.variable import Parameter from easydiffraction.core.variable import StringDescriptor @@ -1225,19 +1226,17 @@ def _get_params_as_dataframe( """ records = [] for param in params: - record = {} # TODO: Merge into one. Add field if attr exists # TODO: f'{param.value!r}' for StringDescriptor? - if isinstance(param, (StringDescriptor, NumericDescriptor, Parameter)): - record = { - ('fittable', 'left'): False, - ('datablock', 'left'): param._identity.datablock_entry_name, - ('category', 'left'): param._identity.category_code, - ('entry', 'left'): param._identity.category_entry_name or '', - ('parameter', 'left'): param.name, - ('value', 'right'): param.value, - } - if isinstance(param, (NumericDescriptor, Parameter)): + record = { + ('fittable', 'left'): False, + ('datablock', 'left'): param._identity.datablock_entry_name, + ('category', 'left'): param._identity.category_code, + ('entry', 'left'): param._identity.category_entry_name or '', + ('parameter', 'left'): param.name, + ('value', 'right'): '' if param.value is None else param.value, + } + if isinstance(param, GenericNumericDescriptor): record |= { ('units', 'left'): _parameter_display_units(param), } From bbdd0ba17aaa3098d92ae9975eeda1eb6997b7c0 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:52:01 +0200 Subject: [PATCH 43/57] Pre-bin posterior distribution histogram, not raw samples --- src/easydiffraction/display/plotting.py | 53 ++++++++++----- .../easydiffraction/display/test_plotting.py | 65 ++++++++++++++++++- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index dcda2cf33..acf6b05ac 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -3043,29 +3043,46 @@ def _add_posterior_distribution_histogram( histogram_bin_edges: np.ndarray | None, ) -> None: """Add the histogram trace for a posterior distribution plot.""" - histogram_kwargs: dict[str, object] = {} - if ( - histogram_bin_edges is not None - and histogram_bin_edges.size >= MIN_POSTERIOR_SAMPLE_COUNT - ): - histogram_kwargs['xbins'] = { - 'start': float(histogram_bin_edges[0]), - 'end': float(histogram_bin_edges[-1]), - 'size': float(histogram_bin_edges[1] - histogram_bin_edges[0]), - } + marker = { + 'color': POSTERIOR_HISTOGRAM_FILL_COLOR, + 'line': {'color': POSTERIOR_HISTOGRAM_LINE_COLOR, 'width': 1}, + } + densities = Plotter._posterior_distribution_histogram_density( + values, + histogram_bin_edges, + ) + if densities is None or histogram_bin_edges is None: + # Degenerate sample (no usable bins): let Plotly bin the few + # raw values client-side; the embedded payload stays tiny. + fig.add_trace( + go.Histogram( + x=values, + histnorm='probability density', + marker=marker, + opacity=0.82, + name='Posterior histogram', + hovertemplate='sample=%{x:.4f}
density: %{y:.2f}', + ) + ) + return + # Pre-bin server-side and emit a Bar trace so only the per-bin + # densities ride in the page, not every raw posterior sample. + # ``go.Histogram(x=values)`` serializes the full sample array + # (hundreds of thousands of values per parameter), bloating the + # docs page and stalling the "Loading plot…" skeleton paint. + edges = np.asarray(histogram_bin_edges, dtype=float) + bin_centers = (edges[:-1] + edges[1:]) / 2.0 + bin_widths = np.diff(edges) fig.add_trace( - go.Histogram( - x=values, - histnorm='probability density', - marker={ - 'color': POSTERIOR_HISTOGRAM_FILL_COLOR, - 'line': {'color': POSTERIOR_HISTOGRAM_LINE_COLOR, 'width': 1}, - }, + go.Bar( + x=bin_centers, + y=densities, + width=bin_widths, + marker=marker, opacity=0.82, name='Posterior histogram', hovertemplate='sample=%{x:.4f}
density: %{y:.2f}', - **histogram_kwargs, ) ) diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index cf016324f..65675987d 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -794,7 +794,11 @@ def test_build_param_distribution_plot_returns_plotly_figure(): assert marginal_trace.line.width == POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH assert marginal_trace.fillcolor == POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR assert marginal_trace.hovertemplate == 'length_a: %{x:.4f}
density: %{y:.4f}' - assert histogram_trace.xbins.size is not None + # The histogram is pre-binned server-side into a Bar trace (per-bin + # densities only) so the raw posterior samples never enter the payload. + assert histogram_trace.type == 'bar' + assert histogram_trace.width is not None + assert len(histogram_trace.x) == len(histogram_trace.y) assert '68% credible interval' not in {trace.name for trace in figure.data} assert interval_trace.fillcolor == POSTERIOR_INTERVAL_95_FILL_COLOR assert max_posterior_trace.line.dash == POSTERIOR_POINT_ESTIMATE_LINE_DASH @@ -806,6 +810,65 @@ def test_build_param_distribution_plot_returns_plotly_figure(): assert figure.layout.yaxis.range is not None +def test_param_distribution_histogram_is_prebinned_not_raw_samples(): + """Distribution histogram embeds per-bin densities, not raw draws. + + A ``go.Histogram`` fed the full posterior sample array serialises + every draw into the figure (megabytes for real chains), which left + the lazy-figure "Loading plot…" skeleton unable to paint until the + browser parsed the whole payload. The trace must instead carry only + the pre-computed per-bin densities. + """ + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples + from easydiffraction.display.plotting import Plotter + + rng = np.random.default_rng(0) + sample_count = 5000 + draws = rng.normal(3.89, 0.001, size=(1, sample_count, 1)) + posterior_samples = PosteriorSamples( + parameter_names=['length_a'], + parameter_samples=draws, + log_posterior=np.zeros((1, sample_count), dtype=float), + ) + parameter = SimpleNamespace( + unique_name='length_a', name='length_a', fit_min=3.885, fit_max=3.895 + ) + summary = PosteriorParameterSummary( + unique_name='length_a', + display_name='length_a', + best_sample_value=float(draws[0, -1, 0]), + median=float(np.median(draws)), + standard_deviation=float(np.std(draws, ddof=1)), + interval_68=tuple(np.quantile(draws, [0.16, 0.84]).tolist()), + interval_95=tuple(np.quantile(draws, [0.025, 0.975]).tolist()), + ) + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + posterior_parameter_summaries=[summary], + posterior_predictive={}, + parameters=[parameter], + ) + plotter = Plotter() + plotter._get_posterior_samples_and_fit_results = MethodType( + lambda self: (posterior_samples, fit_results), plotter + ) + plotter._get_fit_result_for_correlation = MethodType(lambda self: fit_results, plotter) + + figure = plotter._build_param_distribution_plot(parameter) + histogram_trace = next(t for t in figure.data if t.name == 'Posterior histogram') + + assert histogram_trace.type == 'bar' + # Only per-bin densities ride along, far fewer than the raw draws. + assert len(histogram_trace.y) < sample_count // 10 + assert len(histogram_trace.x) == len(histogram_trace.y) + # Density-normalised bars integrate to ~1 across their bin widths. + integral = float( + np.sum(np.asarray(histogram_trace.y) * np.asarray(histogram_trace.width)) + ) + assert integral == pytest.approx(1.0, abs=1e-3) + + def test_plot_param_distribution_routes_ascii_to_marginal_density(monkeypatch): from types import SimpleNamespace From 1e3e57344eb522284b798d3a3eb60477c7c462c1 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:53:32 +0200 Subject: [PATCH 44/57] Move notebook action buttons to top left above title --- docs/docs/assets/stylesheets/extra.css | 24 +++++++++++++++---- docs/overrides/main.html | 32 +++++++++++++++----------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/docs/assets/stylesheets/extra.css b/docs/docs/assets/stylesheets/extra.css index a5a2f73e5..ae3c9bccd 100644 --- a/docs/docs/assets/stylesheets/extra.css +++ b/docs/docs/assets/stylesheets/extra.css @@ -212,9 +212,23 @@ label.md-nav__title[for="__drawer"] { margin-right: auto !important; } -/* Needed for mkdocs-jupyter to show download and other buttons on top of the notebook */ -.md-content__button { - position: relative !important; +/* Notebook action buttons (Open in Colab, Download). The page title is the + notebook's own H1 rendered just below, so lay the buttons out as a + left-aligned row above it instead of the theme's default right float, + which overlapped long titles. */ +.md-content__nb-actions { + display: flex; + gap: 0.4rem; + /* Empty line of separation between the button row and the title */ + margin-bottom: 1.2em; + /* Keep the row above the notebook, which is pulled up via margin-top */ + position: relative; + z-index: 1; +} + +.md-content__nb-actions .md-content__button { + float: none; + margin: 0; } /* Background color of the search input field */ @@ -248,7 +262,9 @@ body[data-md-color-scheme="slate"] .jupyter-wrapper { .jp-Notebook { padding: 0 !important; - margin-top: -3em !important; + /* Pull the notebook up to absorb the jupyter wrapper's intrinsic top + whitespace while leaving a small gap below the action-button row. */ + margin-top: -0.5em !important; /* Ensure notebook content stretches across the page */ width: 100% !important; diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 07acc6194..e0010f85f 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -37,20 +37,24 @@ {# Download link: relative to the current page #} {% set file_url = filename %} - {# Open in Colab (absolute GitHub URL; works anywhere) #} - - {% include ".icons/google-colab.svg" %} - - - {# Download: use a RELATIVE link to the file next to this page #} - - {% include ".icons/material/download.svg" %} - + {# Action buttons as a left-aligned row above the notebook title; see + .md-content__nb-actions in extra.css #} +
+ {# Open in Colab (absolute GitHub URL; works anywhere) #} + + {% include ".icons/google-colab.svg" %} + + + {# Download: use a RELATIVE link to the file next to this page #} + + {% include ".icons/material/download.svg" %} + +
{% endif %} {{ super() }} From 654ab02b3f1b8fd1d69398df63990db2198396ba Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 09:58:14 +0200 Subject: [PATCH 45/57] Drop duplicate pair scatter hover layer, halve sample cap --- src/easydiffraction/display/plotting.py | 21 ++----------------- .../easydiffraction/display/test_plotting.py | 14 ++++++------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index acf6b05ac..2688711e9 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -142,7 +142,7 @@ class PosteriorPairPlotStyleEnum(StrEnum): [0.82, 'rgba(215, 48, 39, 0.98)'], [1.0, 'rgba(215, 48, 39, 0.98)'], ] -POSTERIOR_PAIR_SCATTER_MAX_POINTS = 1500 +POSTERIOR_PAIR_SCATTER_MAX_POINTS = 750 # keep embedded pair scatter small POSTERIOR_PAIR_MAX_DENSITY_SAMPLES = 4000 POSTERIOR_PAIR_MIN_DENSITY_SAMPLES = 800 POSTERIOR_PAIR_TARGET_DENSITY_SAMPLE_BUDGET = 24000 @@ -177,7 +177,6 @@ class PosteriorPairPlotStyleEnum(StrEnum): CORRELATION_CELL_LABEL_CHAR_COUNT = 16 CORRELATION_LABEL_CHAR_WIDTH_FACTOR = 0.6 POSTERIOR_PAIR_SAMPLE_MARKER_SIZE = 6 -POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE = 6 @dataclass(frozen=True) @@ -2096,7 +2095,7 @@ def _add_posterior_pair_off_diagonal( name='Posterior samples', legendgroup='posterior-samples', showlegend=legend_state.show_scatter, - hoverinfo='skip', + hovertemplate=sample_hovertemplate, zorder=0, ), row=row, @@ -2112,22 +2111,6 @@ def _add_posterior_pair_off_diagonal( fig.add_trace(contour_traces[0], row=row, col=col) fig.add_trace(contour_traces[1], row=row, col=col) legend_state.show_contour = False - fig.add_trace( - go.Scatter( - x=x_scatter_values, - y=y_scatter_values, - mode='markers', - marker={ - 'color': 'rgba(0, 0, 0, 0)', - 'size': POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE, - }, - showlegend=False, - hovertemplate=sample_hovertemplate, - zorder=3, - ), - row=row, - col=col, - ) @staticmethod def _configure_posterior_pair_panel_axes( diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index 65675987d..fd05efe58 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -434,7 +434,6 @@ def test_correlation_from_posterior_samples_returns_labeled_dataframe(): def test_build_posterior_pairs_plot_hides_diagonal_ticks_and_uses_annotations(): - from easydiffraction.display.plotting import POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE from easydiffraction.display.plotting import POSTERIOR_PAIR_SAMPLE_MARKER_SIZE from easydiffraction.display.plotting import POSTERIOR_PAIR_TITLE_FONT_SIZE from easydiffraction.display.plotting import SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS @@ -499,14 +498,15 @@ def test_build_posterior_pairs_plot_hides_diagonal_ticks_and_uses_annotations(): assert len(figure.layout.shapes) == 30 assert any(trace.name == 'Posterior contours' for trace in figure.data) sample_trace = next(trace for trace in figure.data if trace.name == 'Posterior samples') - hover_trace = next( - trace - for trace in figure.data - if getattr(trace, 'mode', None) == 'markers' + assert sample_trace.marker.size == POSTERIOR_PAIR_SAMPLE_MARKER_SIZE + # The visible scatter carries hover directly -- no duplicate transparent + # layer embedding a second copy of every sample point. + assert sample_trace.hovertemplate is not None + assert not any( + getattr(trace, 'mode', None) == 'markers' and getattr(trace.marker, 'color', None) == 'rgba(0, 0, 0, 0)' + for trace in figure.data ) - assert sample_trace.marker.size == POSTERIOR_PAIR_SAMPLE_MARKER_SIZE - assert hover_trace.marker.size == POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE def test_build_posterior_pairs_plot_fast_mode_skips_contours(): From aec8c1888bd6e8add11b8f24bedaf7b2be75305c Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 10:01:20 +0200 Subject: [PATCH 46/57] Apply ruff format to distribution histogram test --- tests/unit/easydiffraction/display/test_plotting.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index fd05efe58..bb8f94a32 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -863,9 +863,7 @@ def test_param_distribution_histogram_is_prebinned_not_raw_samples(): assert len(histogram_trace.y) < sample_count // 10 assert len(histogram_trace.x) == len(histogram_trace.y) # Density-normalised bars integrate to ~1 across their bin widths. - integral = float( - np.sum(np.asarray(histogram_trace.y) * np.asarray(histogram_trace.width)) - ) + integral = float(np.sum(np.asarray(histogram_trace.y) * np.asarray(histogram_trace.width))) assert integral == pytest.approx(1.0, abs=1e-3) From 04937a41ee730d3f09bbd5008fbf3616ff28c99e Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 10:28:58 +0200 Subject: [PATCH 47/57] Include integer descriptors in remaining parameter tables --- src/easydiffraction/analysis/analysis.py | 74 +++++++++++------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index af37ff812..38afe0f39 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -52,9 +52,7 @@ from easydiffraction.core.guard import _apply_help_filter from easydiffraction.core.singleton import ConstraintsHandler from easydiffraction.core.variable import GenericNumericDescriptor -from easydiffraction.core.variable import NumericDescriptor from easydiffraction.core.variable import Parameter -from easydiffraction.core.variable import StringDescriptor from easydiffraction.datablocks.experiment.item.base import intensity_category_for from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.display.progress import make_display_handle @@ -76,6 +74,7 @@ from easydiffraction.analysis.categories.fit_result import FitResultBase from easydiffraction.analysis.categories.minimizer.base import MinimizerCategoryBase from easydiffraction.core.posterior import PosteriorParameterSummary + from easydiffraction.core.variable import GenericDescriptorBase # Categories hidden from the parameter summary tables: bulky measured # data and derived, read-only tables that would only add noise. The @@ -184,8 +183,8 @@ def _flush_structure_categories(self) -> None: @staticmethod def _summary_parameters( - params: list[StringDescriptor | NumericDescriptor | Parameter], - ) -> list[StringDescriptor | NumericDescriptor | Parameter]: + params: list[GenericDescriptorBase], + ) -> list[GenericDescriptorBase]: """Return parameters suitable for compact summary displays.""" return [ param @@ -337,25 +336,23 @@ def how_to_access_parameters(self) -> None: project_varname = project._varname for datablock_code, params in all_params.items(): for param in params: - if isinstance(param, (StringDescriptor, NumericDescriptor, Parameter)): - datablock_entry_name = param._identity.datablock_entry_name - category_code = param._identity.category_code - category_entry_name = param._identity.category_entry_name or '' - param_key = param.name - code_variable = ( - f'{project_varname}.{datablock_code}' - f"['{datablock_entry_name}'].{category_code}" - ) - if category_entry_name: - code_variable += f"['{category_entry_name}']" - code_variable += f'.{param_key}' - columns_data.append([ - datablock_entry_name, - category_code, - category_entry_name, - param_key, - code_variable, - ]) + datablock_entry_name = param._identity.datablock_entry_name + category_code = param._identity.category_code + category_entry_name = param._identity.category_entry_name or '' + param_key = param.name + code_variable = ( + f"{project_varname}.{datablock_code}['{datablock_entry_name}'].{category_code}" + ) + if category_entry_name: + code_variable += f"['{category_entry_name}']" + code_variable += f'.{param_key}' + columns_data.append([ + datablock_entry_name, + category_code, + category_entry_name, + param_key, + code_variable, + ]) console.paragraph('How to access parameters') render_table( @@ -402,19 +399,18 @@ def parameter_cif_uids(self) -> None: columns_data = [] for params in all_params.values(): for param in params: - if isinstance(param, (StringDescriptor, NumericDescriptor, Parameter)): - datablock_entry_name = param._identity.datablock_entry_name - category_code = param._identity.category_code - category_entry_name = param._identity.category_entry_name or '' - param_key = param.name - cif_uid = param._cif_handler.uid - columns_data.append([ - datablock_entry_name, - category_code, - category_entry_name, - param_key, - cif_uid, - ]) + datablock_entry_name = param._identity.datablock_entry_name + category_code = param._identity.category_code + category_entry_name = param._identity.category_entry_name or '' + param_key = param.name + cif_uid = param._cif_handler.uid + columns_data.append([ + datablock_entry_name, + category_code, + category_entry_name, + param_key, + cif_uid, + ]) console.paragraph('Show parameter CIF unique identifiers') render_table( @@ -1209,15 +1205,15 @@ def _has_software_provenance(self) -> bool: @staticmethod def _get_params_as_dataframe( - params: list[NumericDescriptor | Parameter], + params: list[GenericDescriptorBase], ) -> pd.DataFrame: """ Convert a list of parameters to a DataFrame. Parameters ---------- - params : list[NumericDescriptor | Parameter] - List of DescriptorFloat or Parameter objects. + params : list[GenericDescriptorBase] + List of descriptor or parameter objects. Returns ------- From 154c769100b07dc956c6c2a27f90e99acc1fa9ea Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 10:28:58 +0200 Subject: [PATCH 48/57] Add regression tests for parameter table rendering --- .../analysis/test_analysis_access_params.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py index af3072e34..a6e1166c0 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py @@ -43,6 +43,26 @@ def _make_param( return param +def _make_int_descriptor(db, cat, entry, name, val): + """Build a read-only IntegerDescriptor (e.g. atom_site.multiplicity).""" + from easydiffraction.core.display_handler import DisplayHandler + from easydiffraction.core.validation import AttributeSpec + from easydiffraction.core.variable import IntegerDescriptor + from easydiffraction.io.cif.handler import CifHandler + + descriptor = IntegerDescriptor( + name=name, + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=[f'_{cat}.{name}']), + display_handler=DisplayHandler(), + ) + descriptor.value = val + descriptor._identity.datablock_entry_name = lambda: db + descriptor._identity.category_code = cat + descriptor._identity.category_entry_name = (lambda: entry) if entry else (lambda: '') + return descriptor + + def test_how_to_access_parameters_prints_paths_and_uids(capsys, monkeypatch): import easydiffraction.analysis.analysis as analysis_mod from easydiffraction.analysis.analysis import Analysis @@ -355,3 +375,98 @@ def render(self, df): free_df = rendered[0] assert free_df['parameter', 'left'].tolist() == ['length_a', 'time_offset'] assert free_df['units', 'left'].tolist() == ['Ų', 'μs'] + + +def test_summary_parameters_excludes_space_group_wyckoff(): + from easydiffraction.analysis.analysis import AnalysisDisplay + + visible = _make_param('lbco', 'cell', '', 'length_a', 4.0) + # The derived space_group_Wyckoff table (read-only, with unreadably + # long coords_xyz) must not clutter the parameter summary tables. + wyckoff = _make_param('lbco', 'space_group_Wyckoff', '48n', 'coords_xyz', 0.0) + + summary = AnalysisDisplay._summary_parameters([visible, wyckoff]) + + assert [param._identity.category_code for param in summary] == ['cell'] + + +def test_all_params_renders_integer_descriptors_without_nan(monkeypatch): + import easydiffraction.analysis.analysis as analysis_mod + from easydiffraction.analysis.analysis import Analysis + + occupancy = _make_param('lbco', 'atom_site', 'O', 'occupancy', 1.0) + # IntegerDescriptors (e.g. atom_site.multiplicity) used to render as + # all-nan rows; None multiplicity occurs for an untabulated group. + multiplicity = _make_int_descriptor('lbco', 'atom_site', 'O', 'multiplicity', 3) + multiplicity_none = _make_int_descriptor('lbco', 'atom_site', 'X', 'multiplicity', None) + + class Coll: + def __init__(self, params): + self.parameters = params + + def __iter__(self): + return iter(()) + + class Project: + def __init__(self): + self.structures = Coll([occupancy, multiplicity, multiplicity_none]) + self.experiments = Coll([]) + + rendered = [] + + class FakeTableRenderer: + def render(self, df): + rendered.append(df) + + monkeypatch.setattr( + analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer()) + ) + Analysis(Project()).display.all_params() + + structure_df = rendered[0] + assert structure_df['parameter', 'left'].tolist() == [ + 'occupancy', + 'multiplicity', + 'multiplicity', + ] + # Integer value rendered as-is; None renders blank; no nan cells. + assert structure_df['value', 'right'].tolist() == [1.0, 3, ''] + assert int(structure_df.isna().sum().sum()) == 0 + + +def test_how_to_access_and_cif_uids_include_integer_descriptors(monkeypatch): + import easydiffraction.analysis.analysis as analysis_mod + from easydiffraction.analysis.analysis import Analysis + + # An IntegerDescriptor used to be dropped from both tables by a + # too-narrow isinstance guard; it must now appear in each. + multiplicity = _make_int_descriptor('lbco', 'atom_site', 'O', 'multiplicity', 3) + + class Coll: + def __init__(self, params): + self.parameters = params + + class Project: + _varname = 'proj' + + def __init__(self): + self.structures = Coll([multiplicity]) + self.experiments = Coll([]) + + captured = {} + + def fake_render_table(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(analysis_mod, 'render_table', fake_render_table) + a = Analysis(Project()) + a.display.how_to_access_parameters() + + access_rows = [' '.join(map(str, row)) for row in captured.get('columns_data') or []] + assert any("proj.structures['lbco'].atom_site['O'].multiplicity" in row for row in access_rows) + + captured.clear() + a.display.parameter_cif_uids() + + uid_rows = [' '.join(map(str, row)) for row in captured.get('columns_data') or []] + assert any('multiplicity' in row for row in uid_rows) From 2879684f7904e803c94a82cb367acb85cb5ae373 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 10:44:40 +0200 Subject: [PATCH 49/57] Name tutorial projects to match their save directories --- docs/docs/tutorials/ed-1.ipynb | 4 ++-- docs/docs/tutorials/ed-1.py | 4 ++-- docs/docs/tutorials/ed-10.ipynb | 2 +- docs/docs/tutorials/ed-10.py | 2 +- docs/docs/tutorials/ed-11.ipynb | 2 +- docs/docs/tutorials/ed-11.py | 2 +- docs/docs/tutorials/ed-12.ipynb | 2 +- docs/docs/tutorials/ed-12.py | 2 +- docs/docs/tutorials/ed-13.ipynb | 4 ++-- docs/docs/tutorials/ed-13.py | 4 ++-- docs/docs/tutorials/ed-14.ipynb | 2 +- docs/docs/tutorials/ed-14.py | 2 +- docs/docs/tutorials/ed-15.ipynb | 4 ++-- docs/docs/tutorials/ed-15.py | 4 ++-- docs/docs/tutorials/ed-16.ipynb | 2 +- docs/docs/tutorials/ed-16.py | 2 +- docs/docs/tutorials/ed-17.ipynb | 2 +- docs/docs/tutorials/ed-17.py | 2 +- docs/docs/tutorials/ed-2.ipynb | 2 +- docs/docs/tutorials/ed-2.py | 2 +- docs/docs/tutorials/ed-20.ipynb | 2 +- docs/docs/tutorials/ed-20.py | 2 +- docs/docs/tutorials/ed-21.ipynb | 2 +- docs/docs/tutorials/ed-21.py | 2 +- docs/docs/tutorials/ed-22.ipynb | 2 +- docs/docs/tutorials/ed-22.py | 2 +- docs/docs/tutorials/ed-25.ipynb | 2 +- docs/docs/tutorials/ed-25.py | 2 +- docs/docs/tutorials/ed-4.ipynb | 2 +- docs/docs/tutorials/ed-4.py | 2 +- docs/docs/tutorials/ed-5.ipynb | 2 +- docs/docs/tutorials/ed-5.py | 2 +- docs/docs/tutorials/ed-6.ipynb | 2 +- docs/docs/tutorials/ed-6.py | 2 +- docs/docs/tutorials/ed-7.ipynb | 2 +- docs/docs/tutorials/ed-7.py | 2 +- docs/docs/tutorials/ed-8.ipynb | 2 +- docs/docs/tutorials/ed-8.py | 2 +- docs/docs/tutorials/ed-9.ipynb | 2 +- docs/docs/tutorials/ed-9.py | 2 +- 40 files changed, 46 insertions(+), 46 deletions(-) diff --git a/docs/docs/tutorials/ed-1.ipynb b/docs/docs/tutorials/ed-1.ipynb index 17a9083d2..0ca03392d 100644 --- a/docs/docs/tutorials/ed-1.ipynb +++ b/docs/docs/tutorials/ed-1.ipynb @@ -75,8 +75,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Create minimal project without name and description\n", - "project = ed.Project()" + "# Create a minimal project with a short name\n", + "project = ed.Project(name='lbco_hrpt')" ] }, { diff --git a/docs/docs/tutorials/ed-1.py b/docs/docs/tutorials/ed-1.py index 4b16fd688..59a574582 100644 --- a/docs/docs/tutorials/ed-1.py +++ b/docs/docs/tutorials/ed-1.py @@ -26,8 +26,8 @@ # ## 📦 Define Project # %% -# Create minimal project without name and description -project = ed.Project() +# Create a minimal project with a short name +project = ed.Project(name='lbco_hrpt') # %% [markdown] # ## 🧩 Define Structure diff --git a/docs/docs/tutorials/ed-10.ipynb b/docs/docs/tutorials/ed-10.ipynb index 84197148d..0b309da49 100644 --- a/docs/docs/tutorials/ed-10.ipynb +++ b/docs/docs/tutorials/ed-10.ipynb @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='ni_pdf')" ] }, { diff --git a/docs/docs/tutorials/ed-10.py b/docs/docs/tutorials/ed-10.py index e9271fd76..46f54c02e 100644 --- a/docs/docs/tutorials/ed-10.py +++ b/docs/docs/tutorials/ed-10.py @@ -21,7 +21,7 @@ # ### Create Project # %% -project = ed.Project() +project = ed.Project(name='ni_pdf') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-11.ipynb b/docs/docs/tutorials/ed-11.ipynb index 47c4d0df0..65a861672 100644 --- a/docs/docs/tutorials/ed-11.ipynb +++ b/docs/docs/tutorials/ed-11.ipynb @@ -72,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='si_nomad_pdf')" ] }, { diff --git a/docs/docs/tutorials/ed-11.py b/docs/docs/tutorials/ed-11.py index 2d000d892..a11f1b233 100644 --- a/docs/docs/tutorials/ed-11.py +++ b/docs/docs/tutorials/ed-11.py @@ -18,7 +18,7 @@ # ### Create Project # %% -project = ed.Project() +project = ed.Project(name='si_nomad_pdf') # %% [markdown] # ### Set Plotting Engine diff --git a/docs/docs/tutorials/ed-12.ipynb b/docs/docs/tutorials/ed-12.ipynb index be3925044..2499a1981 100644 --- a/docs/docs/tutorials/ed-12.ipynb +++ b/docs/docs/tutorials/ed-12.ipynb @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='nacl_xray_pdf')" ] }, { diff --git a/docs/docs/tutorials/ed-12.py b/docs/docs/tutorials/ed-12.py index 6cd87d3a1..8b8f31617 100644 --- a/docs/docs/tutorials/ed-12.py +++ b/docs/docs/tutorials/ed-12.py @@ -21,7 +21,7 @@ # ### Create Project # %% -project = ed.Project() +project = ed.Project(name='nacl_xray_pdf') # %% [markdown] # ### Set Plotting Engine diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index f147d3228..d56eb1c10 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -119,7 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1 = ed.Project(name='reference')" + "project_1 = ed.Project(name='si')" ] }, { @@ -1256,7 +1256,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2 = ed.Project(name='main')\n", + "project_2 = ed.Project(name='lbco_si')\n", "project_2.info.title = 'La0.5Ba0.5CoO3 Fit'\n", "project_2.info.description = 'Fitting simulated powder diffraction pattern of La0.5Ba0.5CoO3.'" ] diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index 10b26e35b..233caad06 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -65,7 +65,7 @@ # analysis workflow. # %% -project_1 = ed.Project(name='reference') +project_1 = ed.Project(name='si') # %% [markdown] # You can set the title and description of the project to provide @@ -739,7 +739,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2 = ed.Project(name='main') +project_2 = ed.Project(name='lbco_si') project_2.info.title = 'La0.5Ba0.5CoO3 Fit' project_2.info.description = 'Fitting simulated powder diffraction pattern of La0.5Ba0.5CoO3.' diff --git a/docs/docs/tutorials/ed-14.ipynb b/docs/docs/tutorials/ed-14.ipynb index b8b6f4b9a..9488915db 100644 --- a/docs/docs/tutorials/ed-14.ipynb +++ b/docs/docs/tutorials/ed-14.ipynb @@ -63,7 +63,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Create minimal project without name and description\n", + "# Create a minimal project with a short name\n", "project = ed.Project(name='tbti_heidi')\n", "project.info.title = 'Tb2Ti2O7 at HEiDi@FRMII'\n", "project.info.description = \"\"\"This project demonstrates a standard\n", diff --git a/docs/docs/tutorials/ed-14.py b/docs/docs/tutorials/ed-14.py index d161f3ceb..03a2b329d 100644 --- a/docs/docs/tutorials/ed-14.py +++ b/docs/docs/tutorials/ed-14.py @@ -14,7 +14,7 @@ # ## 📦 Define Project # %% -# Create minimal project without name and description +# Create a minimal project with a short name project = ed.Project(name='tbti_heidi') project.info.title = 'Tb2Ti2O7 at HEiDi@FRMII' project.info.description = """This project demonstrates a standard diff --git a/docs/docs/tutorials/ed-15.ipynb b/docs/docs/tutorials/ed-15.ipynb index 672c429f1..2fcc797fa 100644 --- a/docs/docs/tutorials/ed-15.ipynb +++ b/docs/docs/tutorials/ed-15.ipynb @@ -63,8 +63,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Create minimal project without name and description\n", - "project = ed.Project()" + "# Create a minimal project with a short name\n", + "project = ed.Project(name='taurine_senju')" ] }, { diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py index ebe7310a2..c8d799405 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -14,8 +14,8 @@ # ## 📦 Define Project # %% -# Create minimal project without name and description -project = ed.Project() +# Create a minimal project with a short name +project = ed.Project(name='taurine_senju') # %% [markdown] # ## 🧩 Define Structure diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb index 9bf0988d1..0e26f35e7 100644 --- a/docs/docs/tutorials/ed-16.ipynb +++ b/docs/docs/tutorials/ed-16.ipynb @@ -374,7 +374,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='si_bragg_pdf')" ] }, { diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py index 8fb264927..1a811a2a8 100644 --- a/docs/docs/tutorials/ed-16.py +++ b/docs/docs/tutorials/ed-16.py @@ -157,7 +157,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='si_bragg_pdf') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb index 8bc7ab0e7..81bce1da7 100644 --- a/docs/docs/tutorials/ed-17.ipynb +++ b/docs/docs/tutorials/ed-17.ipynb @@ -69,7 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project(name='cosio_d20')\n", + "project = ed.Project(name='cosio_d20_scan')\n", "analysis = project.analysis\n", "display = project.display" ] diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index ae2090132..7391e1582 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -20,7 +20,7 @@ # and other related components. # %% -project = ed.Project(name='cosio_d20') +project = ed.Project(name='cosio_d20_scan') analysis = project.analysis display = project.display diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb index f4973448c..60ba1b82c 100644 --- a/docs/docs/tutorials/ed-2.ipynb +++ b/docs/docs/tutorials/ed-2.ipynb @@ -80,7 +80,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='lbco_hrpt')" ] }, { diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py index 521f689e6..e2207a8b2 100644 --- a/docs/docs/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -31,7 +31,7 @@ # ## 📦 Define Project # %% -project = ed.Project() +project = ed.Project(name='lbco_hrpt') # %% [markdown] # ## 🧩 Define Structure diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb index f27b9c986..98d9572a7 100644 --- a/docs/docs/tutorials/ed-20.ipynb +++ b/docs/docs/tutorials/ed-20.ipynb @@ -456,7 +456,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project(name='beer')\n", + "project = Project(name='beer_mcstas')\n", "project.save_as(dir_path='projects/ed_20_beer_mcstas')" ] }, diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py index 78345a210..bc5b1e644 100644 --- a/docs/docs/tutorials/ed-20.py +++ b/docs/docs/tutorials/ed-20.py @@ -222,7 +222,7 @@ # ### Create Project # %% -project = Project(name='beer') +project = Project(name='beer_mcstas') project.save_as(dir_path='projects/ed_20_beer_mcstas') # %% [markdown] diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index a6616b381..0b4ab246e 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -87,7 +87,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='lbco_hrpt_bumps_dream')" ] }, { diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index 2676f9d7f..002f3ba4d 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -38,7 +38,7 @@ # it later if needed. # %% -project = ed.Project() +project = ed.Project(name='lbco_hrpt_bumps_dream') # %% project.save_as(dir_path='projects/ed_21_lbco_hrpt_bumps_dream') diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb index a97ddebaa..d7f559090 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -84,7 +84,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='tbti_heidi_emcee')" ] }, { diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index 79cd24163..d9dcad9ca 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -35,7 +35,7 @@ # workflow inside this object. # %% -project = ed.Project() +project = ed.Project(name='tbti_heidi_emcee') # %% project.save_as(dir_path='projects/ed_22_tbti_heidi_emcee') diff --git a/docs/docs/tutorials/ed-25.ipynb b/docs/docs/tutorials/ed-25.ipynb index 373a68287..b7757aa2f 100644 --- a/docs/docs/tutorials/ed-25.ipynb +++ b/docs/docs/tutorials/ed-25.ipynb @@ -87,7 +87,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='lbco_hrpt_emcee')" ] }, { diff --git a/docs/docs/tutorials/ed-25.py b/docs/docs/tutorials/ed-25.py index 746f5c13b..662615def 100644 --- a/docs/docs/tutorials/ed-25.py +++ b/docs/docs/tutorials/ed-25.py @@ -38,7 +38,7 @@ # it later if needed. # %% -project = ed.Project() +project = ed.Project(name='lbco_hrpt_emcee') # %% project.save_as(dir_path='projects/ed_25_lbco_hrpt_emcee') diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb index 4ee50ba35..8f7797640 100644 --- a/docs/docs/tutorials/ed-4.ipynb +++ b/docs/docs/tutorials/ed-4.ipynb @@ -511,7 +511,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='pbso4_joint')" ] }, { diff --git a/docs/docs/tutorials/ed-4.py b/docs/docs/tutorials/ed-4.py index 633a93b7f..1ab7a4416 100644 --- a/docs/docs/tutorials/ed-4.py +++ b/docs/docs/tutorials/ed-4.py @@ -235,7 +235,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='pbso4_joint') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-5.ipynb b/docs/docs/tutorials/ed-5.ipynb index 3c2eeffd9..a57352ba9 100644 --- a/docs/docs/tutorials/ed-5.ipynb +++ b/docs/docs/tutorials/ed-5.ipynb @@ -361,7 +361,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='cosio_d20')" ] }, { diff --git a/docs/docs/tutorials/ed-5.py b/docs/docs/tutorials/ed-5.py index 35a1bf765..23a091d6e 100644 --- a/docs/docs/tutorials/ed-5.py +++ b/docs/docs/tutorials/ed-5.py @@ -171,7 +171,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='cosio_d20') # %% project.save_as(dir_path='projects/ed_5_cosio_d20') diff --git a/docs/docs/tutorials/ed-6.ipynb b/docs/docs/tutorials/ed-6.ipynb index b763ff164..e38f74ef4 100644 --- a/docs/docs/tutorials/ed-6.ipynb +++ b/docs/docs/tutorials/ed-6.ipynb @@ -320,7 +320,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='hs_hrpt')" ] }, { diff --git a/docs/docs/tutorials/ed-6.py b/docs/docs/tutorials/ed-6.py index 20068a7ef..50a3ab363 100644 --- a/docs/docs/tutorials/ed-6.py +++ b/docs/docs/tutorials/ed-6.py @@ -150,7 +150,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='hs_hrpt') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-7.ipynb b/docs/docs/tutorials/ed-7.ipynb index 66bd528d9..5f5ae0b00 100644 --- a/docs/docs/tutorials/ed-7.ipynb +++ b/docs/docs/tutorials/ed-7.ipynb @@ -286,7 +286,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='si_sepd')" ] }, { diff --git a/docs/docs/tutorials/ed-7.py b/docs/docs/tutorials/ed-7.py index b7ce6ec44..09e51da4f 100644 --- a/docs/docs/tutorials/ed-7.py +++ b/docs/docs/tutorials/ed-7.py @@ -117,7 +117,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='si_sepd') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb index 70d147f0f..097ac6f57 100644 --- a/docs/docs/tutorials/ed-8.ipynb +++ b/docs/docs/tutorials/ed-8.ipynb @@ -498,7 +498,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='ncaf_wish')" ] }, { diff --git a/docs/docs/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py index f049bea2b..94c0a89d5 100644 --- a/docs/docs/tutorials/ed-8.py +++ b/docs/docs/tutorials/ed-8.py @@ -268,7 +268,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='ncaf_wish') # %% [markdown] # ### Add Structure diff --git a/docs/docs/tutorials/ed-9.ipynb b/docs/docs/tutorials/ed-9.ipynb index 23e128637..613268b88 100644 --- a/docs/docs/tutorials/ed-9.ipynb +++ b/docs/docs/tutorials/ed-9.ipynb @@ -429,7 +429,7 @@ "metadata": {}, "outputs": [], "source": [ - "project = Project()" + "project = Project(name='lbco_si_mcstas')" ] }, { diff --git a/docs/docs/tutorials/ed-9.py b/docs/docs/tutorials/ed-9.py index 21a12bc16..a869b2583 100644 --- a/docs/docs/tutorials/ed-9.py +++ b/docs/docs/tutorials/ed-9.py @@ -195,7 +195,7 @@ # ### Create Project # %% -project = Project() +project = Project(name='lbco_si_mcstas') # %% [markdown] # ### Add Structures From 271fc1d759d633c82654aae0c0e8bc5cf188e669 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 4 Jun 2026 11:31:05 +0200 Subject: [PATCH 50/57] Render pandas tables as inline-styled HTML --- src/easydiffraction/display/tablers/pandas.py | 414 ++++++------------ .../display/tablers/test_pandas.py | 133 ++++-- 2 files changed, 211 insertions(+), 336 deletions(-) diff --git a/src/easydiffraction/display/tablers/pandas.py b/src/easydiffraction/display/tablers/pandas.py index 25a013902..08fda01e9 100644 --- a/src/easydiffraction/display/tablers/pandas.py +++ b/src/easydiffraction/display/tablers/pandas.py @@ -1,9 +1,14 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Pandas-based table renderer for notebooks using DataFrame Styler.""" +""" +Pandas-input table renderer emitting inline-styled HTML for notebooks. +""" from __future__ import annotations +import html +import re + try: from IPython.display import HTML from IPython.display import display @@ -11,225 +16,143 @@ HTML = None display = None -import re - from easydiffraction.display.tablers.base import TableBackendBase -from easydiffraction.display.theme import DARK_AXIS_FRAME_COLOR -from easydiffraction.display.theme import LIGHT_AXIS_FRAME_COLOR -from easydiffraction.display.theme import TABLE_AXIS_FRAME_CSS_VAR from easydiffraction.utils.environment import can_use_ipython_display from easydiffraction.utils.logging import log +# Rich-style inline colour markup, e.g. ``[red]text[/red]``. _RICH_COLOR_RE = re.compile(r'\[(\w+)\](.*?)\[/\1\]') -PANDAS_TABLE_THEME_CLASS = 'ed-themed-table' -PANDAS_AXIS_FRAME_COLOR = f'var({TABLE_AXIS_FRAME_CSS_VAR}, {LIGHT_AXIS_FRAME_COLOR})' + +# Theme-neutral translucent greys. Both read correctly on light and dark +# backgrounds, so the table needs no theme-sync script -- which matters +# because JupyterLab strips ``