Skip to content

Commit 80c1d50

Browse files
Enhance Plotting and Display Features, Improve Calibration Handling and Consistency (#59)
* Adds d-spacing parameter to project.py * Adds d parameter to plotting.py * Enhances notebook conversion with Jupytext * Adds 2025 DMSC workshop tutorial to documentation * Adds quick and dirty d-spacing plotting functionality * Adds 1st draft of the dmsc summer school 2025 tutorial * Updates Summer School tutorial * Includes d-spacing branch in documentation build * Relocates easydiffraction installation cell * Refines powder diffraction tutorial * Enhances Plotly figure display in Jupyter Improves the display of Plotly figures in Jupyter notebooks by checking for IPython availability. Converts figures to HTML for better compatibility, avoiding warnings related to unknown MIME types. * Suppresses Cryspy warnings by redirecting stderr Wraps Cryspy calculation in stderr redirection to prevent warning messages from cluttering the output. This is a temporary measure until Cryspy handles warnings more efficiently. * Adjusts default quadratic calibration parameter * Adds function to extract value from XYE file header * Updates the summer school tutorial * Configures Plotly to hide certain mode bar buttons * Refactors CIF display functionality * Enhances progress tracking with notebook support * Improves phase handling in experiments * Removes CIF display call * Converts error logging to printed messages * Adds d-spacing conversion for constant wavelength * Adjusts default plotting engine based on environment * Simplifies LBCO quick tutorial * Standardizes variable naming for 2-theta calculations * Add units to b_iso parameter in AtomSite class * Formats fitter method arguments for readability * Enhances fitting progress tracker with updated display * Refactors header defaults for table rendering * Update category key naming for consistency * Corrects category key in atom site test * Updates tracker start message in the unit test * Expands parameter table with more details following discussion #24 * Switches data download to 'd-spacing' branch in tests * Enhances Descriptor and Parameter string representation * Improves user notifications for calculator engine imports * Refactors calculator methods for improved readability * Refactors report generation to improve table rendering * Enhances table margins for improved layout * Enhances TOF to d-spacing conversion * Use env variable for default branch data download
1 parent 6cbf51e commit 80c1d50

File tree

52 files changed

+6624
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+6624
-328
lines changed

.github/workflows/building-deploying-docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
# Trigger the workflow on push
55
push:
66
# To the develop and master branches
7-
branches: [develop, master, docs]
7+
branches: [develop, master, docs, d-spacing]
88

99
# Allows you to run this workflow manually from the Actions tab
1010
workflow_dispatch:
@@ -101,7 +101,7 @@ jobs:
101101
- name: Convert ${{ env.NOTEBOOKS_DIR }}/*.py to docs/${{env.NOTEBOOKS_DIR }}/*.ipynb
102102
run: |
103103
cp -R ${{ env.NOTEBOOKS_DIR }}/data docs/${{ env.NOTEBOOKS_DIR }}/
104-
jupytext ${{ env.NOTEBOOKS_DIR }}/*.py --to ipynb
104+
jupytext ${{ env.NOTEBOOKS_DIR }}/*.py --from py:percent --to ipynb
105105
mv ${{ env.NOTEBOOKS_DIR }}/*.ipynb docs/${{ env.NOTEBOOKS_DIR }}/
106106
107107
# The following step is needed to avoid the following message during the build:

.github/workflows/testing-code.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ concurrency:
2323
${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
2424
cancel-in-progress: true
2525

26+
env:
27+
# Set the environment variables to be used in all jobs defined in this workflow
28+
# Set the CI_BRANCH environment variable to be the branch name
29+
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
30+
2631
jobs:
2732
testing-code:
2833

.github/workflows/testing-tutorials.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ concurrency:
1414
${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
1515
cancel-in-progress: true
1616

17+
env:
18+
# Set the environment variables to be used in all jobs defined in this workflow
19+
# Set the CI_BRANCH environment variable to be the branch name
20+
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
21+
1722
jobs:
1823
testing-tutorials:
1924

docs/mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ nav:
7777
- Ni pd-neut-cwl: tutorials/pdf_pd-neut-cwl_Ni.ipynb
7878
- Si pd-neut-tof: tutorials/pdf_pd-neut-tof_Si-NOMAD.ipynb
7979
- NaCl pd-xray: tutorials/pdf_pd-xray_NaCl.ipynb
80+
- Workshops & Schools:
81+
- 2025 DMSC: tutorials/dmsc-summer-school-2025_analysis-powder-diffraction.ipynb
8082
- API Reference:
8183
- API Reference: api-reference/index.md
8284
- core: api-reference/core.md

docs/tutorials/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,11 @@ The tutorials are organized into the following categories.
6666
- [NaCl `pd-xray`](pdf_pd-xray_NaCl.ipynb)
6767
Demonstrates a PDF analysis of NaCl using data collected from an X-ray
6868
powder diffraction experiment.
69+
70+
## Workshops & Schools
71+
72+
- [2025 DMSC](dmsc-summer-school-2025_analysis-powder-diffraction.ipynb)
73+
A workshop tutorial that demonstrates a Rietveld refinement of the
74+
La0.5Ba0.5CoO3 crystal structure using time-of-flight neutron powder
75+
diffraction data simulated with McStas. This tutorial is designed for
76+
the ESS DMSC Summer School 2025.

src/easydiffraction/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
from easydiffraction.summary import Summary
2020

2121
# Utils
22-
from easydiffraction.utils.utils import download_from_repository
22+
from easydiffraction.utils.utils import (
23+
download_from_repository,
24+
get_value_from_xye_header
25+
)
2326
from easydiffraction.utils.formatting import (
2427
chapter,
2528
section,
@@ -39,5 +42,6 @@
3942
"chapter",
4043
"section",
4144
"paragraph",
42-
'download_from_repository'
45+
"download_from_repository",
46+
"get_value_from_xye_header"
4347
]

src/easydiffraction/analysis/analysis.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import numpy as np
33
from typing import List, Optional, Union
44

5-
from easydiffraction.utils.utils import render_table
5+
from easydiffraction.utils.utils import (
6+
render_cif,
7+
render_table
8+
)
69
from easydiffraction.utils.formatting import (
710
paragraph,
811
warning
@@ -204,8 +207,15 @@ def how_to_access_parameters(self) -> None:
204207
print(warning(f"No parameters found."))
205208
return
206209

207-
columns_headers = ['Code variable', 'Unique ID for CIF']
208-
columns_alignment = ["left", "left"]
210+
columns_headers = ['datablock',
211+
'category',
212+
'entry',
213+
'parameter',
214+
'How to Access in Python Code',
215+
'Unique Identifier for CIF Constraints']
216+
217+
columns_alignment = ['left', 'left', 'left', 'left', 'left', 'left']
218+
209219
columns_data = []
210220
project_varname = self.project._varname
211221
for datablock_type, params in params.items():
@@ -215,12 +225,17 @@ def how_to_access_parameters(self) -> None:
215225
category_key = param.category_key
216226
entry_id = param.collection_entry_id
217227
param_key = param.name
218-
variable = f"{project_varname}.{datablock_type}['{datablock_id}'].{category_key}"
228+
code_variable = f"{project_varname}.{datablock_type}['{datablock_id}'].{category_key}"
219229
if entry_id:
220-
variable += f"['{entry_id}']"
221-
variable += f".{param_key}"
222-
uid = param._generate_human_readable_unique_id()
223-
columns_data.append([variable, uid])
230+
code_variable += f"['{entry_id}']"
231+
code_variable += f".{param_key}"
232+
cif_uid = param._generate_human_readable_unique_id()
233+
columns_data.append([datablock_id,
234+
category_key,
235+
entry_id,
236+
param_key,
237+
code_variable,
238+
cif_uid])
224239

225240
print(paragraph("How to access parameters"))
226241
render_table(columns_headers=columns_headers,
@@ -384,14 +399,19 @@ def fit(self):
384399

385400
if self.fit_mode == 'joint':
386401
print(paragraph(f"Using all experiments 🔬 {experiment_ids} for '{self.fit_mode}' fitting"))
387-
self.fitter.fit(sample_models, experiments, calculator, weights=self.joint_fit_experiments)
402+
self.fitter.fit(sample_models,
403+
experiments,
404+
calculator,
405+
weights=self.joint_fit_experiments)
388406
elif self.fit_mode == 'single':
389407
for expt_name in experiments.ids:
390408
print(paragraph(f"Using experiment 🔬 '{expt_name}' for '{self.fit_mode}' fitting"))
391409
experiment = experiments[expt_name]
392410
dummy_experiments = Experiments() # TODO: Find a better name
393411
dummy_experiments.add(experiment)
394-
self.fitter.fit(sample_models, dummy_experiments, calculator)
412+
self.fitter.fit(sample_models,
413+
dummy_experiments,
414+
calculator)
395415
else:
396416
raise NotImplementedError(f"Fit mode {self.fit_mode} not implemented yet.")
397417

@@ -417,14 +437,6 @@ def as_cif(self):
417437
return "\n".join(lines)
418438

419439
def show_as_cif(self) -> None:
420-
cif_text = self.as_cif()
421-
lines = cif_text.splitlines()
422-
max_width = max(len(line) for line in lines)
423-
padded_lines = [f"│ {line.ljust(max_width)} │" for line in lines]
424-
top = f"╒{'═' * (max_width + 2)}╕"
425-
bottom = f"╘{'═' * (max_width + 2)}╛"
426-
427-
print(paragraph(f"Analysis 🧮 info as cif"))
428-
print(top)
429-
print("\n".join(padded_lines))
430-
print(bottom)
440+
cif_text: str = self.as_cif()
441+
paragraph_title: str = paragraph(f"Analysis 🧮 info as cif")
442+
render_cif(cif_text, paragraph_title)

src/easydiffraction/analysis/calculators/calculator_base.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import List, Any
44

55
from easydiffraction.core.singletons import ConstraintsHandler
6+
from easydiffraction.sample_models.sample_model import SampleModel
67
from easydiffraction.sample_models.sample_models import SampleModels
78
from easydiffraction.experiments.experiment import Experiment
89

@@ -22,16 +23,22 @@ def engine_imported(self) -> bool:
2223
pass
2324

2425
@abstractmethod
25-
def calculate_structure_factors(self, sample_model: SampleModels, experiment: Experiment) -> None:
26+
def calculate_structure_factors(
27+
self,
28+
sample_model: SampleModel,
29+
experiment: Experiment
30+
) -> None:
2631
"""
2732
Calculate structure factors for a single sample model and experiment.
2833
"""
2934
pass
3035

31-
def calculate_pattern(self,
32-
sample_models: SampleModels,
33-
experiment: Experiment,
34-
called_by_minimizer: bool = False) -> np.ndarray:
36+
def calculate_pattern(
37+
self,
38+
sample_models: SampleModels,
39+
experiment: Experiment,
40+
called_by_minimizer: bool = False
41+
) -> np.ndarray:
3542
"""
3643
Calculate the diffraction pattern for multiple sample models and a single experiment.
3744
@@ -84,10 +91,12 @@ def calculate_pattern(self,
8491
return y_calc_total
8592

8693
@abstractmethod
87-
def _calculate_single_model_pattern(self,
88-
sample_model: SampleModels,
89-
experiment: Experiment,
90-
called_by_minimizer: bool) -> np.ndarray:
94+
def _calculate_single_model_pattern(
95+
self,
96+
sample_model: SampleModels,
97+
experiment: Experiment,
98+
called_by_minimizer: bool
99+
) -> np.ndarray:
91100
"""
92101
Calculate the diffraction pattern for a single sample model and experiment.
93102
@@ -101,7 +110,11 @@ def _calculate_single_model_pattern(self,
101110
"""
102111
pass
103112

104-
def _get_valid_linked_phases(self, sample_models: SampleModels, experiment: Experiment) -> List[Any]:
113+
def _get_valid_linked_phases(
114+
self,
115+
sample_models: SampleModels,
116+
experiment: Experiment
117+
) -> List[Any]:
105118
"""
106119
Get valid linked phases from the experiment.
107120
@@ -126,4 +139,4 @@ def _get_valid_linked_phases(self, sample_models: SampleModels, experiment: Expe
126139
if not valid_linked_phases:
127140
print('Warning: None of the linked phases found in Sample Models. Returning empty pattern.')
128141

129-
return valid_linked_phases
142+
return valid_linked_phases

src/easydiffraction/analysis/calculators/calculator_crysfml.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import numpy as np
22
from typing import Any, Dict, List, Union
3-
from .calculator_base import CalculatorBase
4-
from easydiffraction.utils.formatting import warning
3+
54
from easydiffraction.sample_models.sample_models import SampleModels
65
from easydiffraction.sample_models.sample_models import SampleModel
76
from easydiffraction.experiments.experiment import Experiment
87
from easydiffraction.experiments.experiments import Experiments
98

9+
from .calculator_base import CalculatorBase
10+
1011
try:
1112
from pycrysfml import cfml_py_utilities
13+
print("✅ 'pycrysfml' calculation engine is successfully imported.")
1214
except ImportError:
13-
print(warning('"pycrysfml" module not found. This calculator will not work.'))
15+
print("⚠️ 'pycrysfml' module not found. This calculation engine will not be available.")
1416
cfml_py_utilities = None
1517

1618

@@ -25,7 +27,11 @@ class CrysfmlCalculator(CalculatorBase):
2527
def name(self) -> str:
2628
return "crysfml"
2729

28-
def calculate_structure_factors(self, sample_models: SampleModels, experiments: Experiments) -> None:
30+
def calculate_structure_factors(
31+
self,
32+
sample_models: SampleModels,
33+
experiments: Experiments
34+
) -> None:
2935
"""
3036
Call Crysfml to calculate structure factors.
3137
@@ -61,7 +67,11 @@ def _calculate_single_model_pattern(
6167
y = []
6268
return y
6369

64-
def _adjust_pattern_length(self, pattern: List[float], target_length: int) -> List[float]:
70+
def _adjust_pattern_length(
71+
self,
72+
pattern: List[float],
73+
target_length: int
74+
) -> List[float]:
6575
"""
6676
Adjusts the length of the pattern to match the target length.
6777
@@ -77,7 +87,11 @@ def _adjust_pattern_length(self, pattern: List[float], target_length: int) -> Li
7787
return pattern[:target_length]
7888
return pattern
7989

80-
def _crysfml_dict(self, sample_model: SampleModels, experiment: Experiment) -> Dict[str, Union[Experiment, SampleModel]]:
90+
def _crysfml_dict(
91+
self,
92+
sample_model: SampleModels,
93+
experiment: Experiment
94+
) -> Dict[str, Union[Experiment, SampleModel]]:
8195
"""
8296
Converts the sample model and experiment into a dictionary format for Crysfml.
8397
@@ -95,7 +109,10 @@ def _crysfml_dict(self, sample_model: SampleModels, experiment: Experiment) -> D
95109
"experiments": [experiment_dict]
96110
}
97111

98-
def _convert_sample_model_to_dict(self, sample_model: SampleModels) -> Dict[str, SampleModel]:
112+
def _convert_sample_model_to_dict(
113+
self,
114+
sample_model: SampleModel
115+
) -> Dict[str, Any]:
99116
"""
100117
Converts a sample model into a dictionary format.
101118
@@ -133,7 +150,10 @@ def _convert_sample_model_to_dict(self, sample_model: SampleModels) -> Dict[str,
133150

134151
return sample_model_dict
135152

136-
def _convert_experiment_to_dict(self, experiment: Experiment) -> Dict[str, Any]:
153+
def _convert_experiment_to_dict(
154+
self,
155+
experiment: Experiment
156+
) -> Dict[str, Any]:
137157
"""
138158
Converts an experiment into a dictionary format.
139159
@@ -148,8 +168,8 @@ def _convert_experiment_to_dict(self, experiment: Experiment) -> Dict[str, Any]:
148168
peak = getattr(experiment, "peak", None)
149169

150170
x_data = experiment.datastore.pattern.x
151-
two_theta_min = float(x_data.min())
152-
two_theta_max = float(x_data.max())
171+
twotheta_min = float(x_data.min())
172+
twotheta_max = float(x_data.max())
153173

154174
exp_dict = {
155175
"NPD": {
@@ -163,9 +183,9 @@ def _convert_experiment_to_dict(self, experiment: Experiment) -> Dict[str, Any]:
163183
#"_pd_instr_reflex_s_l": peak_asymm.s_l.value if peak_asymm else 0.0,
164184
#"_pd_instr_reflex_d_l": peak_asymm.d_l.value if peak_asymm else 0.0,
165185
"_pd_meas_2theta_offset": instrument.calib_twotheta_offset.value if instrument else 0.0,
166-
"_pd_meas_2theta_range_min": two_theta_min,
167-
"_pd_meas_2theta_range_max": two_theta_max,
168-
"_pd_meas_2theta_range_inc": (two_theta_max - two_theta_min) / len(x_data)
186+
"_pd_meas_2theta_range_min": twotheta_min,
187+
"_pd_meas_2theta_range_max": twotheta_max,
188+
"_pd_meas_2theta_range_inc": (twotheta_max - twotheta_min) / len(x_data)
169189
}
170190
}
171191

0 commit comments

Comments
 (0)