use std::{
hash::{Hash, Hasher},
mem::size_of,
time::Instant,
};
use crate::CompileResult;
use laddu_expr::{
BinaryOp, Expr, ExprGraph, ExprId, ExprNode, ParameterStructuralKey, UnaryOp, ValueKind,
parameters::{ParamLayout, ParamRegistry, ParameterUpdate},
vector,
};
use serde::{Deserialize, Serialize};
#[cfg(test)]
use crate::facts::NumberClass;
use crate::{
NormalizationDiagnostics, NormalizationPlan,
cas::{Cas, ExtractionDiagnostics, OptimizationBudget, OptimizationDiagnostics},
cost::OptimizationCost,
facts::{DependencyFacts, EvaluationClass, GraphFacts, NodeFacts},
graph_utils::mark_reachable,
};
#[derive(Copy, Clone, Debug)]
pub struct CompileOptions {
cache_policy: CachePolicy,
optimize: bool,
optimization_budget: OptimizationBudget,
}
impl Default for CompileOptions {
fn default() -> Self {
Self {
cache_policy: CachePolicy::default(),
optimize: true,
optimization_budget: OptimizationBudget::default(),
}
}
}
impl CompileOptions {
pub fn new() -> Self {
Self::default()
}
pub fn without_optimizations() -> Self {
Self {
cache_policy: CachePolicy::default(),
optimize: false,
optimization_budget: OptimizationBudget::default(),
}
}
pub fn cache_policy(&self) -> CachePolicy {
self.cache_policy
}
pub fn set_cache_policy(&mut self, cache_policy: CachePolicy) {
self.cache_policy = cache_policy;
}
pub fn with_cache_policy(mut self, cache_policy: CachePolicy) -> Self {
self.set_cache_policy(cache_policy);
self
}
pub fn with_optimization_budget(mut self, budget: crate::OptimizationBudget) -> Self {
self.optimization_budget = budget;
self
}
}
struct Compiler {
source_graph: ExprGraph,
params: ParamLayout,
options: CompileOptions,
analyze_normalization: bool,
}
impl Compiler {
fn new(
source_graph: ExprGraph,
options: &CompileOptions,
analyze_normalization: bool,
) -> CompileResult<Self> {
let params = collect_params(&source_graph)?;
Ok(Self {
source_graph,
params,
options: *options,
analyze_normalization,
})
}
fn compile(self) -> CompileResult<CompiledModel> {
let Self {
source_graph,
params,
options,
analyze_normalization,
} = self;
let parameter_baked = Self::bake_parameters(&source_graph);
if options.optimize {
let search = Cas::import(parameter_baked, options.optimization_budget).search();
let solver_started = Instant::now();
let (execution_graph, execution_extraction) = search.extract_execution()?;
let (normalization_plan, normalization_extraction) = if analyze_normalization {
let remaining = (options.optimization_budget.solver_seconds
- solver_started.elapsed().as_secs_f64())
.max(0.0);
let (normalization_graph, extraction) =
search.extract_normalization_with_solver_seconds(remaining)?;
let normalization_facts = GraphFacts::analyze(&normalization_graph);
(
NormalizationPlan::analyze(&normalization_graph, &normalization_facts),
extraction,
)
} else {
(
NormalizationPlan::analyze_disabled(&execution_graph),
ExtractionDiagnostics {
exact: false,
reason: Some("normalization analysis disabled"),
},
)
};
let optimization_diagnostics =
Some(search.diagnostics(&execution_extraction, &normalization_extraction));
let facts = GraphFacts::analyze(&execution_graph);
let cache_plan = CachePlan::new(&execution_graph, &facts, options.cache_policy);
return Ok(CompiledModel {
source_graph,
graph: execution_graph,
params,
facts,
cache_plan,
normalization_plan,
optimization_diagnostics,
});
}
let execution_graph = parameter_baked;
let facts = GraphFacts::analyze(&execution_graph);
let cache_plan = CachePlan::new(&execution_graph, &facts, options.cache_policy);
let normalization_plan = if analyze_normalization {
NormalizationPlan::analyze(&execution_graph, &facts)
} else {
NormalizationPlan::analyze_disabled(&execution_graph)
};
Ok(CompiledModel {
source_graph,
graph: execution_graph,
params,
facts,
cache_plan,
normalization_plan,
optimization_diagnostics: None,
})
}
fn bake_parameters(source: &ExprGraph) -> ExprGraph {
bake_fixed_parameters(source)
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum CachePolicy {
Off,
#[default]
EventDependent,
}
impl CachePolicy {
fn accepts(self, facts: NodeFacts) -> bool {
match self {
Self::Off => false,
Self::EventDependent => {
facts.dependency.depends_on_event
&& !facts.dependency.depends_on_free_params
&& !facts.dependency.depends_on_fixed_params
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CachePlan {
entries: Vec<CacheEntry>,
materialization_nodes: Vec<ExprId>,
}
impl CachePlan {
pub(crate) fn new(graph: &ExprGraph, facts: &GraphFacts, policy: CachePolicy) -> Self {
if policy == CachePolicy::Off {
return Self::default();
}
let cacheable = graph
.nodes()
.iter()
.enumerate()
.map(|(index, _)| {
let id = ExprId::from_index(index);
let facts = *facts.get(id).expect("facts are complete for graph");
policy.accepts(facts)
})
.collect::<Vec<_>>();
let mut frontier = vec![false; graph.nodes().len()];
if cacheable[graph.root().index()] {
frontier[graph.root().index()] = true;
}
for (index, node) in graph.nodes().iter().enumerate() {
if cacheable[index] {
continue;
}
for child in node.children() {
if cacheable[child.index()] {
frontier[child.index()] = true;
}
}
}
let entries = cacheable
.into_iter()
.zip(frontier)
.enumerate()
.filter(|&(_index, (cacheable, frontier))| cacheable && frontier)
.map(|(index, (_cacheable, _frontier))| {
let id = ExprId::from_index(index);
let facts = *facts.get(id).expect("facts are complete for graph");
CacheEntry {
node: id,
value_kind: facts.value_kind,
evaluation_class: facts.evaluation_class(),
dependency: facts.dependency,
}
})
.collect::<Vec<_>>();
let mut required = vec![false; graph.nodes().len()];
mark_reachable(graph, entries.iter().map(|entry| entry.node), &mut required);
let materialization_nodes = required
.into_iter()
.enumerate()
.filter(|&(_index, required)| required)
.map(|(index, _required)| ExprId::from_index(index))
.collect();
Self {
entries,
materialization_nodes,
}
}
pub fn entries(&self) -> &[CacheEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn node_slot(&self, node: ExprId) -> Option<usize> {
self.entries.iter().position(|entry| entry.node == node)
}
pub fn materialization_nodes(&self) -> &[ExprId] {
&self.materialization_nodes
}
pub fn bytes_per_event(&self) -> usize {
self.entries
.iter()
.map(|entry| entry.storage_kind().bytes_per_event())
.sum()
}
pub fn layout(&self) -> CacheLayout {
let mut offsets = Vec::with_capacity(self.entries.len());
let mut width: usize = 0;
for entry in &self.entries {
offsets.push(width);
width = width
.checked_add(entry.storage_kind().width())
.expect("cache layout width exceeds addressable memory");
}
CacheLayout { offsets, width }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CacheLayout {
offsets: Vec<usize>,
width: usize,
}
impl CacheLayout {
pub fn offsets(&self) -> &[usize] {
&self.offsets
}
pub fn width(&self) -> usize {
self.width
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CacheStorageKind {
Real,
Complex {
width: usize,
},
}
impl CacheStorageKind {
pub fn width(self) -> usize {
match self {
Self::Real => 1,
Self::Complex { width } => width,
}
}
pub fn bytes_per_event(self) -> usize {
match self {
Self::Real => size_of::<f64>(),
Self::Complex { width } => width * size_of::<num::complex::Complex64>(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CacheEntry {
node: ExprId,
value_kind: ValueKind,
evaluation_class: EvaluationClass,
dependency: DependencyFacts,
}
impl CacheEntry {
pub fn node(&self) -> ExprId {
self.node
}
pub fn value_kind(&self) -> ValueKind {
self.value_kind
}
pub fn evaluation_class(&self) -> EvaluationClass {
self.evaluation_class
}
pub fn dependency(&self) -> DependencyFacts {
self.dependency
}
pub fn storage_kind(&self) -> CacheStorageKind {
match self.value_kind {
ValueKind::Real => CacheStorageKind::Real,
ValueKind::Complex => CacheStorageKind::Complex { width: 1 },
ValueKind::Vector { len } => CacheStorageKind::Complex { width: len },
ValueKind::Matrix { rows, cols } => CacheStorageKind::Complex { width: rows * cols },
}
}
}
#[derive(Clone, Debug)]
pub struct CompiledModel {
source_graph: ExprGraph,
graph: ExprGraph,
params: ParamLayout,
facts: GraphFacts,
cache_plan: CachePlan,
normalization_plan: NormalizationPlan,
optimization_diagnostics: Option<OptimizationDiagnostics>,
}
#[derive(Clone, Debug)]
pub struct CompiledQuery {
model: CompiledModel,
outputs: Vec<ExprId>,
}
impl CompiledQuery {
pub fn from_exprs<I>(exprs: I) -> CompileResult<Self>
where
I: IntoIterator<Item = Expr>,
{
let expressions = exprs.into_iter().collect();
Self::compile_expressions(expressions, &CompileOptions::default())
}
pub fn from_exprs_with_options<I>(exprs: I, options: &CompileOptions) -> CompileResult<Self>
where
I: IntoIterator<Item = Expr>,
{
let expressions = exprs.into_iter().collect();
Self::compile_expressions(expressions, options)
}
fn compile_expressions(
expressions: Vec<Expr>,
options: &CompileOptions,
) -> CompileResult<Self> {
if expressions.is_empty() {
return Err(crate::CompileError::Unsupported(
"multi-output query requires at least one expression",
));
}
let model = CompiledModel::from_expr_with_options(&vector(expressions), options)?;
let outputs = match model.graph().node(model.graph().root()) {
Some(ExprNode::Vector { elements }) => elements.clone(),
_ => {
return Err(crate::CompileError::InvalidExecutablePlan(
"compiled query root is not a vector".into(),
));
}
};
Ok(Self { model, outputs })
}
pub fn model(&self) -> &CompiledModel {
&self.model
}
pub fn outputs(&self) -> &[ExprId] {
&self.outputs
}
}
impl Serialize for CompiledModel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.source_graph.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for CompiledModel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Self::from_graph(ExprGraph::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
impl CompiledModel {
#[doc(hidden)]
pub fn optimized_digest(&self) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.graph.root().hash(&mut hasher);
self.graph.nodes().len().hash(&mut hasher);
for node in self.graph.nodes() {
node.structural_key().hash(&mut hasher);
}
for parameter in self.params.specs() {
ParameterStructuralKey::from(parameter).hash(&mut hasher);
}
self.normalization_plan.hash_structure(&mut hasher);
hasher.finish()
}
pub fn project_tags<'a>(&self, tags: impl IntoIterator<Item = &'a str>) -> CompileResult<Self> {
self.project_tags_with_options(tags, &CompileOptions::default())
}
pub fn project_tags_with_options<'a>(
&self,
tags: impl IntoIterator<Item = &'a str>,
options: &CompileOptions,
) -> CompileResult<Self> {
Self::from_graph_with_options(self.source_graph.project_tags(tags), options)
}
pub fn has_tag(&self, tag: &str) -> bool {
(0..self.source_graph.nodes().len()).any(|index| {
self.source_graph
.metadata(ExprId::from_index(index))
.is_some_and(|metadata| metadata.has_tag(tag))
})
}
pub fn from_expr(expr: &Expr) -> CompileResult<Self> {
Self::from_expr_with_options(expr, &CompileOptions::default())
}
pub fn from_expr_with_options(expr: &Expr, options: &CompileOptions) -> CompileResult<Self> {
Self::from_graph_with_options(expr.to_graph(), options)
}
pub fn from_graph(graph: ExprGraph) -> CompileResult<Self> {
Self::from_graph_with_options(graph, &CompileOptions::default())
}
pub fn from_graph_with_options(
graph: ExprGraph,
options: &CompileOptions,
) -> CompileResult<Self> {
Compiler::new(graph, options, true)?.compile()
}
pub(crate) fn from_graph_without_normalization(graph: ExprGraph) -> CompileResult<Self> {
Compiler::new(graph, &CompileOptions::default(), false)?.compile()
}
pub fn graph(&self) -> &ExprGraph {
&self.graph
}
pub fn optimization_diagnostics(&self) -> Option<&OptimizationDiagnostics> {
self.optimization_diagnostics.as_ref()
}
pub fn parameter_polynomial_degree(&self) -> Option<usize> {
let mut degrees: Vec<Option<usize>> = Vec::with_capacity(self.graph.nodes().len());
for node in self.graph.nodes() {
let child = |id: ExprId| degrees.get(id.index()).copied().flatten();
let degree = match node {
ExprNode::RealConst(_)
| ExprNode::ComplexConst(_)
| ExprNode::EventScalar(_)
| ExprNode::EventP4Component { .. } => Some(0),
ExprNode::ScalarParam(_) => Some(1),
ExprNode::Unary { op, input } => {
let input = child(*input)?;
match op {
UnaryOp::Neg | UnaryOp::Real | UnaryOp::Imag | UnaryOp::Conj => Some(input),
UnaryOp::NormSqr => input.checked_mul(2),
UnaryOp::PowI(power) if *power >= 0 => input.checked_mul(*power as usize),
UnaryOp::Sqrt
| UnaryOp::Exp
| UnaryOp::Sin
| UnaryOp::Cos
| UnaryOp::Log
| UnaryOp::PowI(_) => (input == 0).then_some(0),
}
}
ExprNode::Binary { op, lhs, rhs } => {
let lhs = child(*lhs)?;
let rhs = child(*rhs)?;
match op {
BinaryOp::Add | BinaryOp::Sub => Some(lhs.max(rhs)),
BinaryOp::Mul => lhs.checked_add(rhs),
BinaryOp::Div if rhs == 0 => Some(lhs),
BinaryOp::Atan2 if lhs == 0 && rhs == 0 => Some(0),
BinaryOp::Div | BinaryOp::Atan2 => None,
}
}
ExprNode::NaryAdd { terms } => terms
.iter()
.map(|id| child(*id))
.collect::<Option<Vec<_>>>()?
.into_iter()
.max(),
ExprNode::NaryMul { factors } => factors
.iter()
.try_fold(0usize, |degree, id| degree.checked_add(child(*id)?)),
ExprNode::Complex { re, im } => Some(child(*re)?.max(child(*im)?)),
ExprNode::Vector { elements } | ExprNode::Matrix { elements, .. } => elements
.iter()
.map(|id| child(*id))
.collect::<Option<Vec<_>>>()?
.into_iter()
.max(),
ExprNode::Component { input, .. } | ExprNode::MatrixElement { input, .. } => {
child(*input)
}
ExprNode::MatMul { lhs, rhs } | ExprNode::Dot { lhs, rhs } => {
child(*lhs)?.checked_add(child(*rhs)?)
}
ExprNode::MatVec { matrix, vector } => child(*matrix)?.checked_add(child(*vector)?),
ExprNode::Solve { matrix, rhs } if child(*matrix)? == 0 => child(*rhs),
ExprNode::Solve { .. } => None,
};
degrees.push(degree);
}
degrees.get(self.graph.root().index()).copied().flatten()
}
pub fn display_tree(&self) -> laddu_expr::ExprGraphTreeDisplay<'_> {
self.graph.display_tree()
}
pub fn display_dot(&self) -> laddu_expr::ExprGraphDotDisplay<'_> {
self.graph.display_dot()
}
pub fn with_parameters<N, I>(&self, updates: I) -> CompileResult<Self>
where
N: AsRef<str>,
I: IntoIterator<Item = (N, ParameterUpdate)>,
{
let source_graph = self.source_graph.with_parameters(updates)?;
Self::from_graph_with_options(source_graph, &CompileOptions::default())
}
pub fn with_parameters_with_options<N, I>(
&self,
updates: I,
options: &CompileOptions,
) -> CompileResult<Self>
where
N: AsRef<str>,
I: IntoIterator<Item = (N, ParameterUpdate)>,
{
let source_graph = self.source_graph.with_parameters(updates)?;
Self::from_graph_with_options(source_graph, options)
}
pub fn params(&self) -> &ParamLayout {
&self.params
}
pub fn facts(&self) -> &GraphFacts {
&self.facts
}
pub fn cache_plan(&self) -> &CachePlan {
&self.cache_plan
}
pub fn normalization_diagnostics(&self) -> &NormalizationDiagnostics {
self.normalization_plan.diagnostics()
}
#[doc(hidden)]
pub fn normalization_plan(&self) -> &NormalizationPlan {
&self.normalization_plan
}
pub fn cost(&self) -> OptimizationCost {
OptimizationCost::analyze(&self.graph)
}
pub fn node_facts(&self, id: ExprId) -> Option<&NodeFacts> {
self.facts.get(id)
}
}
fn bake_fixed_parameters(graph: &ExprGraph) -> ExprGraph {
let nodes = graph
.nodes()
.iter()
.map(|node| match node {
ExprNode::ScalarParam(parameter) => match parameter.state() {
laddu_expr::parameters::ParamState::Fixed(value) => ExprNode::RealConst(*value),
laddu_expr::parameters::ParamState::Free => node.clone(),
},
_ => node.clone(),
})
.collect();
let metadata = (0..graph.nodes().len())
.map(|index| {
graph
.metadata(ExprId::from_index(index))
.expect("graph metadata is complete")
.clone()
})
.collect();
ExprGraph::from_parts(graph.root(), nodes, metadata).expect("source graph is valid")
}
pub fn collect_params(graph: &ExprGraph) -> CompileResult<ParamLayout> {
let mut registry = ParamRegistry::new();
for node in graph.nodes() {
if let ExprNode::ScalarParam(spec) = node {
registry.register(spec.clone())?;
}
}
Ok(registry.layout()?)
}
#[cfg(test)]
#[path = "model/cache_tests.rs"]
mod cache_tests;
#[cfg(test)]
#[path = "model/tests.rs"]
mod tests;