phasesmith-workflows 0.4.1

Application-neutral native workflows for PhaseSmith
Documentation
//! Project-level ownership and cross-record validation for native Rietveld state.

use std::collections::BTreeMap;

use phasesmith_core::{
    ConstantWavelengthInstrument, OwnedCwContributionArrays, OwnedCwContributions,
};
use phasesmith_crystallography::{IntegratedIntensityCorrectionModel, UnitCell};
use phasesmith_engine::{
    BuiltInScatteringModel, MonochromaticPositionCorrection, StructuralPhaseDefinition,
};
use phasesmith_execution::ExecutionPolicy;
use phasesmith_io::space_group_by_number;
use phasesmith_model::{
    ExperimentRecord, FixedWavelengthSpectrum, HistogramRecord, PatternRecord, ProjectRecord,
    ProviderRequirement, RadiationDefinition, RadiationProbe, RecordId, StructuralPhaseRecord,
};
use phasesmith_workflows::{
    RefinementLimits, RietveldAnalysis, RietveldCalculationOptions, RietveldCovarianceOptions,
    RietveldInput, RietveldParameterSelection, RietveldPhase, RietveldProjectError,
    RietveldProjectState, RietveldRefinementOptions, refine_general_rietveld,
};

fn instrument() -> ConstantWavelengthInstrument {
    ConstantWavelengthInstrument {
        wavelength_angstrom: 1.5406,
        u_deg2: 2.0e-4,
        v_deg2: -1.0e-4,
        w_deg2: 1.2e-4,
        x_deg: 1.5e-3,
        y_deg: 3.0e-3,
    }
}

fn phase() -> RietveldPhase {
    let definition = StructuralPhaseDefinition {
        cell: UnitCell {
            a_angstrom: 4.7,
            b_angstrom: 4.7,
            c_angstrom: 4.7,
            alpha_deg: 90.0,
            beta_deg: 90.0,
            gamma_deg: 90.0,
        },
        space_group: space_group_by_number(221).unwrap().space_group,
        hkl: vec![[1, 0, 0]],
        multiplicity: vec![6],
        fractional_xyz: vec![[0.0, 0.0, 0.0]],
        occupancy: vec![1.0],
        u_iso_angstrom2: vec![0.01],
        anisotropic_mask: vec![false],
        u_aniso_cif_angstrom2: vec![[0.0; 6]],
        scattering_species: vec!["Si".to_owned()],
        scattering_real_offset: Vec::new(),
        scattering_imag_offset: Vec::new(),
        scale: 1.0,
        coordinate_tolerance: 1.0e-10,
        scattering_model: BuiltInScatteringModel::XrayNonResonant,
        correction_model: IntegratedIntensityCorrectionModel::Neutral,
    };
    RietveldPhase::new_with_site_ids(
        RecordId::new("alpha").unwrap(),
        "Alpha",
        vec![RecordId::new("Si1").unwrap()],
        definition,
        OwnedCwContributions::neutral(1),
    )
    .unwrap()
}

fn options() -> RietveldRefinementOptions {
    RietveldRefinementOptions::new(
        RietveldCalculationOptions::new(20.0, true, ExecutionPolicy::new(Some(1), 1).unwrap())
            .unwrap(),
        RefinementLimits::new(4, 100, None, 10).unwrap(),
        1,
        1.0e-8,
        1.0e-7,
        1.0e-6,
        10.0,
        0.3,
        1.0e-6,
        20,
        0.25,
        4,
    )
    .unwrap()
}

