Structure Refinement: PbSO4, NPD + XRD¶
This example demonstrates a more advanced use of the EasyDiffraction library by explicitly creating and configuring structures and experiments before adding them to a project. It could be more suitable for users who are interested in creating custom workflows. This tutorial provides minimal explanation and is intended for users already familiar with EasyDiffraction.
The tutorial covers a Rietveld refinement of PbSO4 crystal structure based on the joint fit of both X-ray and neutron diffraction data.
🛠️ Import Library¶
In [2]:
Copied!
from easydiffraction import ExperimentFactory
from easydiffraction import Project
from easydiffraction import StructureFactory
from easydiffraction import download_data
from easydiffraction import ExperimentFactory
from easydiffraction import Project
from easydiffraction import StructureFactory
from easydiffraction import download_data
In [3]:
Copied!
struct = StructureFactory.from_scratch(name='pbso4')
struct = StructureFactory.from_scratch(name='pbso4')
Set Space Group¶
In [4]:
Copied!
struct.space_group.name_h_m = 'P n m a'
struct.space_group.name_h_m = 'P n m a'
Set Unit Cell¶
In [5]:
Copied!
struct.cell.length_a = 8.47
struct.cell.length_b = 5.39
struct.cell.length_c = 6.95
struct.cell.length_a = 8.47
struct.cell.length_b = 5.39
struct.cell.length_c = 6.95
Set Atom Sites¶
In [6]:
Copied!
struct.atom_sites.create(
id='Pb',
type_symbol='Pb',
fract_x=0.1876,
fract_y=0.25,
fract_z=0.167,
adp_type='Biso',
adp_iso=1.37,
)
struct.atom_sites.create(
id='S',
type_symbol='S',
fract_x=0.0654,
fract_y=0.25,
fract_z=0.684,
adp_type='Biso',
adp_iso=0.3796,
)
struct.atom_sites.create(
id='O1',
type_symbol='O',
fract_x=0.9082,
fract_y=0.25,
fract_z=0.5954,
adp_type='Biso',
adp_iso=1.9840,
)
struct.atom_sites.create(
id='O2',
type_symbol='O',
fract_x=0.1935,
fract_y=0.25,
fract_z=0.5432,
adp_type='Biso',
adp_iso=1.4383,
)
struct.atom_sites.create(
id='O3',
type_symbol='O',
fract_x=0.0811,
fract_y=0.0272,
fract_z=0.8086,
adp_type='Biso',
adp_iso=1.2808,
)
struct.atom_sites.create(
id='Pb',
type_symbol='Pb',
fract_x=0.1876,
fract_y=0.25,
fract_z=0.167,
adp_type='Biso',
adp_iso=1.37,
)
struct.atom_sites.create(
id='S',
type_symbol='S',
fract_x=0.0654,
fract_y=0.25,
fract_z=0.684,
adp_type='Biso',
adp_iso=0.3796,
)
struct.atom_sites.create(
id='O1',
type_symbol='O',
fract_x=0.9082,
fract_y=0.25,
fract_z=0.5954,
adp_type='Biso',
adp_iso=1.9840,
)
struct.atom_sites.create(
id='O2',
type_symbol='O',
fract_x=0.1935,
fract_y=0.25,
fract_z=0.5432,
adp_type='Biso',
adp_iso=1.4383,
)
struct.atom_sites.create(
id='O3',
type_symbol='O',
fract_x=0.0811,
fract_y=0.0272,
fract_z=0.8086,
adp_type='Biso',
adp_iso=1.2808,
)
In [7]:
Copied!
data_path1 = download_data('meas-pbso4-d1a', destination='data')
data_path1 = download_data('meas-pbso4-d1a', destination='data')
Getting data...
Data 'meas-pbso4-d1a': PbSO4, D1A (ILL)
✅ Data 'meas-pbso4-d1a' downloaded to '../../../data/meas-pbso4-d1a.xys'
Create Experiment¶
In [8]:
Copied!
expt1 = ExperimentFactory.from_data_path(
name='npd',
data_path=data_path1,
radiation_probe='neutron',
)
expt1 = ExperimentFactory.from_data_path(
name='npd',
data_path=data_path1,
radiation_probe='neutron',
)
Set Instrument¶
In [9]:
Copied!
expt1.instrument.setup_wavelength = 1.91
expt1.instrument.calib_twotheta_offset = -0.1018
expt1.instrument.setup_wavelength = 1.91
expt1.instrument.calib_twotheta_offset = -0.1018
Set Peak Profile¶
In [10]:
Copied!
expt1.peak.type = 'pseudo-voigt + berar-baldinozzi asymmetry'
expt1.peak.type = 'pseudo-voigt + berar-baldinozzi asymmetry'
⚠️ Switching peak profile type adds these settings with defaults: • asym_beba_a0=0.0 • asym_beba_a1=0.0 • asym_beba_b0=0.0 • asym_beba_b1=0.0
Peak profile type for experiment 'npd' changed to
pseudo-voigt + berar-baldinozzi asymmetry
In [11]:
Copied!
expt1.peak.broad_gauss_u = 0.1678
expt1.peak.broad_gauss_v = -0.4636
expt1.peak.broad_gauss_w = 0.4168
expt1.peak.broad_lorentz_x = 0
expt1.peak.broad_lorentz_y = 0.0879
expt1.peak.asym_beba_a0 = -0.4327
expt1.peak.asym_beba_b0 = -0.0182
expt1.peak.asym_beba_a1 = 0.1976
expt1.peak.asym_beba_b1 = -0.0575
expt1.peak.cutoff_fwhm = 6
expt1.peak.broad_gauss_u = 0.1678
expt1.peak.broad_gauss_v = -0.4636
expt1.peak.broad_gauss_w = 0.4168
expt1.peak.broad_lorentz_x = 0
expt1.peak.broad_lorentz_y = 0.0879
expt1.peak.asym_beba_a0 = -0.4327
expt1.peak.asym_beba_b0 = -0.0182
expt1.peak.asym_beba_a1 = 0.1976
expt1.peak.asym_beba_b1 = -0.0575
expt1.peak.cutoff_fwhm = 6
Set Background¶
Select the background type.
In [12]:
Copied!
expt1.background.type = 'line-segment'
expt1.background.type = 'line-segment'
Background type for experiment 'npd' already set to
line-segment
Add background points.
In [13]:
Copied!
for id, x, y in [
('1', 11.0, 206.4940),
('2', 15.0, 194.7316),
('3', 20.0, 194.5190),
('4', 30.0, 188.3431),
('5', 50.0, 207.7130),
('6', 70.0, 201.6635),
('7', 120.0, 244.1902),
('8', 153.0, 226.3376),
]:
expt1.background.create(id=id, position=x, intensity=y)
for id, x, y in [
('1', 11.0, 206.4940),
('2', 15.0, 194.7316),
('3', 20.0, 194.5190),
('4', 30.0, 188.3431),
('5', 50.0, 207.7130),
('6', 70.0, 201.6635),
('7', 120.0, 244.1902),
('8', 153.0, 226.3376),
]:
expt1.background.create(id=id, position=x, intensity=y)
Set Linked Structures¶
In [14]:
Copied!
expt1.linked_structures.create(structure_id='pbso4', scale=1.5)
expt1.linked_structures.create(structure_id='pbso4', scale=1.5)
In [15]:
Copied!
data_path2 = download_data('meas-pbso4-xray', destination='data')
data_path2 = download_data('meas-pbso4-xray', destination='data')
Getting data...
Data 'meas-pbso4-xray': PbSO4, laboratory X-ray
✅ Data 'meas-pbso4-xray' already present at '../../../data/meas-pbso4-xray.xys'. Keeping existing.
Create Experiment¶
In [16]:
Copied!
expt2 = ExperimentFactory.from_data_path(
name='xrd',
data_path=data_path2,
radiation_probe='xray',
)
expt2 = ExperimentFactory.from_data_path(
name='xrd',
data_path=data_path2,
radiation_probe='xray',
)
Set Instrument¶
In [17]:
Copied!
expt2.instrument.setup_wavelength = 1.540560
expt2.instrument.setup_wavelength_2 = 1.544400
expt2.instrument.setup_wavelength_2_to_1_ratio = 0.5
expt2.instrument.setup_polarization_coefficient = 0.58
expt2.instrument.setup_monochromator_twotheta = 28
expt2.instrument.calib_twotheta_offset = -0.0292
expt2.instrument.setup_wavelength = 1.540560
expt2.instrument.setup_wavelength_2 = 1.544400
expt2.instrument.setup_wavelength_2_to_1_ratio = 0.5
expt2.instrument.setup_polarization_coefficient = 0.58
expt2.instrument.setup_monochromator_twotheta = 28
expt2.instrument.calib_twotheta_offset = -0.0292
Set Peak Profile¶
In [18]:
Copied!
expt2.peak.type = 'pseudo-voigt + berar-baldinozzi asymmetry'
expt2.peak.type = 'pseudo-voigt + berar-baldinozzi asymmetry'
⚠️ Switching peak profile type adds these settings with defaults: • asym_beba_a0=0.0 • asym_beba_a1=0.0 • asym_beba_b0=0.0 • asym_beba_b1=0.0
Peak profile type for experiment 'xrd' changed to
pseudo-voigt + berar-baldinozzi asymmetry
In [19]:
Copied!
expt2.peak.broad_gauss_u = 0.0197
expt2.peak.broad_gauss_v = -0.0185
expt2.peak.broad_gauss_w = 0.0079
expt2.peak.broad_lorentz_y = 0.0645
expt2.peak.asym_beba_a0 = -0.2188
expt2.peak.asym_beba_b0 = -0.0301
expt2.peak.cutoff_fwhm = 6
expt2.peak.broad_gauss_u = 0.0197
expt2.peak.broad_gauss_v = -0.0185
expt2.peak.broad_gauss_w = 0.0079
expt2.peak.broad_lorentz_y = 0.0645
expt2.peak.asym_beba_a0 = -0.2188
expt2.peak.asym_beba_b0 = -0.0301
expt2.peak.cutoff_fwhm = 6
Set Excluded Regions¶
In [20]:
Copied!
expt2.excluded_regions.create(id='1', start=0, end=15)
expt2.excluded_regions.create(id='2', start=160, end=180)
expt2.excluded_regions.create(id='1', start=0, end=15)
expt2.excluded_regions.create(id='2', start=160, end=180)
Set Background¶
Select background type.
In [21]:
Copied!
expt2.background.type = 'chebyshev'
expt2.background.type = 'chebyshev'
Background type for experiment 'xrd' changed to
chebyshev
Add Chebyshev background terms.
In [22]:
Copied!
for id, x, y in [
('1', 0, 153.17),
('2', 1, 62.93),
('3', 2, 9.86),
('4', 3, 10.81),
('5', 4, -6.61),
('6', 5, -7.12),
]:
expt2.background.create(id=id, order=x, coef=y)
for id, x, y in [
('1', 0, 153.17),
('2', 1, 62.93),
('3', 2, 9.86),
('4', 3, 10.81),
('5', 4, -6.61),
('6', 5, -7.12),
]:
expt2.background.create(id=id, order=x, coef=y)
Set Linked Structures¶
In [23]:
Copied!
expt2.linked_structures.create(structure_id='pbso4', scale=0.001)
expt2.linked_structures.create(structure_id='pbso4', scale=0.001)
In [24]:
Copied!
project = Project(name='pbso4_joint')
project = Project(name='pbso4_joint')
Add Structure¶
In [25]:
Copied!
project.structures.add(struct)
project.structures.add(struct)
Add Experiments¶
In [26]:
Copied!
project.experiments.add(expt1)
project.experiments.add(expt2)
project.experiments.add(expt1)
project.experiments.add(expt2)
In [27]:
Copied!
project.analysis.fitting_mode.type = 'joint'
project.analysis.fitting_mode.type = 'joint'
Fitting mode changed to
joint
Set Free Parameters¶
Set structure parameters to be optimized.
In [28]:
Copied!
struct.cell.length_a.free = True
struct.cell.length_b.free = True
struct.cell.length_c.free = True
for atom_id in ('Pb', 'S', 'O1', 'O2', 'O3'):
atom = struct.atom_sites[atom_id]
atom.adp_iso.free = True
struct.cell.length_a.free = True
struct.cell.length_b.free = True
struct.cell.length_c.free = True
for atom_id in ('Pb', 'S', 'O1', 'O2', 'O3'):
atom = struct.atom_sites[atom_id]
atom.adp_iso.free = True
Set experiment parameters to be optimized.
In [29]:
Copied!
expt1.linked_structures['pbso4'].scale.free = True
expt1.instrument.calib_twotheta_offset.free = True
expt1.instrument.setup_wavelength.free = True
expt1.linked_structures['pbso4'].scale.free = True
expt1.instrument.calib_twotheta_offset.free = True
expt1.instrument.setup_wavelength.free = True
In [30]:
Copied!
expt2.linked_structures['pbso4'].scale.free = True
expt2.instrument.calib_twotheta_offset.free = True
expt2.linked_structures['pbso4'].scale.free = True
expt2.instrument.calib_twotheta_offset.free = True
Run Fitting¶
In [31]:
Copied!
project.analysis.fit()
project.analysis.fit()
Using all experiments 🔬 ['npd', 'xrd'] for 'joint' fitting
🚀 Starting fit process with 'lmfit (leastsq)'...
📈 Goodness-of-fit progress:
| iteration | time (s) | χ² | change / status | |
|---|---|---|---|---|
| 1 | 1 | 1.11 | 61.19 | |
| 2 | 12 | 6.86 | 61.19 | |
| 3 | 17 | 9.26 | 26.62 | 56.5% ↓ |
| 4 | 28 | 14.60 | 26.62 | |
| 5 | 31 | 16.34 | 17.61 | 33.9% ↓ |
| 6 | 42 | 22.22 | 17.61 | |
| 7 | 45 | 23.34 | 10.42 | 40.8% ↓ |
| 8 | 55 | 28.84 | 10.42 | |
| 9 | 59 | 30.90 | 4.52 | 56.6% ↓ |
| 10 | 69 | 35.92 | 4.52 | |
| 11 | 73 | 38.06 | 3.56 | 21.2% ↓ |
| 12 | 84 | 43.30 | 3.56 | |
| 13 | 87 | 45.07 | 3.52 | 1.1% ↓ |
| 14 | 97 | 50.54 | 3.53 | |
| 15 | 109 | 55.67 | 3.51 | |
| 16 | 119 | 60.71 | 3.51 | |
| 17 | 129 | 65.78 | 3.51 | |
| 18 | 140 | 70.90 | 3.51 | |
| 19 | 152 | 76.54 | 3.51 | |
| 20 | 158 | 78.46 | 3.51 |
🏆 Best goodness-of-fit (reduced χ²) is 3.51 at iteration 143
✅ Fitting complete.
In [32]:
Copied!
project.display.fit.results()
project.display.fit.results()
⚙️ Settings used:
| Name | Value | Description | |
|---|---|---|---|
| 1 | max_iterations | 1000 | Maximum solver iterations. |
📋 Least-squares fit results:
| Metric | Value | |
|---|---|---|
| 1 | 🧪 Minimizer | lmfit (leastsq) |
| 2 | ✅ Overall status | success |
| 3 | ⏱️ Fitting time (seconds) | 78.46 |
| 4 | 🔁 Iterations | 155 |
| 5 | 📏 Goodness-of-fit (reduced χ²) | 3.51 |
| 6 | 📏 R-factor (Rf, %) | 5.67 |
| 7 | 📏 R-factor squared (Rf², %) | 6.17 |
| 8 | 📏 Weighted R-factor (wR, %) | 5.40 |
📈 Refined parameters:
| datablock | category | entry | parameter | units | start | value | s.u. | change | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | pbso4 | cell | length_a | Å | 8.4700 | 8.4812 | 0.0001 | 0.13 % ↑ | |
| 2 | pbso4 | cell | length_b | Å | 5.3900 | 5.3988 | 0.0000 | 0.16 % ↑ | |
| 3 | pbso4 | cell | length_c | Å | 6.9500 | 6.9605 | 0.0001 | 0.15 % ↑ | |
| 4 | pbso4 | atom_site | Pb | adp_iso | Ų | 1.3700 | 1.3747 | 0.0087 | 0.34 % ↑ |
| 5 | pbso4 | atom_site | S | adp_iso | Ų | 0.3796 | 0.4440 | 0.0327 | 16.97 % ↑ |
| 6 | pbso4 | atom_site | O1 | adp_iso | Ų | 1.9840 | 1.9507 | 0.0282 | 1.68 % ↓ |
| 7 | pbso4 | atom_site | O2 | adp_iso | Ų | 1.4383 | 1.4137 | 0.0264 | 1.71 % ↓ |
| 8 | pbso4 | atom_site | O3 | adp_iso | Ų | 1.2808 | 1.2460 | 0.0168 | 2.72 % ↓ |
| 9 | npd | linked_structure | pbso4 | scale | 1.5000 | 1.4673 | 0.0033 | 2.18 % ↓ | |
| 10 | npd | instrument | wavelength | Å | 1.9100 | 1.9129 | 0.0000 | 0.15 % ↑ | |
| 11 | npd | instrument | twotheta_offset | deg | -0.1018 | -0.1012 | 0.0008 | 0.63 % ↓ | |
| 12 | xrd | linked_structure | pbso4 | scale | 0.0010 | 0.0010 | 0.0000 | 0.02 % ↑ | |
| 13 | xrd | instrument | twotheta_offset | deg | -0.0292 | -0.0290 | 0.0003 | 0.65 % ↓ |
• start = parameter value before refinement
• value = refined value from least-squares minimization
• s.u. = standard uncertainty (one sigma), from the covariance matrix
• change = relative change from start, in %; ↑ = increase, ↓ = decrease
• value = refined value from least-squares minimization
• s.u. = standard uncertainty (one sigma), from the covariance matrix
• change = relative change from start, in %; ↑ = increase, ↓ = decrease
Display Correlations¶
In [33]:
Copied!
project.display.fit.correlations()
project.display.fit.correlations()
Loading plot…
Display Pattern¶
In [34]:
Copied!
project.display.pattern(expt_name='npd')
project.display.pattern(expt_name='npd')
Loading plot…
In [35]:
Copied!
project.display.pattern(expt_name='xrd')
project.display.pattern(expt_name='xrd')
Loading plot…
Display Structure¶
In [36]:
Copied!
project.display.structure(struct_name='pbso4')
project.display.structure(struct_name='pbso4')
Structure 🧩 'pbso4' (Atom view type: 'covalent')
Loading plot…
drag = rotate
wheel = zoom
right-drag = pan
wheel = zoom
right-drag = pan
💾 Save Project¶
In [37]:
Copied!
project.save_as(dir_path='projects/refine-pbso4-joint')
project.save_as(dir_path='projects/refine-pbso4-joint')
Saving project 📦 'pbso4_joint' to '../../../projects/refine-pbso4-joint'
├── 📄 project.edi
├── 📁 structures/
│ └── 📄 pbso4.edi
├── 📁 experiments/
│ └── 📄 npd.edi
│ └── 📄 xrd.edi
├── 📁 analysis/
│ └── 📄 analysis.edi
└── 📁 reports/
└── 📄 pbso4_joint.html