Skip to content

feat: Add ConstantWavelengthInstrument and change CI to get pyobjcryst from main - #8

Merged
sbillinge merged 8 commits into
diffpy:mainfrom
cadenmyers13:constant-wavelength-instrument
Oct 30, 2026
Merged

sbillinge merged 8 commits into
diffpy:mainfrom
cadenmyers13:constant-wavelength-instrument

Conversation

@cadenmyers13

Copy link
Copy Markdown
Contributor

Please merge #7 first. This branch is stacked on it.

What problem does this PR address?

This adds ConstantWavelengthInstrument, the instrument of a constant-wavelength x-ray, neutron or electron diffractometer. It is the first instrument component, and it is built on the Component targets and settings counter from #7.

Parameters (refinable, with ObjCryst's defaults and limits as bounds):

  • zero, displacement, transparency and mu_r, which belong to the pattern.
  • wavelength, which belongs to the pattern's radiation. For a Kα1/Kα2 doublet it is the intensity-weighted mean. It is a Parameter so that it can be refined, for example to calibrate against a standard.

Settings (applied by install; each one bumps settings_version, so every generator holding the instrument installs it again):

  • set_radiation_type("X" | "N" | "E")
  • set_wavelength(wavelength)
  • set_xray_source(element, kalpha2_ratio=0.5)
  • set_xray_wavelengths(kalpha1, kalpha2, kalpha2_ratio=0.5)
  • set_polarization(polarization_rate)

Design choices to review:

  • There is no default wavelength. ObjCryst's default of 1 Å matches no dataset, so wavelength is None until one of the setters gives it a value. Until then, _validate raises a two-part SrFitError, and FitRecipe already validates before a fit.
  • A Kα1/Kα2 doublet only goes with x-rays. Setting a doublet while the radiation type is neutrons or electrons raises an error, and so does the reverse order.
  • The setters check their inputs straight away, with two-part error messages, instead of failing later in update. They check:
    • the wavelength limits
    • that Kα2 is longer than Kα1
    • that the polarization rate is in [-1, 1]
    • the anode element
    • the radiation type

ObjCryst behaviour worked around (each workaround has a test, and each will get an upstream issue):

  • Setting a tube resets the polarization rate, and a new rate only takes effect after the wavelength is set again. install therefore sets the wavelength or tube first, then the rate, then calls Prepare().
  • SetWavelength(wavelength) after a tube keeps the doublet. install sets the wavelength type to monochromatic.
  • An unknown anode element only prints a warning. set_xray_source checks that the wavelength changed. ObjCryst has wavelengths for Ag, Co, Cr, Cu, Fe and Mo only.
  • The Kα2/Kα1 ratio parameter is limited to [0.5, 0.5]. install sets the ratio through SetWavelength(element, ratio), which bypasses the limit.
  • Setting a tube switches the radiation type back to x-rays. Restricting doublets to x-rays avoids this.
  • The wavelength limit of 0.05 Å leaves out electrons from beams faster than about 50 kV. set_wavelength raises an error for these, so electron support is limited for now.

Tests use plain pyobjcryst PbSO4 patterns, as in #6, and compare each result with the same setting made directly in pyobjcryst. They check that:

  • update pushes each Parameter.
  • Each setter sets wavelength and bumps settings_version.
  • A Cu tube and explicit Cu wavelengths both match ObjCryst's tube, at Kα2/Kα1 ratios of 0.5 and 0.4.
  • The polarization factor at the strongest peak follows the formula.
  • Installing again after a polarization change, or after switching from a tube to a single wavelength or to neutrons, gives the right pattern.
  • One instrument installed into two patterns configures both.
  • Each invalid input raises its error message.

Also in this PR:

  • The mark_settings_changed docstring example in component.py now uses set_xray_source.
  • A news item.

CI will fail, as on the previous PRs, because the tests need pyobjcryst main.

@cadenmyers13

Copy link
Copy Markdown
Contributor Author

@sbillinge ready for review. please merge #7 first. Here's a usage code snippet

from pathlib import Path

import numpy as np
from pyobjcryst.crystal import create_crystal_from_cif
from pyobjcryst.powderpattern import PowderPattern

from diffpy.cmirietveld.instruments import ConstantWavelengthInstrument

# A pyobjcryst pattern for PbSO4 on a 16-110 degree 2theta grid
crystal = create_crystal_from_cif(str(Path("tests/testdata/PbSO4-Wyckoff.cif")))
two_theta = np.arange(16.0, 110.0, 0.025)
pattern = PowderPattern()
pattern.SetPowderPatternX(np.radians(two_theta))
pattern.SetPowderPatternObs(np.zeros_like(two_theta))
diffraction = pattern.AddPowderPatternDiffraction(crystal)

# Settings: a lab Cu source with a polarized beam
instrument = ConstantWavelengthInstrument()
instrument.set_xray_source("Cu")
instrument.set_polarization(0.95)

# Parameters: refinable values, here set by hand
instrument.zero.set_value(0.02)
instrument.wavelength.set_value(1.5419)

# Apply the settings, then push the Parameters before calculating
instrument.install(pattern, diffraction)
instrument.update(pattern, diffraction)
pattern.Prepare()
calculated_intensity = np.array(pattern.GetPowderPatternCalc())

@sbillinge

Copy link
Copy Markdown
Contributor

sure. #7 is failing tests. Is it waiting on something else to merge first?

@sbillinge

Copy link
Copy Markdown
Contributor

One thing here is that there is some "magic" (things resetting behind the scenes when other things are set). The logic is perfect (set a tube resets a polarization) so I am ok with it, but it would be great if there was some kind of log or warning to the user when things like this happens so they don't spend hours figuring out why something they set magically changed its value. Not sure the best way to do that, but I just want to minimize witchcraft.

@cadenmyers13 cadenmyers13 changed the title feat: Add ConstantWavelengthInstrument feat: Add ConstantWavelengthInstrument and change CI to get pyobjcryst from main Oct 29, 2026
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.45946% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.94%. Comparing base (d14d455) to head (2ce0be8).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
tests/test_instruments.py 99.34% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main       #8       +/-   ##
===========================================
+ Coverage   50.00%   97.94%   +47.94%     
===========================================
  Files           2        5        +3     
  Lines          18      535      +517     
===========================================
+ Hits            9      524      +515     
- Misses          9       11        +2     
Files with missing lines Coverage Δ
tests/conftest.py 83.92% <100.00%> (+48.21%) ⬆️
tests/test_component.py 100.00% <100.00%> (ø)
tests/test_profiles.py 100.00% <100.00%> (ø)
tests/test_instruments.py 99.34% <99.34%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cadenmyers13

Copy link
Copy Markdown
Contributor Author

Warning tests

test_setter_warns_wavelength_replaced
Common

  • C1: Rerunning the setup cell after refining a synchrotron wavelength warns that the refined value was replaced.
  • C3: Setting your own Kα1/Kα2 wavelengths again after refining warns that the refined value was replaced.
    Edge
  • C2: Setting the Cu tube again after refining, to try a different Kα2 ratio, warns that the refined value was replaced.

test_setter_warns_doublet_replaced
Common

  • C1: Switching from a Cu tube to a single wavelength warns that the doublet was dropped.
  • C3: Switching from a Cu tube to your own Kα1/Kα2 wavelengths warns that the Cu doublet was replaced.
    Edge
  • C2: Switching from your own Kα1/Kα2 wavelengths to a single wavelength warns that the doublet was dropped.
  • C4: Switching from your own Kα1/Kα2 wavelengths to a Cu tube warns that your wavelengths were replaced.

test_polarization_warns_without_xrays
Common

  • C1: Setting a polarization rate on a neutron instrument warns that it has no effect.
    Edge
  • C2: Switching an instrument that has a polarization rate to neutrons warns that the rate no longer has an effect.
  • C3: The same, switching to electrons.

test_setter_without_warning
Common

  • C1: Switching from a Cu tube to a Mo tube before refining doesn't warn.
  • C2: Correcting a synchrotron wavelength before refining doesn't warn.
  • C3: Switching from a single wavelength to a Cu tube doesn't warn.
  • C5: Setting a polarization rate for x-rays doesn't warn.
  • C6: Leaving the polarization at 0 for neutrons doesn't warn.
    Edge
  • C4: Switching from a single wavelength to your own Kα1/Kα2 wavelengths doesn't warn.

test_install_kalpha2_ratio_kept
Common

  • C1: Switching tubes without ever giving a ratio keeps the starting ratio, 0.5.
    Edge
  • C2: Switching from a Cu tube to a Mo tube keeps the ratio you gave.
  • C3: Switching from a Cu tube to your own Kα1/Kα2 wavelengths keeps the ratio you gave.
  • C4: Switching from your own Kα1/Kα2 wavelengths to a Cu tube keeps the ratio you gave.
  • C5: Giving a new ratio replaces the old one.

Non-warning tests

test_parameter_names

  • A new instrument is called "instrument" and has zero, displacement, transparency, mu_r and wavelength.

test_pattern_parameter_values_and_bounds

  • C1: The zero shift starts at 0, within ObjCryst's limits of ±0.05 rad.
  • C2: The displacement starts at 0, within ObjCryst's limits of ±0.05 rad.
  • C3: The transparency starts at 0, within ObjCryst's limits of ±0.05 rad.
  • C4: The absorption starts at 0, within ObjCryst's limits of 0 to 1000.

test_wavelength_before_set

  • A new instrument has no wavelength until you set one.

test_validate_before_wavelength_set

  • Starting a fit without setting a wavelength gives an error saying how to set one.

test_wavelength_after_setter

  • C1: Setting a single wavelength sets the wavelength to that value.
  • C2: Setting a Cu tube sets the wavelength to the tube's mean wavelength.
  • C3: Setting your own Kα1/Kα2 wavelengths sets the wavelength to their mean.
  • C4: Setting a Cu tube with a different Kα2 ratio sets the wavelength to the mean for that ratio.
  • C5: Setting your own Kα1/Kα2 wavelengths with a different ratio sets the wavelength to the mean for that ratio.

test_settings_version_after_setter

  • C1: Changing the radiation type makes generators reinstall the instrument.
  • C2: Setting a single wavelength makes generators reinstall the instrument.
  • C3: Setting a Cu tube makes generators reinstall the instrument.
  • C4: Setting your own Kα1/Kα2 wavelengths makes generators reinstall the instrument.
  • C5: Setting a polarization rate makes generators reinstall the instrument.

test_update_pattern_parameters

  • C1: Changing the zero shift gives the same pattern as setting it directly in pyobjcryst.
  • C2: Changing the displacement gives the same pattern as setting it directly in pyobjcryst.
  • C3: Changing the transparency gives the same pattern as setting it directly in pyobjcryst.
  • C4: Changing the absorption gives the same pattern as setting it directly in pyobjcryst.

test_update_wavelength

  • Refining the wavelength gives the same pattern as setting it directly in pyobjcryst.

test_install_shared_instrument

  • One instrument shared by two phases sets up both patterns the same way.

test_install_neutron_radiation

  • A neutron instrument gives the same pattern as neutrons set directly in pyobjcryst.

test_install_radiation_type

  • C1: Setting neutrons gives the pattern neutron radiation.
  • C2: Setting electrons gives the pattern electron radiation.

test_install_neutrons_after_tube

  • Switching an x-ray tube instrument to neutrons leaves nothing over from the tube.

test_install_polarization_ratio

  • A polarization rate changes the strongest peak by the amount the polarization formula predicts.

test_install_after_polarization_change

  • Changing only the polarization rate still changes the pattern.

test_install_tube_set_after_polarization

  • Setting the tube after the polarization rate keeps the rate.

test_install_polarization_rate_at_range_edges

  • C1: A polarization rate of -1 is accepted.
  • C2: A polarization rate of 1 is accepted.

test_install_single_wavelength_after_tube

  • Switching from a tube to a single wavelength drops the second peak of the doublet.

test_install_xray_source

  • C1: A Cu tube gives the same pattern as a Cu tube set directly in pyobjcryst.
  • C2: A Cu tube with a ratio of 0.4 keeps that ratio instead of falling back to 0.5.

test_install_xray_wavelengths

  • C1: Entering ObjCryst's Cu wavelengths yourself gives the same pattern as the Cu tube.
  • C2: The same with a ratio of 0.4.

test_set_xray_source_unknown_element

  • Asking for a tube ObjCryst doesn't know gives an error listing the ones it has.

test_set_wavelength_outside_limits

  • C1: A wavelength above 20 Å gives an error stating ObjCryst's limits.
  • C2: A modern electron-beam wavelength, below 0.05 Å, gives an error stating ObjCryst's limits.
  • C3: Kα1/Kα2 wavelengths above 20 Å give an error stating ObjCryst's limits.

test_set_xray_wavelengths_kalpha2_not_longer

  • Entering Kα1 and Kα2 the wrong way round gives an error saying so.

test_set_doublet_with_neutron_radiation

  • C1: Setting a Cu tube on a neutron instrument gives an error.
  • C2: Setting your own Kα1/Kα2 wavelengths on a neutron instrument gives an error.

test_set_radiation_type_with_doublet

  • C1: Switching a Cu tube instrument to neutrons gives an error saying to set a single wavelength first.
  • C2: Switching a Cu tube instrument to electrons gives the same error.

test_set_radiation_type_unknown_type

  • Asking for an unknown radiation type gives an error listing x-rays, neutrons and electrons.

test_set_polarization_outside_range

  • C1: A polarization rate above 1 gives an error, which also explains how to convert a GSAS polarization.
  • C2: A polarization rate below -1 gives the same error.

@cadenmyers13

Copy link
Copy Markdown
Contributor Author

@sbillinge ready for review. Once again, please merge #7 first.

I added warnings for several different cases. I recognize some of them are edge cases, but including them doesn't complicate the code too much. Above i have summarized the cases for each tests.

I also changed CI on here such that it it downloads pyobjcryst from main so the tests run. I wrote an issue #9 to change this back for release. This will strictly be for development.

@sbillinge
sbillinge merged commit 2f7c6fe into diffpy:main Oct 30, 2026
6 checks passed
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.

2 participants