Files
wifi-densepose/vendor/ruvector/docs/research/sublinear-time-solver/ddd/integration-patterns.md

19 KiB
Raw Permalink Blame History

Sublinear-Time Solver: DDD Integration Patterns

Version: 1.0 Date: 2026-02-20 Status: Proposed


1. Anti-Corruption Layers

Anti-Corruption Layers (ACLs) translate between the Solver Core bounded context and each consuming bounded context, preventing domain model leakage.

1.1 Solver-to-Coherence ACL

Translates between Prime Radiant's sheaf graph types and the solver's sparse matrix types.

/// ACL: Coherence Engine ←→ Solver Core
pub struct CoherenceSolverAdapter {
    solver: Arc<dyn SparseLaplacianSolver>,
    cache: DashMap<u64, SolverResult>, // Keyed on graph version hash
}

impl CoherenceSolverAdapter {
    /// Convert SheafGraph to CsrMatrix for solver input
    pub fn sheaf_to_csr(graph: &SheafGraph) -> CsrMatrix<f32> {
        let n = graph.node_count();
        let mut row_ptrs = Vec::with_capacity(n + 1);
        let mut col_indices = Vec::new();
        let mut values = Vec::new();

        row_ptrs.push(0u32);
        for node_id in 0..n {
            let edges = graph.edges_from(node_id);
            let degree: f32 = edges.iter().map(|e| e.weight).sum();

            // Laplacian: L = D - A
            // Add diagonal (degree)
            col_indices.push(node_id as u32);
            values.push(degree);

            // Add off-diagonal (-weight)
            for edge in &edges {
                col_indices.push(edge.target as u32);
                values.push(-edge.weight);
            }
            row_ptrs.push(col_indices.len() as u32);
        }

        CsrMatrix { values: values.into(), col_indices: col_indices.into(), row_ptrs, rows: n, cols: n }
    }

    /// Convert solver result back to coherence energy
    pub fn solution_to_energy(
        solution: &SolverResult,
        graph: &SheafGraph,
    ) -> CoherenceEnergy {
        // Residual vector r = L*x represents per-edge contradiction
        let residual_norm = solution.convergence.final_residual;

        // Energy = sum of squared edge residuals
        let energy = residual_norm * residual_norm;

        // Per-node energy distribution
        let node_energies: Vec<f64> = solution.solution.iter()
            .map(|&x| (x as f64) * (x as f64))
            .collect();

        CoherenceEnergy {
            global_energy: energy,
            node_energies,
            solver_algorithm: solution.algorithm_used,
            solver_iterations: solution.iterations,
            accuracy_bound: solution.error_bounds.relative_error,
        }
    }

    /// Cached solve: reuse result if graph hasn't changed
    pub async fn solve_coherence(
        &self,
        graph: &SheafGraph,
        signal: &[f32],
    ) -> Result<CoherenceEnergy, SolverError> {
        let graph_hash = graph.content_hash();

        if let Some(cached) = self.cache.get(&graph_hash) {
            return Ok(Self::solution_to_energy(&cached, graph));
        }

        let csr = Self::sheaf_to_csr(graph);
        let system = SparseSystem::new(csr, signal.to_vec());
        let result = self.solver.solve(&system)?;

        self.cache.insert(graph_hash, result.clone());
        Ok(Self::solution_to_energy(&result, graph))
    }
}

1.2 Solver-to-GNN ACL

Translates between GNN message passing and sparse system solves.

/// ACL: GNN ←→ Solver Core
pub struct GnnSolverAdapter {
    solver: Arc<dyn SolverEngine>,
}

