Skip to content

feat: add PowderDiffractionGenerator and methods for printing information - #12

Open
cadenmyers13 wants to merge 6 commits into
diffpy:mainfrom
cadenmyers13:powder-diffraction-generator
Open

cadenmyers13 wants to merge 6 commits into
diffpy:mainfrom
cadenmyers13:powder-diffraction-generator

Conversation

@cadenmyers13

Copy link
Copy Markdown
Contributor

What problem does this PR address?

This adds PowderDiffractionGenerator, an srfit ProfileGenerator that calculates one phase's Bragg intensity with pyobjcryst. It is put together from the components of the earlier PRs: a structure, an instrument (#8) and a peak profile (#6, #10). Part of #1.

Example

from diffpy.cmirietveld.instruments import ConstantWavelengthInstrument
from diffpy.cmirietveld.peak_profiles import PseudoVoigtProfile
from diffpy.cmirietveld.powderdiffractiongenerator import PowderDiffractionGenerator
from diffpy.srfit.fitbase import FitContribution, FitRecipe

instrument = ConstantWavelengthInstrument()
instrument.set_xray_source("Cu")

generator = PowderDiffractionGenerator()          # named "bragg"
generator.set_structure("PbSO4-Wyckoff.cif")      # a Crystal, a CIF path or a ParameterSet
generator.set_instrument(instrument)
generator.set_peak_profile(PseudoVoigtProfile())

contribution = FitContribution("xray")
contribution.set_profile(profile)
contribution.add_profile_generator(generator)
contribution.set_equation("bragg")

recipe = FitRecipe()
recipe.add_contribution(contribution)
recipe.add_variable(generator.scale)
recipe.add_variable(generator.phase.a)
recipe.add_variable(generator.peak_profile.W)
recipe.add_variable(generator.instrument.zero)

generator.print_info() shows what the generator holds:

PowderDiffractionGenerator 'bragg'
  scale = 1
  phase: Crystal 'O4 Pb S', space group P n m a
    a = 8.48
    b = 5.398
    c = 6.958
    alpha = 90
    beta = 90
    gamma = 90
  instrument: ConstantWavelengthInstrument
    settings: radiation_type = X, xray_source = Cu, kalpha2_ratio = 0.5, polarization_rate = 0
    zero = 0
    displacement = 0
    transparency = 0
    mu_r = 0
    wavelength = 1.54184
  peak_profile: PseudoVoigtProfile
    U = 0
    V = 0
    W = 0.01
    eta0 = 0.5
    eta1 = 0
    asym0 = 1
    asym1 = 0
    asym2 = 0

To refine one structure against two datasets, give both generators the same ParameterSet:

phase = structure_to_parameter_set("pbso4", crystal)
xray_generator.set_structure(phase)
neutron_generator.set_structure(phase)

Design choices to review

  • No defaults. The structure, the instrument and the peak profile must all be set, so every script shows which models it refines. Calculating, or starting a fit, before one is set raises a two-part error, for example: "The generator has no peak profile. Please set one with set_peak_profile, such as a PseudoVoigtProfile or a TCHProfile."
  • set_structure takes a structure, a path or a ParameterSet, matching diffpy.cmipdf's PDF generators. It reads CIF files only. A diffpy.structure Structure raises a two-part error, because ObjCryst can't calculate with it.
  • Components are installed lazily, just before each calculation. The generator records which component object and settings_version it last installed. It installs again when either changes, so a new component, a changed setting (such as a new x-ray tube) and a component set before the structure all go through one path.
  • Component.get_settings() is new. It returns a component's settings, such as the radiation type or the x-ray tube, so print_info can show them next to the refinable Parameters. It returns {} by default; ConstantWavelengthInstrument lists the settings in use.
  • Replacing a component or structure warns if its Parameters are in a recipe. Otherwise the recipe would silently refine Parameters that no longer change the pattern. Detecting this relies on srfit's private _observers.
  • scale is applied in Python. ObjCryst's own scale factor stays at 1, because changing it doesn't invalidate its cached pattern.
  • Prepare() runs before every calculation, because ObjCryst doesn't regenerate its reflection list when the wavelength or the grid changes. This adds about 0.6 ms per call.
  • Only 2θ grids for now. Fitting data on a q or d grid comes in a later PR.

Tests compare the generator with plain pyobjcryst set up the same way. They cover:

  • pseudo-Voigt and TCH profiles
  • a peak profile defined in conftest.py, standing in for one from another package, which works without any change to the generator
  • changes to Parameters and settings, including through a FitContribution
  • components and structures shared between two generators
  • values outside ObjCryst's limits
  • a missing structure, instrument or peak profile, both when calculating and when starting a fit
  • print_info and get_settings
  • every error and warning message

What should the reviewer(s) do?

Review the design choices above, and merge.

cadenmyers13 and others added 5 commits November 2, 2026 12:10
@cadenmyers13

Copy link
Copy Markdown
Contributor Author

@sbillinge ready for review, please see the section "What should the reviewer(s) do?"

diffpy.cmistructure 0.1.1 requires Python <3.14, so CI's tests on 3.14
could not install it. Cap requires-python at <3.14 in pyproject.toml and
cookiecutter.json, and use 3.13 in the getting-started docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.73%. Comparing base (e7f5fd4) to head (06f3507).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #12      +/-   ##
==========================================
+ Coverage   97.72%   98.73%   +1.01%     
==========================================
  Files           6        7       +1     
  Lines         658     1189     +531     
==========================================
+ Hits          643     1174     +531     
  Misses         15       15              
Files with missing lines Coverage Δ
tests/conftest.py 90.62% <100.00%> (+6.69%) ⬆️
tests/test_component.py 100.00% <100.00%> (ø)
tests/test_instruments.py 99.35% <100.00%> (+0.01%) ⬆️
tests/test_powderdiffractiongenerator.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant