holos-tda 0.9.0

Vietoris-Rips persistence and checked degree-Rips modules
Documentation
use std::collections::BTreeSet;

use crate::{Error, KineticEdgeKey, Result};

use super::model::{
    CohomologyInterventionArtifact, CohomologyInterventionCandidate, CohomologyInterventionLimits,
    CohomologyInterventionScenario, CohomologyInterventionStatus, FORMAT_MAX_CANDIDATES,
    FORMAT_MAX_ORACLE_CALLS, FORMAT_MAX_PROOF_TERMS, FORMAT_MAX_SCENARIOS, FORMAT_MAX_SEARCH_NODES,
};

pub(super) fn validate_claim_shape(artifact: &CohomologyInterventionArtifact) -> Result<()> {
    if artifact.before_ranks.len() != artifact.scenarios.len()
        || artifact.after_ranks.len() != artifact.scenarios.len()
        || artifact.oracle_calls > artifact.oracle_limit
        || artifact.search_nodes > artifact.node_limit
    {
        Err(Error::InvalidInput(
            "cohomology intervention claim shape is invalid".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_edits(artifact: &CohomologyInterventionArtifact) -> Result<()> {
    let positions = artifact
        .edits
        .iter()
        .map(|edit| {
            artifact.candidates.binary_search(edit).map_err(|_| {
                Error::InvalidInput("cohomology intervention edit is not a candidate".into())
            })
        })
        .collect::<Result<Vec<_>>>()?;
    if positions.windows(2).any(|pair| pair[0] >= pair[1])
        || artifact.edits.len() > artifact.max_edits
    {
        Err(Error::InvalidInput(
            "cohomology intervention edit list is not canonical".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_root_blocker_claim(
    artifact: &CohomologyInterventionArtifact,
    limits: CohomologyInterventionLimits,
) -> Result<()> {
    let mut seen = BTreeSet::new();
    let mut proof_terms = 0usize;
    let mut bound = 0u64;
    for blocker in &artifact.root_blockers {
        validate_root_blocker(blocker, artifact.candidates.len(), &mut seen)?;
        proof_terms = proof_terms.checked_add(blocker.len()).ok_or_else(|| {
            Error::InvalidInput("cohomology intervention proof term count overflows".into())
        })?;
        let minimum = blocker
            .iter()
            .map(|position| artifact.candidates[*position].cost)
            .min()
            .expect("a checked blocker is nonempty");
        bound = bound.checked_add(minimum).ok_or_else(|| {
            Error::InvalidInput("cohomology intervention blocker bound overflows".into())
        })?;
    }
    if proof_terms > limits.max_proof_terms.min(FORMAT_MAX_PROOF_TERMS)
        || bound != artifact.root_blocker_bound
    {
        Err(Error::InvalidInput(
            "cohomology intervention blocker claim is invalid".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_root_blocker(
    blocker: &[usize],
    candidate_count: usize,
    seen: &mut BTreeSet<usize>,
) -> Result<()> {
    if blocker.is_empty()
        || blocker.windows(2).any(|pair| pair[0] >= pair[1])
        || blocker.iter().any(|position| *position >= candidate_count)
        || blocker.iter().any(|position| !seen.insert(*position))
    {
        Err(Error::InvalidInput(
            "cohomology intervention root blockers are not canonical and disjoint".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_status_claim(artifact: &CohomologyInterventionArtifact) -> Result<()> {
    match artifact.status {
        CohomologyInterventionStatus::Optimal => validate_optimal_claim(artifact),
        CohomologyInterventionStatus::Infeasible => validate_infeasible_claim(artifact),
        CohomologyInterventionStatus::SearchIncomplete => validate_incomplete_claim(artifact),
    }
}

pub(super) fn validate_optimal_claim(artifact: &CohomologyInterventionArtifact) -> Result<()> {
    if artifact.lower_bound_cost.is_none()
        || artifact.lower_bound_cost != artifact.upper_bound_cost
        || artifact.edits.is_empty() && artifact.upper_bound_cost != Some(0)
    {
        Err(Error::InvalidInput(
            "optimal intervention bounds are invalid".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_infeasible_claim(artifact: &CohomologyInterventionArtifact) -> Result<()> {
    if !artifact.edits.is_empty()
        || artifact.lower_bound_cost.is_some()
        || artifact.upper_bound_cost.is_some()
    {
        Err(Error::InvalidInput(
            "infeasible intervention carries a finite bound".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_incomplete_claim(artifact: &CohomologyInterventionArtifact) -> Result<()> {
    if artifact.lower_bound_cost.is_none()
        || artifact
            .lower_bound_cost
            .zip(artifact.upper_bound_cost)
            .is_some_and(|(lower, upper)| lower > upper)
    {
        Err(Error::InvalidInput(
            "incomplete intervention bounds are invalid".into(),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn validate_decoded_artifact(
    artifact: &CohomologyInterventionArtifact,
    bytes: &[u8],
    limits: CohomologyInterventionLimits,
) -> Result<()> {
    artifact.validate_claim(limits)?;
    if artifact.compute_digest()? != artifact.digest {
        return Err(Error::InvalidInput(
            "cohomology intervention digest differs from its content".into(),
        ));
    }
    artifact.verify(limits)?;
    if artifact.encode(limits)? != bytes {
        return Err(Error::InvalidInput(
            "cohomology intervention encoding is not canonical".into(),
        ));
    }
    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub(super) fn validate_problem(
    vertex_count: usize,
    scale: f64,
    scenarios: &[CohomologyInterventionScenario],
    candidates: &[CohomologyInterventionCandidate],
    max_edits: usize,
    oracle_limit: usize,
    node_limit: usize,
    limits: CohomologyInterventionLimits,
) -> Result<()> {
    validate_problem_scope(vertex_count, scale, scenarios, limits)?;
    for scenario in scenarios {
        validate_edges(
            vertex_count,
            &scenario.active_edges,
            limits.max_edges_per_scenario,
            "active edge",
        )?;
    }
    validate_candidates(vertex_count, candidates, limits)?;
    validate_inactive_candidates(scenarios, candidates)?;
    validate_search_limits(
        max_edits,
        candidates.len(),
        oracle_limit,
        node_limit,
        limits,
    )
}

pub(super) fn validate_problem_scope(
    vertex_count: usize,
    scale: f64,
    scenarios: &[CohomologyInterventionScenario],
    limits: CohomologyInterventionLimits,
) -> Result<()> {
    if vertex_count > limits.max_vertices {
        return Err(Error::InvalidInput(
            "cohomology intervention vertex count exceeds its limit".into(),
        ));
    }
    if !scale.is_finite() || scale < 0.0 {
        return Err(Error::InvalidInput(
            "cohomology intervention scale must be finite and non-negative".into(),
        ));
    }
    if scenarios.is_empty() || scenarios.len() > limits.max_scenarios.min(FORMAT_MAX_SCENARIOS) {
        return Err(Error::InvalidInput(
            "cohomology intervention scenario count is invalid".into(),
        ));
    }
    Ok(())
}

pub(super) fn validate_candidates(
    vertex_count: usize,
    candidates: &[CohomologyInterventionCandidate],
    limits: CohomologyInterventionLimits,
) -> Result<()> {
    if candidates.len() > limits.max_candidates.min(FORMAT_MAX_CANDIDATES)
        || candidates.iter().any(|candidate| candidate.cost == 0)
        || candidates.iter().any(|candidate| {
            candidate.edge.u >= candidate.edge.v || candidate.edge.v >= vertex_count
        })
        || candidates
            .windows(2)
            .any(|pair| pair[0].edge >= pair[1].edge)
    {
        return Err(Error::InvalidInput(
            "cohomology intervention candidate list is not canonical".into(),
        ));
    }
    Ok(())
}

pub(super) fn validate_inactive_candidates(
    scenarios: &[CohomologyInterventionScenario],
    candidates: &[CohomologyInterventionCandidate],
) -> Result<()> {
    for scenario in scenarios {
        let active = scenario
            .active_edges
            .iter()
            .copied()
            .collect::<BTreeSet<_>>();
        if candidates
            .iter()
            .any(|candidate| active.contains(&candidate.edge))
        {
            return Err(Error::InvalidInput(
                "cohomology intervention candidate is active in a scenario".into(),
            ));
        }
    }
    Ok(())
}

pub(super) fn validate_search_limits(
    max_edits: usize,
    candidate_count: usize,
    oracle_limit: usize,
    node_limit: usize,
    limits: CohomologyInterventionLimits,
) -> Result<()> {
    if max_edits > candidate_count {
        return Err(Error::InvalidInput(
            "cohomology intervention edit limit exceeds the candidate count".into(),
        ));
    }
    if oracle_limit == 0
        || oracle_limit > limits.max_oracle_calls.min(FORMAT_MAX_ORACLE_CALLS)
        || node_limit == 0
        || node_limit > limits.max_search_nodes.min(FORMAT_MAX_SEARCH_NODES)
    {
        return Err(Error::InvalidInput(
            "cohomology intervention search limits are invalid".into(),
        ));
    }
    Ok(())
}

pub(super) fn validate_edges(
    vertex_count: usize,
    edges: &[KineticEdgeKey],
    maximum: usize,
    name: &str,
) -> Result<()> {
    if edges.len() > maximum
        || edges
            .iter()
            .any(|edge| edge.u >= edge.v || edge.v >= vertex_count)
        || edges.windows(2).any(|pair| pair[0] >= pair[1])
    {
        return Err(Error::InvalidInput(format!(
            "cohomology intervention {name} list is not canonical or exceeds its limit"
        )));
    }
    Ok(())
}