impl GnnSolverAdapter {
    /// Sublinear message aggregation using sparse solver
    /// Replaces: O(n × avg_degree) per layer
    /// With: O(nnz × log(1/ε)) per layer
    pub fn sublinear_aggregate(
        &self,
        adjacency: &CsrMatrix<f32>,
        features: &[Vec<f32>],
        epsilon: f64,
    ) -> Result<Vec<Vec<f32>>, SolverError> {
        let n = adjacency.rows;
        let feature_dim = features[0].len();
        let mut aggregated = vec![vec![0.0f32; feature_dim]; n];

        // Solve A·X_col = F_col for each feature dimension
        // Using batch solver amortization
        for d in 0..feature_dim {
            let rhs: Vec<f32> = features.iter().map(|f| f[d]).collect();
            let system = SparseSystem::new(adjacency.clone(), rhs);
            let result = self.solver.solve_with_budget(
                &system,
                ComputeBudget::for_lane(ComputeLane::Heavy),
            )?;

            for i in 0..n {
                aggregated[i][d] = result.solution[i];
            }
        }

        Ok(aggregated)
    }
}

/// GNN aggregation strategy using solver
pub struct SublinearAggregation {
    adapter: GnnSolverAdapter,
    epsilon: f64,
}

impl AggregationStrategy for SublinearAggregation {
    fn aggregate(
        &self,
        adjacency: &CsrMatrix<f32>,
        features: &[Vec<f32>],
    ) -> Vec<Vec<f32>> {
        self.adapter.sublinear_aggregate(adjacency, features, self.epsilon)
            .unwrap_or_else(|_| {
                // Fallback to mean aggregation
                MeanAggregation.aggregate(adjacency, features)
            })
    }
}

1.3 Solver-to-Graph ACL

Translates between ruvector-graph's property graph model and solver's sparse adjacency.

/// ACL: Graph Analytics ←→ Solver Core
pub struct GraphSolverAdapter {
    push_solver: Arc<dyn SublinearPageRank>,
}

impl GraphSolverAdapter {
    /// Convert PropertyGraph to SparseAdjacency for solver
    pub fn property_graph_to_adjacency(graph: &PropertyGraph) -> SparseAdjacency {
        let n = graph.node_count();
        let edges: Vec<(usize, usize, f32)> = graph.edges()
            .map(|e| (e.source, e.target, e.weight.unwrap_or(1.0)))
            .collect();

        SparseAdjacency {
            adj: CsrMatrix::from_edges(&edges, n),
            directed: graph.is_directed(),
            weighted: graph.is_weighted(),
        }
    }

    /// Solver-accelerated PageRank using Forward Push
    /// Replaces: O(n × m × iterations) power iteration
    /// With: O(1/ε) Forward Push
    pub fn fast_pagerank(
        &self,
        graph: &PropertyGraph,
        source: usize,
        alpha: f64,
        epsilon: f64,
    ) -> Result<Vec<(usize, f64)>, SolverError> {
        let adj = Self::property_graph_to_adjacency(graph);
        let problem = GraphProblem {
            id: ProblemId::new(),
            graph: adj,
            query: GraphQuery::SingleSource { source },
            parameters: PushParameters { alpha, epsilon, max_iterations: 1_000_000 },
        };

        let result = self.push_solver.solve(&problem)?;

        // Convert solver output to ranked node list
        let mut ranked: Vec<(usize, f64)> = result.solution.iter()
            .enumerate()
            .map(|(i, &score)| (i, score as f64))
            .filter(|(_, score)| *score > epsilon)
            .collect();
        ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
        Ok(ranked)
    }
}

1.4 Platform ACL (WASM / NAPI / REST / MCP)

Serialization boundary between domain types and platform representations.

/// WASM ACL
#[wasm_bindgen]
pub struct JsSolverConfig {
    inner: SolverConfig,
}

#[wasm_bindgen]
impl JsSolverConfig {
    #[wasm_bindgen(constructor)]
    pub fn new(js_config: JsValue) -> Result<JsSolverConfig, JsValue> {
        let config: SolverConfig = serde_wasm_bindgen::from_value(js_config)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        Ok(JsSolverConfig { inner: config })
    }
}

