Skip to content

Resolves issue with multi-phase fitting #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/testing-code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:

- name: Install Python dependencies
shell: bash
run: python -m pip install -r requirements.txt
run: python -m pip install .

- name: Run Python unit tests
shell: bash
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ nav:
- HS pd-neut-cwl: tutorials/cryst-struct_pd-neut-cwl_HS-HRPT.ipynb
- Si pd-neut-tof: tutorials/cryst-struct_pd-neut-tof_Si-SEPD.ipynb
- NCAF pd-neut-tof: tutorials/cryst-struct_pd-neut-tof_NCAF-WISH.ipynb
- LBCO+Si McStas: tutorials/cryst-struct_pd-neut-tof_multphase-LBCO-Si_McStas.ipynb
- Pair Distribution Function:
- Ni pd-neut-cwl: tutorials/pdf_pd-neut-cwl_Ni.ipynb
- Si pd-neut-tof: tutorials/pdf_pd-neut-tof_Si-NOMAD.ipynb
Expand Down
41 changes: 26 additions & 15 deletions src/easydiffraction/analysis/calculators/calculator_cryspy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from .calculator_base import CalculatorBase
from easydiffraction.utils.formatting import warning

from easydiffraction.sample_models.sample_models import SampleModels
from easydiffraction.experiments.experiments import Experiments
from easydiffraction.sample_models.sample_model import SampleModel
from easydiffraction.experiments.experiment import Experiment

try:
Expand Down Expand Up @@ -33,19 +32,21 @@ def __init__(self) -> None:
super().__init__()
self._cryspy_dicts: Dict[str, Dict[str, Any]] = {}

def calculate_structure_factors(self, sample_models: SampleModels, experiments: Experiments) -> None:
def calculate_structure_factors(self,
sample_model: SampleModel,
experiment: Experiment) -> None:
"""
Raises a NotImplementedError as HKL calculation is not implemented.

Args:
sample_models: The sample models to calculate structure factors for.
experiments: The experiments associated with the sample models.
sample_model: The sample model to calculate structure factors for.
experiment: The experiment associated with the sample models.
"""
raise NotImplementedError("HKL calculation is not implemented for CryspyCalculator.")

