Skip to content
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
7 changes: 7 additions & 0 deletions hexrd/core/fitting/calibration/powder.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(
tth_distortion=None,
calibration_picks=None,
xray_source: Optional[str] = None,
fixed_pink_asymmetry: Optional[dict] = None,
):
assert list(instr.detectors.keys()) == list(
img_dict.keys()
Expand All @@ -58,6 +59,11 @@ def __init__(
self.min_ampl = min_ampl
self.pktype = pktype
self.bgtype = bgtype
# Optional dict of fixed pink-beam shape params (typically from a prior
# WPPF run) applied to every peak with vary=False. Keys depend on
# pktype; see spectrum.pink_beam_asymmetry_params. Ignored for non-pink
# pktypes.
self.fixed_pink_asymmetry = fixed_pink_asymmetry

self._tth_distortion = tth_distortion
self._update_tth_distortion_panels()
Expand Down Expand Up @@ -130,6 +136,7 @@ def spectrum_kwargs(self):
fwhm_init=self.fwhm_estimate,
min_ampl=self.min_ampl,
min_pk_sep=self.min_pk_sep,
fixed_pink_asymmetry=self.fixed_pink_asymmetry,
)

@property
Expand Down
44 changes: 44 additions & 0 deletions hexrd/core/fitting/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@

pk_prefix_tmpl = "pk%d_"

# The tth-dependent shape parameters for each pink-beam profile. These are the
# parameters that can be supplied via ``fixed_pink_asymmetry`` (typically from a
# prior WPPF refinement) to be held fixed during calibration. Each is a
# polynomial in tan(theta): dcs has separate rising/falling edges (alpha, beta),
# while the heating profiles have a single exponential term (sigma or tau).
pink_beam_asymmetry_params = {
'pink_beam_dcs': ('alpha0', 'alpha1', 'beta0', 'beta1'),
'pink_beam_heating': ('sigma0', 'sigma1'),
'pink_beam_exponential': ('tau0', 'tau1', 'tau2'),
}

alpha0_DFLT, alpha1_DFLT, beta0_DFLT, beta1_DFLT = np.r_[14.45, 0.0, 3.0162, -7.9411]

param_hints_DFLT = (True, None, None, None, None)
Expand Down Expand Up @@ -424,6 +435,7 @@ def __init__(
fwhm_init=None,
min_ampl=1e-4,
min_pk_sep=pk_sep_min,
fixed_pink_asymmetry=None,
):
"""
Instantiates spectrum model.
Expand All @@ -439,6 +451,16 @@ def __init__(
DESCRIPTION. The default is 'pvoigt'.
bgtype : TYPE, optional
DESCRIPTION. The default is 'linear'.
fixed_pink_asymmetry : dict, optional
If provided and pktype is one of the pink-beam profiles, the
tth-dependent shape parameters for that profile are initialized
from this dict and held fixed (vary=False) for every peak. The
expected keys depend on the peak type (see
``pink_beam_asymmetry_params``): alpha0/alpha1/beta0/beta1 for
'pink_beam_dcs', sigma0/sigma1 for 'pink_beam_heating', and
tau0/tau1/tau2 for 'pink_beam_exponential'. Intended for use with
values taken from a prior WPPF refinement. Ignored for other peak
types. The default is None.

Returns
-------
Expand All @@ -459,6 +481,14 @@ def __init__(
self._pktype = pktype
self._bgtype = bgtype

# Only meaningful for the pink-beam profiles; ignored otherwise.
if (
fixed_pink_asymmetry is not None
and pktype not in pink_beam_asymmetry_params
):
fixed_pink_asymmetry = None
self._fixed_pink_asymmetry = fixed_pink_asymmetry

master_keys_pks = _function_dict_1d[pktype]
master_keys_bkg = _function_dict_1d[bgtype]

Expand Down Expand Up @@ -556,6 +586,16 @@ def __init__(
for mp in mparams[1:]:
_set_equality_constraints(initial_params_pks, ((mp, mparams[0]),))

# If WPPF-derived shape values were provided, write them into every
# peak's params. These params (alpha/beta for dcs, sigma for heating,
# tau for exponential) are already vary=False from the per-type setup
# above and stay fixed through fit().
if self._fixed_pink_asymmetry is not None:
for i in range(num_peaks):
prefix = pk_prefix_tmpl % i
for pname, value in self._fixed_pink_asymmetry.items():
initial_params_pks[prefix + pname].value = value

# background
initial_params_bkg = Parameters()
initial_params_bkg.add_many(
Expand Down Expand Up @@ -617,6 +657,10 @@ def fit(self):
param.vary = False

res0 = self.model.fit(ydata, params=self.params, x=xdata)
# With fixed asymmetry supplied, skip the second-stage alpha/beta
# refinement and return the first-stage fit (only amp + cen vary).
if self._fixed_pink_asymmetry is not None:
return res0
if res0.success:
new_p = res0.params
_set_refinement_by_name(new_p, 'alpha0', vary=True)
Expand Down
7 changes: 7 additions & 0 deletions hexrd/powder/fitting/calibration/powder.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(
tth_distortion=None,
calibration_picks=None,
xray_source: Optional[str] = None,
fixed_pink_asymmetry: Optional[dict] = None,
):
assert list(instr.detectors.keys()) == list(
img_dict.keys()
Expand All @@ -58,6 +59,11 @@ def __init__(
self.min_ampl = min_ampl
self.pktype = pktype
self.bgtype = bgtype
# Optional dict of fixed pink-beam shape params (typically from a prior
# WPPF run) applied to every peak with vary=False. Keys depend on
# pktype; see spectrum.pink_beam_asymmetry_params. Ignored for non-pink
# pktypes.
self.fixed_pink_asymmetry = fixed_pink_asymmetry

self._tth_distortion = tth_distortion
self._update_tth_distortion_panels()
Expand Down Expand Up @@ -130,6 +136,7 @@ def spectrum_kwargs(self):
fwhm_init=self.fwhm_estimate,
min_ampl=self.min_ampl,
min_pk_sep=self.min_pk_sep,
fixed_pink_asymmetry=self.fixed_pink_asymmetry,
)

@property
Expand Down
26 changes: 26 additions & 0 deletions tests/core/fitting/calibration/test_fitting_calibration_powder.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,29 @@ def test_evaluation(pc):
pc._evaluate(output='bad')
pc.data_dict = {'det1': []}
assert pc.residual().size == 0


# --- Fixed pink-beam asymmetry forwarding ---


def test_fixed_pink_asymmetry_forwarded_to_spectrum_kwargs(mocks: dict) -> None:
# The asymmetry dict is stored and passed through to the SpectrumModel
# via spectrum_kwargs, so it reaches the per-ring fit.
fixed = {'alpha0': 11.0, 'alpha1': 0.4, 'beta0': 2.1, 'beta1': -5.2}
pc = PowderCalibrator(
mocks['instr'],
mocks['mat'],
{'det1': np.zeros((10, 10))},
tth_tol=0.5,
eta_tol=5.0,
pktype='pink_beam_dcs',
fixed_pink_asymmetry=fixed,
)
assert pc.fixed_pink_asymmetry == fixed
assert pc.spectrum_kwargs['fixed_pink_asymmetry'] == fixed


def test_fixed_pink_asymmetry_defaults_to_none(pc: PowderCalibrator) -> None:
# Default behavior is unchanged: no asymmetry is forwarded.
assert pc.fixed_pink_asymmetry is None
assert pc.spectrum_kwargs['fixed_pink_asymmetry'] is None
110 changes: 110 additions & 0 deletions tests/core/fitting/test_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,113 @@ def test_spectrummodel_background_params_and_none_fwhm_guess():
n_pk = num_func_params["gaussian"]
n_bg = num_func_params["linear"]
assert p0.size == len(centers) * n_pk + n_bg


# ---- fixed pink-beam shape params (from a prior WPPF refinement) ----

# Per-pktype shape values, distinct from the module defaults so we can tell
# they were actually written. Keys/order match s.pink_beam_asymmetry_params.
FIXED_PINK_VALUES = {
'pink_beam_dcs': {
'alpha0': 12.0,
'alpha1': 0.5,
'beta0': 2.5,
'beta1': -6.5,
},
'pink_beam_heating': {'sigma0': 0.05, 'sigma1': 1.5},
'pink_beam_exponential': {'tau0': 1.5, 'tau1': -1.2, 'tau2': 0.3},
}

_PINK_FUNCS = {
'pink_beam_dcs': s.pink_beam_dcs,
'pink_beam_heating': s.pink_beam_heating,
'pink_beam_exponential': s.pink_beam_exponential,
}

PINK_TYPES = list(FIXED_PINK_VALUES)


def _pink_data(pktype: str) -> tuple[np.ndarray, float]:
# Build a clean single-peak spectrum from the fixed shape values, so the
# fit is well-conditioned. The shape values slot in right after amp, cen
# and before the two fwhm args, matching every pink-beam signature.
fixed = FIXED_PINK_VALUES[pktype]
x = np.linspace(0.5, 1.5, 201)
amp, cen = 2.0, 1.0
y = _PINK_FUNCS[pktype](x, amp, cen, *fixed.values(), 0.05, 0.05) + 0.01
return np.vstack([x, y]).T, cen


@pytest.mark.parametrize("pktype", PINK_TYPES)
def test_fixed_pink_shape_written_and_held_fixed(pktype: str) -> None:
# When the shape params are supplied for a pink-beam profile, every peak
# takes the supplied values and is not refined.
fixed = FIXED_PINK_VALUES[pktype]
data, cen = _pink_data(pktype)
sm = SpectrumModel(
data,
[cen],
pktype=pktype,
bgtype="linear",
fwhm_init=0.05,
min_ampl=1e-8,
fixed_pink_asymmetry=fixed,
)
for name, value in fixed.items():
param = sm.peak_params['pk0_' + name]
assert param.value == value
assert param.vary is False


@pytest.mark.parametrize("pktype", PINK_TYPES)
def test_fixed_pink_shape_survives_fit(pktype: str) -> None:
# The supplied values stay fixed (vary=False, unchanged) through fit() for
# every pink-beam profile.
fixed = FIXED_PINK_VALUES[pktype]
data, cen = _pink_data(pktype)
sm = SpectrumModel(
data,
[cen],
pktype=pktype,
bgtype="linear",
fwhm_init=0.05,
min_ampl=1e-8,
fixed_pink_asymmetry=fixed,
)
res = sm.fit()
for name, value in fixed.items():
assert res.params['pk0_' + name].vary is False
assert res.params['pk0_' + name].value == pytest.approx(value)


def test_fixed_pink_shape_ignored_for_other_pktypes() -> None:
# The option is only meaningful for pink-beam profiles; for anything else
# it is silently dropped.
data, cen = _pink_data('pink_beam_dcs')
sm = SpectrumModel(
data,
[cen],
pktype="gaussian",
bgtype="linear",
fwhm_init=0.05,
fixed_pink_asymmetry=FIXED_PINK_VALUES['pink_beam_dcs'],
)
assert sm._fixed_pink_asymmetry is None


def test_fixed_pink_dcs_skips_second_stage_refinement() -> None:
# dcs is the one type whose second stage would otherwise free alpha0; the
# fixed flow must skip it. (heating/exponential never refine their shape
# params, so they have no equivalent contrast.)
data, cen = _pink_data('pink_beam_dcs')
sm_free = SpectrumModel(
data,
[cen],
pktype="pink_beam_dcs",
bgtype="linear",
fwhm_init=0.05,
min_ampl=1e-8,
)
res_free = sm_free.fit()
assert res_free.success
assert res_free.params['pk0_alpha0'].vary is True
Loading