/// REST ACL
pub async fn solve_handler(
    State(state): State<AppState>,
    Json(request): Json<SolverRequest>,
) -> Result<Json<SolverResponse>, AppError> {
    // Translate REST types to domain types
    let system = SparseSystem::from_request(&request)?;
    let budget = ComputeBudget::from_request(&request);

    // Execute domain logic
    let result = state.orchestrator.solve(system).await?;

    // Translate domain result to REST response
    Ok(Json(SolverResponse::from_result(&result)))
}

/// MCP ACL
pub fn solver_tool_schema() -> McpTool {
    McpTool {
        name: "solve_sublinear".to_string(),
        description: "Solve sparse linear system using sublinear algorithms".to_string(),
        input_schema: json!({
            "type": "object",
            "required": ["matrix_rows", "matrix_cols", "values", "col_indices", "row_ptrs", "rhs"],
            "properties": {
                "matrix_rows": { "type": "integer", "minimum": 1 },
                "matrix_cols": { "type": "integer", "minimum": 1 },
                "values": { "type": "array", "items": { "type": "number" } },
                "col_indices": { "type": "array", "items": { "type": "integer" } },
                "row_ptrs": { "type": "array", "items": { "type": "integer" } },
                "rhs": { "type": "array", "items": { "type": "number" } },
                "tolerance": { "type": "number", "default": 1e-6 },
                "max_iterations": { "type": "integer", "default": 1000 },
                "algorithm": { "type": "string", "enum": ["auto", "neumann", "cg", "true"] },
            }
        }),
    }
}

2. Shared Kernel

Types shared between Solver Core and other bounded contexts.

2.1 Sparse Matrix Types

Shared between Solver Core and Min-Cut Context:

// crates/ruvector-solver/src/shared/sparse.rs
// Also used by ruvector-mincut

pub use crate::domain::values::CsrMatrix;
pub use crate::domain::values::SparsityProfile;

/// Conversion between CsrMatrix and CscMatrix (Compressed Sparse Column)
impl<T: Copy> CsrMatrix<T> {
    pub fn to_csc(&self) -> CscMatrix<T> { ... }
    pub fn transpose(&self) -> CsrMatrix<T> { ... }
}

2.2 Error Types

Shared across all solver-related contexts:

// crates/ruvector-solver/src/shared/errors.rs

#[derive(Debug, thiserror::Error)]
pub enum SolverError {
    #[error("solver did not converge: {iterations} iterations, best residual {best_residual}")]
    NonConvergence { iterations: usize, best_residual: f64, budget: ComputeBudget },