def _calculate_single_model_pattern(
self,
sample_model: SampleModels,
sample_model: SampleModel,
experiment: Experiment,
called_by_minimizer: bool = False
) -> Union[np.ndarray, List[float]]:
Expand All @@ -65,8 +66,10 @@ def _calculate_single_model_pattern(
Returns:
The calculated diffraction pattern as a NumPy array or a list of floats.
"""
combined_name = f"{sample_model.name}_{experiment.name}"

if called_by_minimizer:
if self._cryspy_dicts and experiment.name in self._cryspy_dicts:
if self._cryspy_dicts and combined_name in self._cryspy_dicts:
cryspy_dict = self._recreate_cryspy_dict(sample_model, experiment)
else:
cryspy_obj = self._recreate_cryspy_obj(sample_model, experiment)
Expand All @@ -75,7 +78,7 @@ def _calculate_single_model_pattern(
cryspy_obj = self._recreate_cryspy_obj(sample_model, experiment)
cryspy_dict = cryspy_obj.get_dictionary()

self._cryspy_dicts[experiment.name] = copy.deepcopy(cryspy_dict)
self._cryspy_dicts[combined_name] = copy.deepcopy(cryspy_dict)

cryspy_in_out_dict: Dict[str, Any] = {}
rhochi_calc_chi_sq_by_dictionary(
Expand All @@ -99,14 +102,16 @@ def _calculate_single_model_pattern(
try:
signal_plus = cryspy_in_out_dict[cryspy_block_name]['signal_plus']
signal_minus = cryspy_in_out_dict[cryspy_block_name]['signal_minus']
y_calc_total = signal_plus + signal_minus
y_calc = signal_plus + signal_minus
except KeyError:
print(f"[CryspyCalculator] Error: No calculated data for {cryspy_block_name}")
return []

return y_calc_total
return y_calc

def _recreate_cryspy_dict(self, sample_model: SampleModels, experiment: Experiment) -> Dict[str, Any]:
def _recreate_cryspy_dict(self,
sample_model: SampleModel,
experiment: Experiment) -> Dict[str, Any]:
"""
Recreates the Cryspy dictionary for the given sample model and experiment.

Expand All @@ -117,7 +122,8 @@ def _recreate_cryspy_dict(self, sample_model: SampleModels, experiment: Experime
Returns:
The updated Cryspy dictionary.
"""
cryspy_dict = copy.deepcopy(self._cryspy_dicts[experiment.name])
combined_name = f"{sample_model.name}_{experiment.name}"
cryspy_dict = copy.deepcopy(self._cryspy_dicts[combined_name])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the main change - indexing cryspy_dict with properly defined combined name.
Good catch.

cryspy_model_id = f'crystal_{sample_model.name}'
cryspy_model_dict = cryspy_dict[cryspy_model_id]
Expand Down Expand Up @@ -185,7 +191,9 @@ def _recreate_cryspy_dict(self, sample_model: SampleModels, experiment: Experime

return cryspy_dict

def _recreate_cryspy_obj(self, sample_model: SampleModels, experiment: Experiment) -> Any:
def _recreate_cryspy_obj(self,
sample_model: SampleModel,
experiment: Experiment) -> Any:
"""
Recreates the Cryspy object for the given sample model and experiment.

Expand All @@ -212,7 +220,8 @@ def _recreate_cryspy_obj(self, sample_model: SampleModels, experiment: Experimen

return cryspy_obj

def _convert_sample_model_to_cryspy_cif(self, sample_model: SampleModels) -> str:
def _convert_sample_model_to_cryspy_cif(self,
sample_model: SampleModel) -> str:
"""
Converts a sample model to a Cryspy CIF string.

Expand All @@ -224,7 +233,9 @@ def _convert_sample_model_to_cryspy_cif(self, sample_model: SampleModels) -> str
"""
return sample_model.as_cif()

def _convert_experiment_to_cryspy_cif(self, experiment: Experiment, linked_phase: Any) -> str:
def _convert_experiment_to_cryspy_cif(self,
experiment: Experiment,
linked_phase: Any) -> str:
"""
Converts an experiment to a Cryspy CIF string.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import os
import tempfile
from numpy.testing import assert_almost_equal

from easydiffraction import (
Project,
SampleModel,
Experiment,
download_from_repository
)

TEMP_DIR = tempfile.gettempdir()


def test_single_fit_neutron_pd_tof_mcstas_lbco_si() -> None:
# Set sample models
model_1 = SampleModel('lbco')
model_1.space_group.name_h_m = 'P m -3 m'
model_1.space_group.it_coordinate_system_code = '1'
model_1.cell.length_a = 3.8909
model_1.atom_sites.add('La', 'La', 0, 0, 0, wyckoff_letter='a', b_iso=0.2, occupancy=0.5)
model_1.atom_sites.add('Ba', 'Ba', 0, 0, 0, wyckoff_letter='a', b_iso=0.2, occupancy=0.5)
model_1.atom_sites.add('Co', 'Co', 0.5, 0.5, 0.5, wyckoff_letter='b', b_iso=0.2567)
model_1.atom_sites.add('O', 'O', 0, 0.5, 0.5, wyckoff_letter='c', b_iso=1.4041)

model_2 = SampleModel('si')
model_2.space_group.name_h_m = 'F d -3 m'
model_2.space_group.it_coordinate_system_code = '2'
model_2.cell.length_a = 5.43146
model_2.atom_sites.add('Si', 'Si', 0.0, 0.0, 0.0, wyckoff_letter='a', b_iso=0.0)

# Set experiment
data_file = 'mcstas_lbco-si.xys'
download_from_repository(data_file, branch='fix-multiphase-fit', destination=TEMP_DIR)
expt = Experiment('mcstas', beam_mode='time-of-flight', data_path=os.path.join(TEMP_DIR, data_file))
expt.instrument.setup_twotheta_bank = 94.90931761529106
expt.instrument.calib_d_to_tof_offset = 0.0
expt.instrument.calib_d_to_tof_linear = 58724.76869981215
expt.instrument.calib_d_to_tof_quad = -0.00001
expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter'
expt.peak.broad_gauss_sigma_0 = 45137
expt.peak.broad_gauss_sigma_1 = -52394
expt.peak.broad_gauss_sigma_2 = 22998
expt.peak.broad_mix_beta_0 = 0.0055
expt.peak.broad_mix_beta_1 = 0.0041
expt.peak.asym_alpha_0 = 0.0
expt.peak.asym_alpha_1 = 0.0097
expt.linked_phases.add('lbco', scale=4.0)
expt.linked_phases.add('si', scale=0.2)
for x in range(45000, 115000, 5000):
expt.background.add(x=x, y=0.2)

# Create project
project = Project()
project.sample_models.add(model_1)
project.sample_models.add(model_2)
project.experiments.add(expt)

# Prepare for fitting
project.analysis.current_calculator = 'cryspy'
project.analysis.current_minimizer = 'lmfit (leastsq)'

# Select fitting parameters
model_1.cell.length_a.free = True
model_1.atom_sites['La'].b_iso.free = True
model_1.atom_sites['Ba'].b_iso.free = True
model_1.atom_sites['Co'].b_iso.free = True
model_1.atom_sites['O'].b_iso.free = True
model_2.cell.length_a.free = True
model_2.atom_sites['Si'].b_iso.free = True
expt.linked_phases['lbco'].scale.free = True
expt.linked_phases['si'].scale.free = True
expt.peak.broad_gauss_sigma_0.free = True
expt.peak.broad_gauss_sigma_1.free = True
expt.peak.broad_gauss_sigma_2.free = True
expt.peak.asym_alpha_1.free = True
expt.peak.broad_mix_beta_0.free = True
expt.peak.broad_mix_beta_1.free = True
for point in expt.background:
point.y.free = True

# Perform fit
project.analysis.fit()

# Compare fit quality
assert_almost_equal(project.analysis.fit_results.reduced_chi_square,
desired=2.87,
decimal=1)


if __name__ == '__main__':
test_single_fit_neutron_pd_tof_mcstas_lbco_si()
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_calculate_single_model_pattern(mock_rhochi_calc, mock_sample_model, moc
def test_recreate_cryspy_dict(mock_sample_model, mock_experiment):
calculator = CryspyCalculator()
calculator._cryspy_dicts = {
"experiment1": {
"sample1_experiment1": {
"pd_experiment1": {
"offset_ttheta": [0.1],
"wavelength": [1.54],
Expand Down
Loading