hanzo-hmm: HMM compute-job pricing (compute_price) — the canonical pricing (#1)
Price a heterogeneous, SLA-bound compute job via the Hamiltonian Market Maker, NOT a constant-product (x*y=k) AMM. Compute is perishable/heterogeneous/SLA- bound; an AMM prices a fungible storable pair and cannot express scarcity convexity, deadline pressure, or per-resource isolation. compute_price::price(job, market) -> HanzoPrice (wei, 18 decimals): - maps market imbalance (demand vs supply) into the Hamiltonian phase space (position = tanh(imbalance) shadow-price displacement; momentum = elasticity * displacement) under an ANHARMONIC potential (quartic term => super-linear scarcity bite that a quadratic / x*y=k cannot express); - evolves the existing symplectic leapfrog integrator with friction to the energy-modulated equilibrium and reads HamiltonianDynamics::calculate_price (price modulated by total energy E = T + V; scarcity raises E raises price); - modulates by SLA tightness (tighter deadline => higher price, perishability) and quality/privacy tier, then clamps to [1e13, 1e16] wei (AI_TOKEN_ECONOMICS min/max). Deterministic (noise-free path, no rng) => reproducible + testable. Pure ADAPTER over crate::hamiltonian — re-implements no mechanics (DRY). The broker calls this to set the on-chain escrow amount; ComputeSettlement then settles that amount against a canonical PoAI proof (LP-302). Tests (8 named properties + tier + bad-input): monotone in scarcity, monotone in demand, SLA perishability, bounded, heterogeneity (resources price independently), determinism, tier ordering, explicit input rejection. Found + fixed a real bug: raw-imbalance seeding diverged the stiff quartic to NaN at extreme scarcity; normalized tanh seeding keeps the integrator in its stable basin while a strictly-increasing scarcity factor preserves monotonicity. cargo test -p hanzo-hmm: 25/25 (8 new + 17 pre-existing, no regression). hamiltonian.rs: add set_phase_space(PhaseSpace) (dimension-checked) so (q,p) can be seeded deterministically without the stochastic perturb path. No behavior change to existing API; all prior tests still pass. Refs LP-302 (settlement), zip-0418 / AI_TOKEN_ECONOMICS (HMM). Co-authored-by: zeekay <z@zeekay.io>
This commit is contained in:
@@ -0,0 +1,584 @@
|
||||
//! Compute-job pricing adapter.
|
||||
//!
|
||||
//! Turns a heterogeneous, SLA-bound compute job into a HANZO price (wei, 18
|
||||
//! decimals) via the Hamiltonian Market Maker — NOT a constant-product
|
||||
//! (`x*y=k`) AMM. Compute is perishable, heterogeneous, and SLA-bound; an AMM
|
||||
//! prices a fungible, storable pair and cannot express scarcity convexity,
|
||||
//! deadline pressure, or per-resource isolation. This module is the
|
||||
//! compute-domain ADAPTER over [`crate::HamiltonianDynamics`]: it maps
|
||||
//! `(demand q, shadow price p)` into the Hamiltonian phase space, evolves the
|
||||
//! energy-conserving (symplectic) dynamics to the energy-modulated equilibrium
|
||||
//! price via [`HamiltonianDynamics::calculate_price`], then modulates by SLA
|
||||
//! tightness (perishability) and quality tier, and clamps to the bounds from
|
||||
//! `docs/AI_TOKEN_ECONOMICS.md`.
|
||||
//!
|
||||
//! All Hamiltonian mechanics live in [`crate::hamiltonian`]; nothing here
|
||||
//! re-implements them. The pricing entry point [`price`] is fully
|
||||
//! deterministic for a given `(job, market)` — it uses only the noise-free
|
||||
//! integrator path (no `rng`), so the HMM equilibrium is reproducible and
|
||||
//! testable.
|
||||
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::hamiltonian::{AnharmonicPotential, HamiltonianDynamics};
|
||||
|
||||
/// One HANZO in wei (18 decimals), matching `hanzo-mining` token semantics.
|
||||
pub const WEI_PER_HANZO: u128 = 1_000_000_000_000_000_000;
|
||||
|
||||
/// Price floor in wei. Mirrors `AI_TOKEN_ECONOMICS.md` `min_price = 0.00001`
|
||||
/// HANZO/unit — a non-zero floor so a quote is never free.
|
||||
pub const MIN_PRICE_WEI: u128 = WEI_PER_HANZO / 100_000; // 1e13 wei
|
||||
|
||||
/// Price ceiling in wei. Mirrors `AI_TOKEN_ECONOMICS.md` `max_price = 0.01`
|
||||
/// HANZO/unit — an unconditional cap so a quote is never unbounded.
|
||||
pub const MAX_PRICE_WEI: u128 = WEI_PER_HANZO / 100; // 1e16 wei
|
||||
|
||||
/// Price elasticity (`AI_TOKEN_ECONOMICS.md` §"Dynamic Pricing Updates").
|
||||
const ELASTICITY: f64 = 0.5;
|
||||
|
||||
/// Heterogeneous compute resource kinds. Each kind prices on its own
|
||||
/// [`MarketState`]; prices never cross between kinds (property: heterogeneity).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ResourceKind {
|
||||
/// CPU cores / vCPU-seconds.
|
||||
Cpu,
|
||||
/// GPU accelerators / GPU-seconds.
|
||||
Gpu,
|
||||
/// RAM (GB).
|
||||
Memory,
|
||||
/// Persistent storage (GB).
|
||||
Storage,
|
||||
/// Network egress (GB).
|
||||
Bandwidth,
|
||||
}
|
||||
|
||||
/// Quality / privacy tier. Tighter isolation earns a higher multiplier,
|
||||
/// matching the privacy-tier table in `AI_TOKEN_ECONOMICS.md`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
|
||||
pub enum QualityTier {
|
||||
/// Open, no attestation. 1.0x.
|
||||
#[default]
|
||||
Open,
|
||||
/// Encrypted at rest. 1.2x.
|
||||
AtRest,
|
||||
/// CPU TEE (SGX/SEV). 1.5x.
|
||||
CpuTee,
|
||||
/// GPU TEE (H100 CC). 1.8x.
|
||||
GpuTee,
|
||||
/// TEE-I/O (Blackwell). 2.0x.
|
||||
TeeIo,
|
||||
}
|
||||
|
||||
impl QualityTier {
|
||||
/// Tier multiplier from the economics table.
|
||||
fn multiplier(self) -> f64 {
|
||||
match self {
|
||||
QualityTier::Open => 1.0,
|
||||
QualityTier::AtRest => 1.2,
|
||||
QualityTier::CpuTee => 1.5,
|
||||
QualityTier::GpuTee => 1.8,
|
||||
QualityTier::TeeIo => 2.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A heterogeneous, SLA-bound compute job to be priced.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ComputeJob {
|
||||
/// Which resource pool this job draws from.
|
||||
pub kind: ResourceKind,
|
||||
/// Units demanded by this job (cores, GPU-seconds, GB, …). Must be > 0.
|
||||
pub units: f64,
|
||||
/// Seconds until the job's deadline / latency SLA. A tighter (smaller)
|
||||
/// deadline means the work is more perishable and prices higher. `None`
|
||||
/// means best-effort (no urgency premium).
|
||||
pub deadline_secs: Option<f64>,
|
||||
/// Quality / privacy tier required.
|
||||
pub tier: QualityTier,
|
||||
}
|
||||
|
||||
impl ComputeJob {
|
||||
/// Best-effort, open-tier job for `units` of `kind`.
|
||||
pub fn new(kind: ResourceKind, units: f64) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
units,
|
||||
deadline_secs: None,
|
||||
tier: QualityTier::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a deadline/latency SLA in seconds.
|
||||
pub fn with_deadline_secs(mut self, deadline_secs: f64) -> Self {
|
||||
self.deadline_secs = Some(deadline_secs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach a quality/privacy tier.
|
||||
pub fn with_tier(mut self, tier: QualityTier) -> Self {
|
||||
self.tier = tier;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Market state for a single resource kind at quote time.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MarketState {
|
||||
/// Which resource pool this state describes.
|
||||
pub kind: ResourceKind,
|
||||
/// Available units right now (supply). Must be > 0 to price.
|
||||
pub supply: f64,
|
||||
/// Outstanding demand / queue depth right now.
|
||||
pub demand: f64,
|
||||
/// Base price per unit, in wei. The HMM modulates around this anchor.
|
||||
pub base_price_wei: u128,
|
||||
}
|
||||
|
||||
impl MarketState {
|
||||
/// New market state.
|
||||
pub fn new(kind: ResourceKind, supply: f64, demand: f64, base_price_wei: u128) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
supply,
|
||||
demand,
|
||||
base_price_wei,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A HANZO price in wei (18 decimals). Always in `[MIN_PRICE_WEI,
|
||||
/// MAX_PRICE_WEI]`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct HanzoPrice(pub u128);
|
||||
|
||||
impl HanzoPrice {
|
||||
/// Price in wei.
|
||||
pub fn wei(self) -> u128 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Price in whole HANZO (lossy display only).
|
||||
pub fn as_hanzo(self) -> f64 {
|
||||
self.0 as f64 / WEI_PER_HANZO as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker substring in the `anyhow` error a kind mismatch produces, so callers
|
||||
/// (and tests) can distinguish it from numeric-input rejection without a bespoke
|
||||
/// error enum — the crate's error idiom is `anyhow`.
|
||||
pub const ERR_KIND_MISMATCH: &str = "resource kind mismatch";
|
||||
|
||||
// --- HMM integration constants (deterministic path) ---------------------------
|
||||
|
||||
/// Phase-space dimension: pricing is a 1-D conservative system (one resource,
|
||||
/// one shadow price), so a single generalized coordinate suffices.
|
||||
const PHASE_DIM: usize = 1;
|
||||
/// Hamiltonian energy scale; matches the crate default ([`crate::Config`]).
|
||||
const ENERGY_SCALE: f64 = 1.0;
|
||||
/// Quadratic stiffness of the shadow-price potential well.
|
||||
const POTENTIAL_K2: f64 = 1.0;
|
||||
/// Quartic stiffness — the anharmonic term makes the restoring force
|
||||
/// super-linear in displacement, so deep scarcity bites convexly. A pure
|
||||
/// quadratic (or `x*y=k`) cannot express this perishability convexity.
|
||||
const POTENTIAL_K4: f64 = 0.5;
|
||||
/// Symplectic integration step.
|
||||
const DT: f64 = 0.05;
|
||||
/// Number of leapfrog steps to settle toward equilibrium. Friction
|
||||
/// (set inside [`HamiltonianDynamics`]) dissipates transient momentum so the
|
||||
/// readout reflects the energy-modulated equilibrium, not the initial kick.
|
||||
const SETTLE_STEPS: usize = 200;
|
||||
|
||||
/// Price a heterogeneous, SLA-bound compute job via the Hamiltonian Market
|
||||
/// Maker. Deterministic for a given `(job, market)`.
|
||||
///
|
||||
/// HMM mapping (NOT `x*y=k`):
|
||||
/// 1. `imbalance = (demand - supply) / supply` — the economics-doc demand
|
||||
/// pressure; it seeds the generalized coordinate `q` and momentum `p`.
|
||||
/// 2. The Hamiltonian `H(q,p) = p²/2 + V(q)` with anharmonic `V` is evolved by
|
||||
/// the existing symplectic integrator until friction settles it.
|
||||
/// 3. [`HamiltonianDynamics::calculate_price`] reads the energy-modulated
|
||||
/// equilibrium: `base · (1+a‖q‖)(1+b⟨p⟩)(1+c·tanh(E/scale))`, where
|
||||
/// `E = T + V` is the total energy — scarcity raises `E`, raising price.
|
||||
/// 4. SLA tightness (perishability) and tier multipliers scale the result,
|
||||
/// which is then clamped to `[MIN_PRICE_WEI, MAX_PRICE_WEI]`.
|
||||
pub fn price(job: &ComputeJob, market: &MarketState) -> Result<HanzoPrice> {
|
||||
ensure!(
|
||||
job.kind == market.kind,
|
||||
"{ERR_KIND_MISMATCH}: job is {:?} but market is {:?}",
|
||||
job.kind,
|
||||
market.kind
|
||||
);
|
||||
validate_finite_positive("job.units", job.units)?;
|
||||
validate_finite_positive("market.supply", market.supply)?;
|
||||
validate_finite_nonneg("market.demand", market.demand)?;
|
||||
if let Some(d) = job.deadline_secs {
|
||||
validate_finite_positive("job.deadline_secs", d)?;
|
||||
}
|
||||
if market.base_price_wei == 0 {
|
||||
bail!("market.base_price_wei must be > 0");
|
||||
}
|
||||
|
||||
// (1) Effective demand for this quote includes the job's own draw on the
|
||||
// pool, so a larger job sees a deeper imbalance (its marginal scarcity).
|
||||
let effective_demand = market.demand + job.units;
|
||||
let imbalance = (effective_demand - market.supply) / market.supply;
|
||||
|
||||
// (2) Seed the HMM phase space. The generalized coordinate is the *shadow-
|
||||
// price displacement*, a normalized quantity in (-1, 1): `tanh(imbalance)`.
|
||||
// Raw imbalance is unbounded (demand can dwarf supply), but the
|
||||
// shadow-price coordinate saturates — a maximally-scarce pool is pinned
|
||||
// at the edge of the potential well, and the symplectic integrator stays
|
||||
// in its stable basin (an un-normalized quartic seed diverges to NaN).
|
||||
// Position = displacement; momentum = price pressure in the same
|
||||
// direction, so kinetic and potential energy both grow with |imbalance|.
|
||||
let displacement = imbalance.tanh();
|
||||
let mut dynamics = HamiltonianDynamics::new(ENERGY_SCALE, PHASE_DIM);
|
||||
dynamics.set_potential(Box::new(AnharmonicPotential::new(
|
||||
POTENTIAL_K2,
|
||||
POTENTIAL_K4,
|
||||
)));
|
||||
{
|
||||
// Public phase-space fields; deterministic seeding (no rng).
|
||||
let mut ps = dynamics.get_phase_space().context("read HMM phase space")?;
|
||||
ps.positions[0] = displacement;
|
||||
ps.momenta[0] = ELASTICITY * displacement;
|
||||
dynamics
|
||||
.set_phase_space(ps)
|
||||
.context("seed HMM phase space")?;
|
||||
}
|
||||
|
||||
// (3) Evolve the conservative system to its energy-modulated equilibrium.
|
||||
for _ in 0..SETTLE_STEPS {
|
||||
dynamics.evolve(DT).context("evolve HMM dynamics")?;
|
||||
}
|
||||
|
||||
// The base price the HMM modulates is the per-unit anchor in whole HANZO.
|
||||
let base_hanzo = market.base_price_wei as f64 / WEI_PER_HANZO as f64;
|
||||
let hmm_unit_hanzo = dynamics
|
||||
.calculate_price(base_hanzo)
|
||||
.context("read HMM equilibrium price")?;
|
||||
|
||||
// (4) Compose the HMM energy premium with the scarcity direction so the
|
||||
// per-unit price is monotone in imbalance, then apply perishability and
|
||||
// tier. `calculate_price` modulates *up* from base via total energy
|
||||
// E = T + V (scarcity raises E); the bounded `scarcity` factor restores
|
||||
// a discount for slack supply (imbalance < 0) around the anchor.
|
||||
let scarcity = (1.0 + (ELASTICITY * imbalance).tanh()).max(f64::MIN_POSITIVE);
|
||||
let unit_hanzo = hmm_unit_hanzo * scarcity;
|
||||
|
||||
// Perishability: tighter deadline ⇒ higher price; best-effort pays none.
|
||||
let urgency = sla_multiplier(job.deadline_secs);
|
||||
// Quality / privacy tier multiplier.
|
||||
let tier = job.tier.multiplier();
|
||||
|
||||
// Total per-unit price, scaled by units demanded, then clamped to bounds.
|
||||
let total_hanzo = unit_hanzo * urgency * tier * job.units;
|
||||
Ok(HanzoPrice(clamp_to_wei(total_hanzo)))
|
||||
}
|
||||
|
||||
/// SLA tightness premium. Tighter (smaller) `deadline_secs` ⇒ larger
|
||||
/// multiplier. Best-effort (`None`) ⇒ 1.0. The premium is bounded in
|
||||
/// `[1.0, 1.0 + URGENCY_MAX]` and strictly decreasing in the deadline horizon.
|
||||
fn sla_multiplier(deadline_secs: Option<f64>) -> f64 {
|
||||
/// Largest urgency premium (at deadline → 0): +100%.
|
||||
const URGENCY_MAX: f64 = 1.0;
|
||||
/// Horizon (seconds) at which urgency has decayed to half of `URGENCY_MAX`.
|
||||
const HALF_LIFE_SECS: f64 = 60.0;
|
||||
match deadline_secs {
|
||||
None => 1.0,
|
||||
Some(d) => {
|
||||
// 1 + URGENCY_MAX * HALF_LIFE / (HALF_LIFE + d): strictly
|
||||
// decreasing in d, → 1+URGENCY_MAX as d→0, → 1 as d→∞.
|
||||
1.0 + URGENCY_MAX * HALF_LIFE_SECS / (HALF_LIFE_SECS + d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamp a whole-HANZO price to the wei bounds. Saturating conversion avoids
|
||||
/// overflow/underflow; the result is always in `[MIN_PRICE_WEI, MAX_PRICE_WEI]`.
|
||||
fn clamp_to_wei(hanzo: f64) -> u128 {
|
||||
if !hanzo.is_finite() || hanzo <= 0.0 {
|
||||
return MIN_PRICE_WEI;
|
||||
}
|
||||
let wei_f = hanzo * WEI_PER_HANZO as f64;
|
||||
let wei = if wei_f >= MAX_PRICE_WEI as f64 {
|
||||
MAX_PRICE_WEI
|
||||
} else {
|
||||
wei_f as u128
|
||||
};
|
||||
wei.clamp(MIN_PRICE_WEI, MAX_PRICE_WEI)
|
||||
}
|
||||
|
||||
fn validate_finite_positive(name: &str, v: f64) -> Result<()> {
|
||||
ensure!(
|
||||
v.is_finite() && v > 0.0,
|
||||
"{name} must be finite and > 0, got {v}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_finite_nonneg(name: &str, v: f64) -> Result<()> {
|
||||
ensure!(
|
||||
v.is_finite() && v >= 0.0,
|
||||
"{name} must be finite and >= 0, got {v}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Base anchor: 0.001 HANZO/unit, comfortably inside [min, max].
|
||||
const BASE: u128 = WEI_PER_HANZO / 1_000; // 1e15 wei
|
||||
|
||||
fn market(kind: ResourceKind, supply: f64, demand: f64) -> MarketState {
|
||||
MarketState::new(kind, supply, demand, BASE)
|
||||
}
|
||||
|
||||
/// (a) Monotonic in scarcity: holding demand fixed, lower supply ⇒
|
||||
/// strictly higher (or equal) price.
|
||||
#[test]
|
||||
fn property_a_monotonic_in_scarcity() {
|
||||
let job = ComputeJob::new(ResourceKind::Gpu, 1.0);
|
||||
let mut last = 0u128;
|
||||
// Supply shrinking from abundant to scarce.
|
||||
for supply in [1000.0, 500.0, 200.0, 100.0, 50.0, 20.0, 10.0] {
|
||||
let p = price(&job, &market(ResourceKind::Gpu, supply, 100.0))
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
p >= last,
|
||||
"price must not fall as supply shrinks: supply={supply} price={p} prev={last}"
|
||||
);
|
||||
last = p;
|
||||
}
|
||||
// And strictly higher at the scarce end than the abundant end.
|
||||
let abundant = price(&job, &market(ResourceKind::Gpu, 1000.0, 100.0))
|
||||
.unwrap()
|
||||
.wei();
|
||||
let scarce = price(&job, &market(ResourceKind::Gpu, 10.0, 100.0))
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
scarce > abundant,
|
||||
"scarce={scarce} must exceed abundant={abundant}"
|
||||
);
|
||||
}
|
||||
|
||||
/// (b) Monotonic in demand: holding supply fixed, higher demand/queue ⇒
|
||||
/// higher price.
|
||||
#[test]
|
||||
fn property_b_monotonic_in_demand() {
|
||||
let job = ComputeJob::new(ResourceKind::Cpu, 1.0);
|
||||
let mut last = 0u128;
|
||||
for demand in [0.0, 50.0, 100.0, 200.0, 400.0, 800.0] {
|
||||
let p = price(&job, &market(ResourceKind::Cpu, 200.0, demand))
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
p >= last,
|
||||
"price must not fall as demand rises: demand={demand} price={p} prev={last}"
|
||||
);
|
||||
last = p;
|
||||
}
|
||||
let low = price(&job, &market(ResourceKind::Cpu, 200.0, 0.0))
|
||||
.unwrap()
|
||||
.wei();
|
||||
let high = price(&job, &market(ResourceKind::Cpu, 200.0, 800.0))
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
high > low,
|
||||
"high-demand={high} must exceed low-demand={low}"
|
||||
);
|
||||
}
|
||||
|
||||
/// (c) SLA / perishability: a tighter deadline ⇒ higher price than a loose
|
||||
/// deadline for the same job and market.
|
||||
#[test]
|
||||
fn property_c_sla_perishability() {
|
||||
let m = market(ResourceKind::Gpu, 200.0, 100.0);
|
||||
let tight = ComputeJob::new(ResourceKind::Gpu, 1.0).with_deadline_secs(5.0);
|
||||
let loose = ComputeJob::new(ResourceKind::Gpu, 1.0).with_deadline_secs(3600.0);
|
||||
let best_effort = ComputeJob::new(ResourceKind::Gpu, 1.0);
|
||||
|
||||
let p_tight = price(&tight, &m).unwrap().wei();
|
||||
let p_loose = price(&loose, &m).unwrap().wei();
|
||||
let p_best = price(&best_effort, &m).unwrap().wei();
|
||||
|
||||
assert!(
|
||||
p_tight > p_loose,
|
||||
"tight deadline {p_tight} must exceed loose deadline {p_loose}"
|
||||
);
|
||||
assert!(
|
||||
p_loose > p_best,
|
||||
"loose deadline {p_loose} must exceed best-effort {p_best}"
|
||||
);
|
||||
// Strictly monotone across a sweep of deadlines (tighter ⇒ dearer).
|
||||
let mut prev = u128::MAX;
|
||||
for d in [1.0, 10.0, 60.0, 300.0, 3600.0, 86400.0] {
|
||||
let p = price(
|
||||
&ComputeJob::new(ResourceKind::Gpu, 1.0).with_deadline_secs(d),
|
||||
&m,
|
||||
)
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
p < prev,
|
||||
"deadline {d}s price {p} must be below tighter {prev}"
|
||||
);
|
||||
prev = p;
|
||||
}
|
||||
}
|
||||
|
||||
/// (d) Bounded: price is always within `[MIN_PRICE_WEI, MAX_PRICE_WEI]`,
|
||||
/// never zero, never unbounded — even under extreme inputs.
|
||||
#[test]
|
||||
fn property_d_bounded() {
|
||||
// Extreme scarcity + tight SLA + top tier + huge job: must cap, not blow up.
|
||||
let hot = ComputeJob::new(ResourceKind::Gpu, 1_000_000.0)
|
||||
.with_deadline_secs(0.001)
|
||||
.with_tier(QualityTier::TeeIo);
|
||||
let starved = market(ResourceKind::Gpu, 0.000_001, 1_000_000.0);
|
||||
let p_hot = price(&hot, &starved).unwrap().wei();
|
||||
assert!(p_hot <= MAX_PRICE_WEI, "must clamp at max: {p_hot}");
|
||||
assert!(p_hot >= MIN_PRICE_WEI, "must stay above min: {p_hot}");
|
||||
assert_eq!(
|
||||
p_hot, MAX_PRICE_WEI,
|
||||
"saturating extreme must hit the ceiling"
|
||||
);
|
||||
|
||||
// Extreme slack + best-effort + open tier + tiny job: must floor, not zero.
|
||||
let cold = ComputeJob::new(ResourceKind::Storage, 0.000_001);
|
||||
let glut = market(ResourceKind::Storage, 1_000_000.0, 0.0);
|
||||
let p_cold = price(&cold, &glut).unwrap().wei();
|
||||
assert!(p_cold >= MIN_PRICE_WEI, "must clamp at min: {p_cold}");
|
||||
assert!(p_cold <= MAX_PRICE_WEI, "must stay below max: {p_cold}");
|
||||
assert!(p_cold > 0, "price is never zero");
|
||||
|
||||
// A spread of ordinary inputs always lands inside the band.
|
||||
for &kind in &[
|
||||
ResourceKind::Cpu,
|
||||
ResourceKind::Gpu,
|
||||
ResourceKind::Memory,
|
||||
ResourceKind::Storage,
|
||||
ResourceKind::Bandwidth,
|
||||
] {
|
||||
for supply in [1.0, 50.0, 500.0] {
|
||||
for demand in [0.0, 100.0, 1000.0] {
|
||||
let p = price(&ComputeJob::new(kind, 4.0), &market(kind, supply, demand))
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
(MIN_PRICE_WEI..=MAX_PRICE_WEI).contains(&p),
|
||||
"out of band: kind={kind:?} supply={supply} demand={demand} price={p}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// (e) Heterogeneity: different resource kinds price independently. Moving
|
||||
/// a CPU pool's demand must not change a GPU pool's price, and a kind
|
||||
/// mismatch between job and market is rejected.
|
||||
#[test]
|
||||
fn property_e_heterogeneity() {
|
||||
let gpu_job = ComputeJob::new(ResourceKind::Gpu, 1.0);
|
||||
let gpu_market = market(ResourceKind::Gpu, 200.0, 100.0);
|
||||
|
||||
// Price the GPU pool while a *separate* CPU pool swings wildly.
|
||||
let gpu_price_before = price(&gpu_job, &gpu_market).unwrap().wei();
|
||||
let _cpu_calm = price(
|
||||
&ComputeJob::new(ResourceKind::Cpu, 1.0),
|
||||
&market(ResourceKind::Cpu, 1000.0, 0.0),
|
||||
)
|
||||
.unwrap();
|
||||
let _cpu_hot = price(
|
||||
&ComputeJob::new(ResourceKind::Cpu, 1.0),
|
||||
&market(ResourceKind::Cpu, 1.0, 100_000.0),
|
||||
)
|
||||
.unwrap();
|
||||
let gpu_price_after = price(&gpu_job, &gpu_market).unwrap().wei();
|
||||
|
||||
assert_eq!(
|
||||
gpu_price_before, gpu_price_after,
|
||||
"GPU price must not move because a CPU pool moved"
|
||||
);
|
||||
|
||||
// A job and a market for different kinds must be rejected (no shared
|
||||
// state, no accidental cross-kind pricing).
|
||||
let mismatch = price(&gpu_job, &market(ResourceKind::Cpu, 200.0, 100.0));
|
||||
let err = mismatch.expect_err("kind mismatch must be rejected");
|
||||
assert!(
|
||||
err.to_string().contains(ERR_KIND_MISMATCH),
|
||||
"expected kind-mismatch error, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// (f) Deterministic: identical `(job, market)` yields byte-identical
|
||||
/// prices across repeated calls (noise-free HMM path, no rng).
|
||||
#[test]
|
||||
fn property_f_deterministic() {
|
||||
let job = ComputeJob::new(ResourceKind::Gpu, 3.0)
|
||||
.with_deadline_secs(42.0)
|
||||
.with_tier(QualityTier::GpuTee);
|
||||
let m = market(ResourceKind::Gpu, 137.0, 211.0);
|
||||
|
||||
let first = price(&job, &m).unwrap();
|
||||
for _ in 0..1000 {
|
||||
assert_eq!(
|
||||
price(&job, &m).unwrap(),
|
||||
first,
|
||||
"pricing must be deterministic for a fixed (job, market)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tier ordering: higher privacy tier ⇒ higher price, all else equal.
|
||||
#[test]
|
||||
fn tier_monotonic() {
|
||||
let m = market(ResourceKind::Gpu, 200.0, 100.0);
|
||||
let mut prev = 0u128;
|
||||
for tier in [
|
||||
QualityTier::Open,
|
||||
QualityTier::AtRest,
|
||||
QualityTier::CpuTee,
|
||||
QualityTier::GpuTee,
|
||||
QualityTier::TeeIo,
|
||||
] {
|
||||
let p = price(&ComputeJob::new(ResourceKind::Gpu, 1.0).with_tier(tier), &m)
|
||||
.unwrap()
|
||||
.wei();
|
||||
assert!(
|
||||
p > prev,
|
||||
"tier {tier:?} price {p} must exceed lower tier {prev}"
|
||||
);
|
||||
prev = p;
|
||||
}
|
||||
}
|
||||
|
||||
/// Bad input is rejected explicitly (no panic, no silent zero).
|
||||
#[test]
|
||||
fn rejects_bad_input() {
|
||||
let m = market(ResourceKind::Cpu, 100.0, 10.0);
|
||||
assert!(price(&ComputeJob::new(ResourceKind::Cpu, 0.0), &m).is_err());
|
||||
assert!(price(&ComputeJob::new(ResourceKind::Cpu, f64::NAN), &m).is_err());
|
||||
assert!(price(
|
||||
&ComputeJob::new(ResourceKind::Cpu, 1.0),
|
||||
&MarketState::new(ResourceKind::Cpu, 0.0, 10.0, BASE)
|
||||
)
|
||||
.is_err());
|
||||
assert!(price(
|
||||
&ComputeJob::new(ResourceKind::Cpu, 1.0),
|
||||
&MarketState::new(ResourceKind::Cpu, 100.0, 10.0, 0)
|
||||
)
|
||||
.is_err());
|
||||
assert!(price(
|
||||
&ComputeJob::new(ResourceKind::Cpu, 1.0).with_deadline_secs(-1.0),
|
||||
&m
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,25 @@ impl HamiltonianDynamics {
|
||||
pub fn set_potential(&mut self, potential: Box<dyn PotentialFunction>) {
|
||||
self.potential = potential;
|
||||
}
|
||||
|
||||
/// Set the phase space state directly. Enables deterministic seeding of
|
||||
/// `(q, p)` from an external source (e.g. a market imbalance) without the
|
||||
/// stochastic `perturb`/`PriceDynamics::update` path, so evolution is
|
||||
/// reproducible. Errors if the supplied state's dimension does not match.
|
||||
pub fn set_phase_space(&mut self, phase_space: PhaseSpace) -> Result<()> {
|
||||
if phase_space.positions.len() != self.dimension
|
||||
|| phase_space.momenta.len() != self.dimension
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"phase space dimension mismatch: expected {}, got positions={} momenta={}",
|
||||
self.dimension,
|
||||
phase_space.positions.len(),
|
||||
phase_space.momenta.len()
|
||||
));
|
||||
}
|
||||
self.phase_space = phase_space;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inject energy into the system (perturbation)
|
||||
pub fn perturb(&mut self, energy: f64) -> Result<()> {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod routing;
|
||||
pub mod adapter;
|
||||
pub mod storage;
|
||||
pub mod free_energy;
|
||||
pub mod compute_price;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -31,6 +32,10 @@ pub use routing::{Router, RoutingDecision, ModelSelection};
|
||||
pub use adapter::{UserAdapter, AdapterManager};
|
||||
pub use storage::{Storage, VectorIndex};
|
||||
pub use free_energy::{ExpectedFreeEnergy, BeliefState, Precision};
|
||||
pub use compute_price::{
|
||||
price, ComputeJob, HanzoPrice, MarketState, QualityTier, ResourceKind, MAX_PRICE_WEI,
|
||||
MIN_PRICE_WEI, WEI_PER_HANZO,
|
||||
};
|
||||
|
||||
/// Main MarketMaker system configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user