    #[error("numerical instability in {source}: {detail}")]
    NumericalInstability { source: &'static str, detail: String },

    #[error("compute budget exhausted: {progress:.1}% complete")]
    BudgetExhausted { budget: ComputeBudget, progress: f64 },

    #[error("invalid input: {0}")]
    InvalidInput(#[from] ValidationError),

    #[error("precision loss: expected ε={expected_eps}, achieved ε={achieved_eps}")]
    PrecisionLoss { expected_eps: f64, achieved_eps: f64 },

    #[error("all algorithms failed")]
    AllAlgorithmsFailed,

    #[error("backend error: {0}")]
    BackendError(#[from] Box<dyn std::error::Error + Send + Sync>),
}

2.3 Compute Budget

Shared between Solver and Coherence Gate's compute ladder:

// Used by both ruvector-solver and cognitum-gate-tilezero
pub use crate::domain::entities::ComputeBudget;
pub use crate::domain::entities::ComputeLane;

3. Published Language

3.1 Solver Protocol (JSON Schema)

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ruvector.io/schemas/solver/v1",
  "title": "RuVector Sublinear Solver Protocol v1",
  "definitions": {
    "SolverRequest": {
      "type": "object",
      "required": ["system"],
      "properties": {
        "system": { "$ref": "#/definitions/SparseSystem" },
        "config": { "$ref": "#/definitions/SolverConfig" },
        "budget": { "$ref": "#/definitions/ComputeBudget" }
      }
    },
    "SparseSystem": {
      "type": "object",
      "required": ["rows", "cols", "values", "col_indices", "row_ptrs", "rhs"],
      "properties": {
        "rows": { "type": "integer", "minimum": 1, "maximum": 10000000 },
        "cols": { "type": "integer", "minimum": 1, "maximum": 10000000 },
        "values": { "type": "array", "items": { "type": "number" } },
        "col_indices": { "type": "array", "items": { "type": "integer", "minimum": 0 } },
        "row_ptrs": { "type": "array", "items": { "type": "integer", "minimum": 0 } },
        "rhs": { "type": "array", "items": { "type": "number" } }
      }
    },
    "SolverResult": {
      "type": "object",
      "properties": {
        "solution": { "type": "array", "items": { "type": "number" } },
        "algorithm_used": { "type": "string" },
        "iterations": { "type": "integer" },
        "residual_norm": { "type": "number" },
        "wall_time_us": { "type": "integer" },
        "converged": { "type": "boolean" },
        "error_bounds": {
          "type": "object",
          "properties": {
            "absolute_error": { "type": "number" },
            "relative_error": { "type": "number" }
          }
        }
      }
    }
  }
}

4. Event-Driven Integration

4.1 Event Flow Architecture

    SolverOrchestrator
           │
    emits SolverEvent
           │
    ┌──────┴──────┐
    │ broadcast    │
    │ ::Sender     │
    └──────┬──────┘
           │
     ┌─────┼─────┬──────────┬──────────┐
     ▼     ▼     ▼          ▼          ▼
 Coherence Metrics  Stream   Audit   SONA
 Engine   Collector  API     Trail   Learning
     │       │       │        │        │
     ▼       ▼       ▼        ▼        ▼
 Update  Prometheus Server-  Witness  Update
 energy  counters   Sent     chain   routing
                    Events   entry   weights

4.2 Coherence Gate as Solver Governor

Solve Request
     │
     ▼
┌────────────────┐
│ Complexity Est.│  "How expensive will this be?"
└───────┬────────┘
        │
        ▼
┌────────────────┐
│ Gate Decision  │  Permit / Defer / Deny
└───┬────┬───┬───┘
    │    │   │
 Permit Defer Deny
    │    │   │
    ▼    ▼   ▼
Execute Wait Reject
 solver  for  with
         human witness
         approval

4.3 SONA Feedback Loop

[Solve Request] → [Route] → [Execute] → [Record Result]
                     ▲                         │
                     │                         ▼
               [Update Routing]     [SONA micro-LoRA update]
               [Weights]                       │
                     ▲                         │
                     └─── EWC-protected ──────┘
                          weight update

5. Dependency Injection

5.1 Generic Type Parameters

/// Solver generic over numeric backend
pub struct SublinearSolver<B: NumericBackend = NalgebraBackend> {
    backend: B,
    config: SolverConfig,
}

impl<B: NumericBackend> SolverEngine for SublinearSolver<B> {
    type Input = SparseSystem;
    type Output = SolverResult;
    type Error = SolverError;

    fn solve(&self, input: &Self::Input) -> Result<Self::Output, Self::Error> {
        // Implementation using self.backend for matrix operations
        todo!()
    }
}

5.2 Runtime DI via Arc

/// Application state with DI
pub struct AppState {
    pub solver: Arc<dyn SolverEngine<Input = SparseSystem, Output = SolverResult, Error = SolverError>>,
    pub router: Arc<AlgorithmRouter>,
    pub session_repo: Arc<dyn SolverSessionRepository>,
    pub event_bus: broadcast::Sender<SolverEvent>,
}

6. Integration with Existing Patterns

6.1 Core-Binding-Surface Compliance

ruvector-solver           → Core (pure Rust algorithms)
ruvector-solver-wasm      → Binding (wasm-bindgen)
ruvector-solver-node      → Binding (NAPI-RS)
@ruvector/solver (npm)    → Surface (TypeScript API)

6.2 Event Sourcing Alignment

SolverEvent matches Prime Radiant's DomainEvent contract:

  • #[serde(tag = "type")] — Discriminated union in JSON
  • Deterministic replay via event log
  • Content-addressable via SHAKE-256 hash
  • Tamper-detectable in witness chain

6.3 Compute Ladder Integration

Solver maps to cognitum-gate-tilezero compute lanes:

Lane Solver Use Case Budget
Reflex Cached result lookup <1ms, 1MB
Retrieval Small solve (n<1K) or Push query ~10ms, 16MB
Heavy Full CG/Neumann/BMSSP solve ~100ms, 256MB
Deliberate TRUE with preprocessing, streaming Unbounded

7. Migration Patterns

7.1 Strangler Fig for Coherence Engine

Gradual replacement of dense Laplacian computation:

impl CoherenceComputer {
    pub fn compute_energy(&self, graph: &SheafGraph) -> CoherenceEnergy {
        let density = graph.edge_density();

        #[cfg(feature = "sublinear-coherence")]
        if density < 0.05 {
            // New: Sublinear path for sparse graphs
            if let Ok(energy) = self.solver_adapter.solve_coherence(graph, &signal) {
                return energy;
            }
            // Fallthrough to dense on solver failure
        }

        // Existing: Dense path (unchanged)
        self.dense_laplacian_energy(graph)
    }
}

Phase 1: Feature flag (opt-in, default off) Phase 2: Default on for sparse graphs (density < 5%) Phase 3: Default on for all graphs after benchmark validation Phase 4: Remove dense path (breaking change in major version)

7.2 Branch by Abstraction for GNN

pub enum AggregationStrategy {
    Mean,
    Max,
    Sum,
    Attention,
    #[cfg(feature = "sublinear-gnn")]
    Sublinear { epsilon: f64 },
}

impl GnnLayer {
    pub fn aggregate(&self, adj: &CsrMatrix<f32>, features: &[Vec<f32>]) -> Vec<Vec<f32>> {
        match self.strategy {
            AggregationStrategy::Mean => mean_aggregate(adj, features),
            AggregationStrategy::Max => max_aggregate(adj, features),
            AggregationStrategy::Sum => sum_aggregate(adj, features),
            AggregationStrategy::Attention => attention_aggregate(adj, features),
            #[cfg(feature = "sublinear-gnn")]
            AggregationStrategy::Sublinear { epsilon } => {
                SublinearAggregation::new(epsilon).aggregate(adj, features)
            }
        }
    }
}

8. Cross-Cutting Concerns

8.1 Observability

use tracing::{instrument, info, warn};

impl SolverOrchestrator {
    #[instrument(skip(self, system), fields(n = system.matrix.rows, nnz = system.matrix.nnz()))]
    pub async fn solve(&self, system: SparseSystem) -> Result<SolverResult, SolverError> {
        let algorithm = self.router.select(&system.profile());
        info!(algorithm = ?algorithm, "routing decision");

        let start = Instant::now();
        let result = self.execute(algorithm, &system).await;
        let elapsed = start.elapsed();

        match &result {
            Ok(r) => info!(iterations = r.iterations, residual = r.residual_norm, elapsed_us = elapsed.as_micros() as u64, "solve completed"),
            Err(e) => warn!(error = %e, "solve failed"),
        }

        result
    }
}

8.2 Caching

pub struct SolverCache {
    results: DashMap<u64, (SolverResult, Instant)>,
    ttl: Duration,
    max_entries: usize,
}

impl SolverCache {
    pub fn get_or_compute(
        &self,
        key: u64,
        compute: impl FnOnce() -> Result<SolverResult, SolverError>,
    ) -> Result<SolverResult, SolverError> {
        if let Some(entry) = self.results.get(&key) {
            if entry.1.elapsed() < self.ttl {
                return Ok(entry.0.clone());
            }
        }

        let result = compute()?;
        self.results.insert(key, (result.clone(), Instant::now()));

        // Evict if over capacity
        if self.results.len() > self.max_entries {
            self.evict_oldest();
        }

        Ok(result)
    }
}