fn state() -> RietveldProjectState {
    let phase = phase();
    let x_deg = (0..101)
        .map(|index| 20.0 + f64::from(index) * 0.1)
        .collect::<Vec<_>>();
    let pattern = PatternRecord::new(
        x_deg.clone(),
        Some(vec![0.0; x_deg.len()]),
        None,
        None,
        None,
    )
    .unwrap();
    let position = MonochromaticPositionCorrection {
        zero_shift_deg: 0.0,
        bragg_brentano_mm: None,
        debye_scherrer_micrometre: None,
    };
    let input = RietveldInput::new(
        pattern.clone(),
        instrument(),
        None,
        position,
        vec![phase.clone()],
    )
    .unwrap();
    let selection = RietveldParameterSelection::default();
    let native_options = options();
    let solved = refine_general_rietveld(
        &input,
        &selection,
        &[None],
        &[],
        &native_options,
        RietveldCovarianceOptions::default(),
        None,
        None,
    )
    .unwrap();
    RietveldProjectState {
        project: ProjectRecord {
            project_id: RecordId::new("project").unwrap(),
            revision: 3,
            name: "Project".to_owned(),
            histograms: vec![HistogramRecord {
                histogram_id: RecordId::new("histogram").unwrap(),
                name: "Histogram".to_owned(),
                pattern,
                experiment: ExperimentRecord::new(
                    instrument(),
                    RadiationDefinition::Monochromatic {
                        probe: RadiationProbe::Xray,
                        wavelength_angstrom: instrument().wavelength_angstrom,
                    },
                    None,
                    position,
                )
                .unwrap(),
                phase_ids: vec![phase.phase_id().clone()],
            }],
            tof_histograms: Vec::new(),
            phases: vec![StructuralPhaseRecord {
                phase_id: phase.phase_id().clone(),
                name: phase.name().to_owned(),
                definition: phase.definition().clone(),
                required_providers: Vec::new(),
            }],
            metadata: BTreeMap::new(),
        },
        analyses: vec![RietveldAnalysis {
            histogram_id: RecordId::new("histogram").unwrap(),
            input,
            selection,
            lattice_bounds: vec![None],
            constraints: Vec::new(),
            options: native_options,
            covariance: RietveldCovarianceOptions::default(),
            checkpoint: Some(solved.checkpoint),
        }],
    }
}

#[test]
fn complete_project_analysis_and_checkpoint_validate_together() {
    state().validate().unwrap();
}

#[test]
fn fixed_spectrum_project_analysis_validates_without_python_runtime() {
    let mut value = state();
    let spectrum = FixedWavelengthSpectrum::new(vec![1.5406, 1.54439], vec![1.0, 0.48]).unwrap();
    value.project.histograms[0].experiment.radiation = RadiationDefinition::FixedSpectrum {
        probe: RadiationProbe::Xray,
        spectrum: spectrum.clone(),
    };
    let previous = value.analyses[0].input.clone();
    value.analyses[0].input = RietveldInput::new_fixed_spectrum(
        previous.pattern,
        previous.instrument,
        spectrum,
        previous.axial_geometry,
        previous.position_correction,
        previous.phases,
    )
    .unwrap();
    value.analyses[0].checkpoint = None;
    value.validate().unwrap();
}

#[test]
fn cross_record_mismatches_are_rejected_before_adapter_use() {
    let mut value = state();
    value.analyses[0].options.limits = RefinementLimits::new(1, 100, None, 10).unwrap();
    value.analyses[0]
        .checkpoint
        .as_mut()
        .unwrap()
        .completed_iterations = 2;
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::CheckpointExceedsIterationLimit)
    ));

    let mut value = state();
    value.analyses.push(value.analyses[0].clone());
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::DuplicateAnalysis { .. })
    ));

    let mut value = state();
    value.analyses[0].histogram_id = RecordId::new("missing").unwrap();
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::UnknownHistogram { .. })
    ));

    let mut value = state();
    value.project.histograms[0].pattern.background_y[0] = 1.0;
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::HistogramStateMismatch { .. })
    ));

    let mut value = state();
    value.project.histograms[0].experiment.radiation = RadiationDefinition::FixedSpectrum {
        probe: RadiationProbe::Xray,
        spectrum: FixedWavelengthSpectrum::new(vec![1.5406, 1.5444], vec![1.0, 0.5]).unwrap(),
    };
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::HistogramStateMismatch { .. })
    ));

    let mut value = state();
    value.project.phases[0].required_providers =
        vec![ProviderRequirement::new("custom.scattering", "1").unwrap()];
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::ExternalProviderRequired { .. })
    ));

    let mut value = state();
    let stored = &value.project.phases[0];
    value.analyses[0].input.phases[0] = RietveldPhase::new_with_site_ids(
        stored.phase_id.clone(),
        stored.name.clone(),
        vec![RecordId::new("Si1").unwrap()],
        stored.definition.clone(),
        OwnedCwContributions::new(
            1,
            0,
            OwnedCwContributionArrays {
                gaussian_variance_deg2: vec![0.0],
                lorentzian_fwhm_deg: vec![0.0],
                intensity_multiplier: vec![2.0],
                d_gaussian_variance_d_position: vec![0.0],
                d_lorentzian_fwhm_d_position: vec![0.0],
                d_intensity_multiplier_d_position: vec![0.0],
                ..OwnedCwContributionArrays::default()
            },
        )
        .unwrap(),
    )
    .unwrap();
    value.analyses[0].checkpoint = None;
    assert!(matches!(
        value.validate(),
        Err(RietveldProjectError::OpaqueStaticContributions { .. })
    ));
}