use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt::{Display, Formatter};
use phasesmith_model::RecordId;
use crate::{
LatticeBounds, ParameterError, ParameterKey, ParameterSet, ParameterSpec,
PreparedGeneralRietveldObjective, RietveldCalculationOptions, RietveldGeneralObjectiveError,
RietveldGeneralParameterError, RietveldInput, RietveldParameterLayout,
RietveldParameterSelection,
};
#[derive(Clone, Debug, PartialEq)]
pub struct JointRietveldHistogram {
pub histogram_id: RecordId,
pub input: RietveldInput,
pub selection: RietveldParameterSelection,
pub lattice_bounds: Vec<Option<LatticeBounds>>,
pub calculation: RietveldCalculationOptions,
}
#[derive(Clone, Debug, PartialEq)]
pub struct JointRietveldProduct {
pub histogram_id: RecordId,
pub profile: Vec<f64>,
pub derivative: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct JointRietveldGradient {
pub calculated: Vec<Vec<f64>>,
pub objective: f64,
pub gradient: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct JointRietveldLayout {
parameters: ParameterSet,
histogram_ids: Vec<RecordId>,
local_layouts: Vec<RietveldParameterLayout>,
local_to_joint: Vec<Vec<usize>>,
}
impl JointRietveldLayout {
pub fn new(histograms: &[JointRietveldHistogram]) -> Result<Self, JointRietveldError> {
validate_histograms(histograms)?;
let local_layouts = histograms
.iter()
.map(|histogram| {
RietveldParameterLayout::new(
&histogram.input,
&histogram.selection,
&histogram.lattice_bounds,
)
})
.collect::<Result<Vec<_>, _>>()?;
let mut specs = Vec::new();
let mut shared = BTreeMap::<ParameterKey, usize>::new();
let mut local_to_joint = Vec::with_capacity(histograms.len());
for (histogram, layout) in histograms.iter().zip(&local_layouts) {
let mut mapping = Vec::with_capacity(layout.parameters().specs().len());
for spec in layout.parameters().specs() {
let joint_index = if is_shared(spec.key()) {
if let Some(index) = shared.get(spec.key()).copied() {
if specs[index] != *spec {
return Err(JointRietveldError::SharedParameterMismatch {
key: spec.key().clone(),
});
}
index
} else {
let index = specs.len();
specs.push(spec.clone());
shared.insert(spec.key().clone(), index);
index
}
} else {
let key = ParameterKey::new(
spec.key().module(),
format!("{}/{}", histogram.histogram_id, spec.key().owner_id()),
spec.key().name(),
)?;
let index = specs.len();
specs.push(ParameterSpec::new(
key,
spec.value(),
spec.unit(),
spec.bounds(),
spec.scale(),
spec.refine(),
)?);
index
};
mapping.push(joint_index);
}
local_to_joint.push(mapping);
}
Ok(Self {
parameters: ParameterSet::new(specs)?,
histogram_ids: histograms
.iter()
.map(|histogram| histogram.histogram_id.clone())
.collect(),
local_layouts,
local_to_joint,
})
}
#[must_use]
pub const fn parameters(&self) -> &ParameterSet {
&self.parameters
}
#[must_use]
pub fn histogram_ids(&self) -> &[RecordId] {
&self.histogram_ids
}
pub fn apply_values(
&self,
histograms: &[JointRietveldHistogram],
values: &[f64],
) -> Result<Vec<JointRietveldHistogram>, JointRietveldError> {
self.validate_contract(histograms)?;
if values.len() != self.parameters.specs().len() {
return Err(JointRietveldError::ValueLengthMismatch);
}
histograms
.iter()
.enumerate()
.map(|(histogram_index, histogram)| {
let local_values = self.local_to_joint[histogram_index]
.iter()
.map(|index| values[*index])
.collect::<Vec<_>>();
let mut updated = histogram.clone();
updated.input = self.local_layouts[histogram_index]
.apply_values(&histogram.input, &local_values)?;
Ok(updated)
})
.collect()
}
fn validate_contract(
&self,
histograms: &[JointRietveldHistogram],
) -> Result<(), JointRietveldError> {
if histograms.len() != self.histogram_ids.len()
|| histograms
.iter()
.zip(&self.histogram_ids)
.any(|(histogram, expected)| histogram.histogram_id != *expected)
{
return Err(JointRietveldError::HistogramContractMismatch);
}
validate_histograms(histograms)?;
for ((histogram, expected_layout), expected_mapping) in histograms
.iter()
.zip(&self.local_layouts)
.zip(&self.local_to_joint)
{
let current_layout = RietveldParameterLayout::new(
&histogram.input,
&histogram.selection,
&histogram.lattice_bounds,
)?;
if current_layout != *expected_layout
|| current_layout.parameters().specs().len() != expected_mapping.len()
{
return Err(JointRietveldError::HistogramContractMismatch);
}
}
Ok(())
}
fn local_direction(
&self,
histogram_index: usize,
direction: &[f64],
) -> Result<Vec<f64>, JointRietveldError> {
if direction.len() != self.parameters.specs().len() {
return Err(JointRietveldError::ValueLengthMismatch);
}
Ok(self.local_to_joint[histogram_index]
.iter()
.map(|index| direction[*index])
.collect())
}
fn scatter_add(
&self,
histogram_index: usize,
local: &[f64],
joint: &mut [f64],
) -> Result<(), JointRietveldError> {
if local.len() != self.local_to_joint[histogram_index].len() {
return Err(JointRietveldError::LocalProductLengthMismatch);
}
for (value, index) in local.iter().zip(&self.local_to_joint[histogram_index]) {
joint[*index] += value;
}
Ok(())
}
}
pub struct PreparedJointRietveldObjective {
histograms: Vec<JointRietveldHistogram>,
layout: JointRietveldLayout,
objectives: Vec<PreparedGeneralRietveldObjective>,
}
impl PreparedJointRietveldObjective {
pub fn new(
histograms: Vec<JointRietveldHistogram>,
layout: JointRietveldLayout,
) -> Result<Self, JointRietveldError> {
layout.validate_contract(&histograms)?;
let objectives = histograms
.iter()
.zip(&layout.local_layouts)
.map(|(histogram, local_layout)| {
PreparedGeneralRietveldObjective::new(
histogram.input.clone(),
histogram.calculation.clone(),
local_layout.clone(),
)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
histograms,
layout,
objectives,
})
}
#[must_use]
pub const fn layout(&self) -> &JointRietveldLayout {
&self.layout
}
#[must_use]
pub fn preparation_evaluation_count(&self) -> usize {
self.objectives
.iter()
.map(PreparedGeneralRietveldObjective::preparation_evaluation_count)
.sum()
}
#[must_use]
pub fn normal_product_evaluation_count(&self) -> usize {
self.objectives
.iter()
.map(PreparedGeneralRietveldObjective::normal_product_evaluation_count)
.sum()
}
pub fn jvp(&self, direction: &[f64]) -> Result<Vec<JointRietveldProduct>, JointRietveldError> {
self.objectives
.iter()
.enumerate()
.map(|(index, objective)| {
let local = self.layout.local_direction(index, direction)?;
let (profile, derivative) = objective.jvp(&local)?;
Ok(JointRietveldProduct {
histogram_id: self.histograms[index].histogram_id.clone(),
profile,
derivative,
})
})
.collect()
}
pub fn vjp(&self, sample_weights: &[Vec<f64>]) -> Result<Vec<f64>, JointRietveldError> {
if sample_weights.len() != self.objectives.len() {
return Err(JointRietveldError::HistogramProductCountMismatch);
}
let mut result = vec![0.0; self.layout.parameters.specs().len()];
for (index, (objective, weights)) in self.objectives.iter().zip(sample_weights).enumerate()
{
self.layout
.scatter_add(index, &objective.vjp(weights)?, &mut result)?;
}
Ok(result)
}
pub fn normal_product(
&self,
direction: &[f64],
damping: f64,
) -> Result<Vec<f64>, JointRietveldError> {
if !damping.is_finite() || damping < 0.0 {
return Err(JointRietveldError::InvalidDamping);
}
let mut result = vec![0.0; self.layout.parameters.specs().len()];
for (index, objective) in self.objectives.iter().enumerate() {
let local = self.layout.local_direction(index, direction)?;
let local_product = objective.normal_product(&local, 0.0)?;
self.layout
.scatter_add(index, &local_product, &mut result)?;
}
for (value, direction) in result.iter_mut().zip(direction) {
*value += damping * direction;
}
Ok(result)
}
pub fn gradient(&self) -> Result<JointRietveldGradient, JointRietveldError> {
let mut calculated = Vec::with_capacity(self.objectives.len());
let mut gradient = vec![0.0; self.layout.parameters.specs().len()];
let mut value = 0.0;
for (index, objective) in self.objectives.iter().enumerate() {
let (profile, local_gradient) = objective.gradient()?;
value += 0.5 * objective.calculation().metrics.chi_square;
calculated.push(profile);
self.layout
.scatter_add(index, &local_gradient, &mut gradient)?;
}
Ok(JointRietveldGradient {
calculated,
objective: value,
gradient,
})
}
}
fn is_shared(key: &ParameterKey) -> bool {
matches!(key.module(), "lattice" | "site")
}
fn validate_histograms(histograms: &[JointRietveldHistogram]) -> Result<(), JointRietveldError> {
if histograms.len() < 2 {
return Err(JointRietveldError::TooFewHistograms);
}
if histograms
.iter()
.map(|histogram| &histogram.histogram_id)
.collect::<BTreeSet<_>>()
.len()
!= histograms.len()
{
return Err(JointRietveldError::DuplicateHistogramId);
}
let shared_selection = histograms[0].selection.structural;
if histograms.iter().skip(1).any(|histogram| {
let selection = histogram.selection.structural;
selection.lattice != shared_selection.lattice
|| selection.coordinates != shared_selection.coordinates
|| selection.occupancy != shared_selection.occupancy
|| selection.u_iso != shared_selection.u_iso
}) {
return Err(JointRietveldError::SharedSelectionMismatch);
}
let mut phases = BTreeMap::new();
for histogram in histograms {
histogram.input.validate()?;
for phase in &histogram.input.phases {
let definition = phase.definition();
let contract = (
phase.site_ids(),
definition.cell,
&definition.space_group,
&definition.fractional_xyz,
&definition.occupancy,
&definition.u_iso_angstrom2,
&definition.anisotropic_mask,
&definition.u_aniso_cif_angstrom2,
definition.coordinate_tolerance.to_bits(),
);
if let Some(previous) = phases.insert(phase.phase_id().clone(), contract) {
if previous != contract {
return Err(JointRietveldError::SharedPhaseMismatch {
phase_id: phase.phase_id().clone(),
});
}
}
}
}
Ok(())
}
#[derive(Debug)]
pub enum JointRietveldError {
TooFewHistograms,
DuplicateHistogramId,
SharedSelectionMismatch,
SharedPhaseMismatch {
phase_id: RecordId,
},
SharedParameterMismatch {
key: ParameterKey,
},
HistogramContractMismatch,
ValueLengthMismatch,
HistogramProductCountMismatch,
LocalProductLengthMismatch,
InvalidDamping,
Parameter(ParameterError),
GeneralParameter(RietveldGeneralParameterError),
GeneralObjective(RietveldGeneralObjectiveError),
}
impl Display for JointRietveldError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooFewHistograms => {
formatter.write_str("joint Rietveld objective requires at least two histograms")
}
Self::DuplicateHistogramId => {
formatter.write_str("joint Rietveld histogram IDs must be unique")
}
Self::SharedSelectionMismatch => {
formatter.write_str("joint Rietveld shared structural selections must match")
}
Self::SharedPhaseMismatch { phase_id } => write!(
formatter,
"joint Rietveld phase {phase_id:?} has incompatible shared structural state"
),
Self::SharedParameterMismatch { key } => {
write!(
formatter,
"joint Rietveld shared parameter {key} is incompatible"
)
}
Self::HistogramContractMismatch => {
formatter.write_str("joint Rietveld histogram contract changed under the layout")
}
Self::ValueLengthMismatch => {
formatter.write_str("joint Rietveld value/direction length is wrong")
}
Self::HistogramProductCountMismatch => {
formatter.write_str("joint Rietveld histogram reverse-product count is wrong")
}
Self::LocalProductLengthMismatch => {
formatter.write_str("joint Rietveld local product length is wrong")
}
Self::InvalidDamping => {
formatter.write_str("joint Rietveld damping must be finite and non-negative")
}
Self::Parameter(error) => Display::fmt(error, formatter),
Self::GeneralParameter(error) => Display::fmt(error, formatter),
Self::GeneralObjective(error) => Display::fmt(error, formatter),
}
}
}
impl Error for JointRietveldError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Parameter(error) => Some(error),
Self::GeneralParameter(error) => Some(error),
Self::GeneralObjective(error) => Some(error),
Self::TooFewHistograms
| Self::DuplicateHistogramId
| Self::SharedSelectionMismatch
| Self::SharedPhaseMismatch { .. }
| Self::SharedParameterMismatch { .. }
| Self::HistogramContractMismatch
| Self::ValueLengthMismatch
| Self::HistogramProductCountMismatch
| Self::LocalProductLengthMismatch
| Self::InvalidDamping => None,
}
}
}
impl From<ParameterError> for JointRietveldError {
fn from(value: ParameterError) -> Self {
Self::Parameter(value)
}
}
impl From<RietveldGeneralParameterError> for JointRietveldError {
fn from(value: RietveldGeneralParameterError) -> Self {
Self::GeneralParameter(value)
}
}
impl From<RietveldGeneralObjectiveError> for JointRietveldError {
fn from(value: RietveldGeneralObjectiveError) -> Self {
Self::GeneralObjective(value)
}
}
impl From<crate::RietveldError> for JointRietveldError {
fn from(value: crate::RietveldError) -> Self {
Self::GeneralParameter(RietveldGeneralParameterError::Rietveld(value))
}
}