Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .coverage
Binary file not shown.
20 changes: 20 additions & 0 deletions radial_membrane_ai/governor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,30 @@
from radial_membrane_ai.membrane import BehavioralString, RadialMembrane
from radial_membrane_ai.channels import channel_coherence
from radial_membrane_ai.admissibility import apply_lyapunov_dissipation, phase_smoothing
from radial_membrane_ai.exceptions import ValidationError


@dataclass
class GovernorConfig:
def __post_init__(self) -> None:
self.validate()

def validate(self) -> None:
"""Enforces strict invariants on Governor Configuration."""
weights = [
("w_d", self.w_d), ("w_a", self.w_a), ("w_l", self.w_l),
("w_c", self.w_c), ("w_T", self.w_T), ("w_K", self.w_K),
("lyapunov_alpha", self.lyapunov_alpha), ("lyapunov_beta", self.lyapunov_beta),
("lyapunov_gamma", self.lyapunov_gamma)
]
for w_name, w_val in weights:
if w_val < 0.0:
raise ValidationError(f"Weight {w_name} must be non-negative, got {w_val}.")
if self.learning_rate <= 0.0:
raise ValidationError(f"learning_rate must be strictly positive, got {self.learning_rate}.")
if self.suppression_weight < 0.0:
raise ValidationError(f"suppression_weight must be non-negative, got {self.suppression_weight}.")

"""
Configuration coefficients for the local cost function and stability control.

Expand Down
20 changes: 20 additions & 0 deletions radial_membrane_ai/membrane.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,33 @@
from dataclasses import dataclass
import numpy as np
from typing import TYPE_CHECKING
from radial_membrane_ai.exceptions import ValidationError, GeometryValidationError

if TYPE_CHECKING:
from radial_membrane_ai.facet import FacetVector


@dataclass
class BehavioralString:
def __post_init__(self) -> None:
self.validate()

def validate(self) -> None:
"""Enforces strict invariants on BehavioralString."""
if self.radius < 0.0:
raise GeometryValidationError(f"Radius cannot be negative, got {self.radius}.")
if self.tension < 0.0:
raise GeometryValidationError(f"Tension cannot be negative, got {self.tension}.")
if self.stiffness < 0.0:
raise GeometryValidationError(f"Stiffness cannot be negative, got {self.stiffness}.")
if self.activation < 0.0:
raise ValidationError(f"Activation cannot be negative, got {self.activation}.")
if self.cost < 0.0:
raise ValidationError(f"Cost cannot be negative, got {self.cost}.")
if self.theta < -1e-5 or self.theta > 2.0 * math.pi + 1e-5:
raise GeometryValidationError(f"Angle theta must be in [0, 2pi], got {self.theta}.")
if not (1 <= self.index <= 12):
raise ValidationError(f"Index must be between 1 and 12, got {self.index}.")
"""
A controllable dimension of behavior on the radial identity membrane.

Expand Down
34 changes: 34 additions & 0 deletions radial_membrane_ai/tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@ def test_membrane_initialization() -> None:
with pytest.raises(ValueError):
membrane.get_quadrant_activation("invalid_quadrant")

# Test BehavioralString strict validation
from radial_membrane_ai.exceptions import ValidationError, GeometryValidationError
with pytest.raises(GeometryValidationError, match="Radius cannot be negative"):
BehavioralString("err", 1, 0.0, 0.5, -1.0, 0.0, 0.0, 0.0)

with pytest.raises(GeometryValidationError, match="Tension cannot be negative"):
BehavioralString("err", 1, 0.0, 0.5, 1.0, 0.0, -0.1, 0.0)

with pytest.raises(GeometryValidationError, match="Stiffness cannot be negative"):
BehavioralString("err", 1, 0.0, 0.5, 1.0, 0.0, 0.0, -0.5)

with pytest.raises(ValidationError, match="Activation cannot be negative"):
BehavioralString("err", 1, 0.0, -0.5, 1.0, 0.0, 0.0, 0.0)

with pytest.raises(ValidationError, match="Cost cannot be negative"):
BehavioralString("err", 1, 0.0, 0.5, 1.0, -2.0, 0.0, 0.0)

with pytest.raises(GeometryValidationError, match="Angle theta must be in"):
BehavioralString("err", 1, -1.0, 0.5, 1.0, 0.0, 0.0, 0.0)

with pytest.raises(ValidationError, match="Index must be between"):
BehavioralString("err", 15, 0.0, 0.5, 1.0, 0.0, 0.0, 0.0)


def test_membrane_activation_updates_and_field() -> None:
"""
Expand Down Expand Up @@ -228,6 +251,17 @@ def test_governor_costs_and_stability() -> None:
gov.energy_history = [1.0, 2.0, 3.0, 4.0, 5.0]
assert not gov.is_stable() # growing and unstable

# Test GovernorConfig validation
from radial_membrane_ai.exceptions import ValidationError
with pytest.raises(ValidationError, match="Weight w_d must be non-negative"):
GovernorConfig(w_d=-0.1)

with pytest.raises(ValidationError, match="learning_rate must be strictly positive"):
GovernorConfig(learning_rate=0.0)

with pytest.raises(ValidationError, match="suppression_weight must be non-negative"):
GovernorConfig(suppression_weight=-1.5)


def test_boundary_geometry() -> None:
"""
Expand Down
42 changes: 42 additions & 0 deletions radial_membrane_ai/tests/test_ufo_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

from __future__ import annotations
import pytest
import numpy as np

from radial_membrane_ai.ufo_engine import (
Expand Down Expand Up @@ -219,3 +220,44 @@ def test_serialization_and_exports(tmp_path) -> None:
}
export_simulation_results_to_json(dummy_results, str(results_file))
assert results_file.exists()


def test_stability_metrics_post_tick_violations() -> None:
"""
Tests that post-tick verification on stability metrics (Lyapunov energy,
tension, stiffness, curvature, deviation) correctly raises GovernanceError on violations.
"""
from radial_membrane_ai.exceptions import GovernanceError

# 1. Test Lyapunov energy violation
engine_sa = SingleAgentEngine()
engine_sa.membrane.strings[0].tension = 20.0 # Exceeds hard_energy_limit of 15.0 through computed energy
with pytest.raises(GovernanceError, match="Lyapunov energy.*exceeded hard limit"):
engine_sa.tick(task_value=0.5, excitation=np.ones(12) * 0.5)

# 2. Test Tension violation
engine_sa2 = SingleAgentEngine()
if hasattr(engine_sa2.membrane, "temporal_state") and engine_sa2.membrane.temporal_state:
engine_sa2.membrane.temporal_state.accumulated_tension = 100.0 # Exceeds hard_tension_limit of 10.0
with pytest.raises(GovernanceError, match="Temporal tension.*exceeded hard limit"):
engine_sa2.tick(task_value=0.5, excitation=np.ones(12) * 0.5)

# 3. Test Stiffness violation
engine_sa3 = SingleAgentEngine()
# Artificially modify a string stiffness to exceed 2.0
engine_sa3.membrane.strings[0].stiffness = 5.0
with pytest.raises(GovernanceError, match="Stiffness.*exceeded hard limit"):
engine_sa3.tick(task_value=0.5, excitation=np.ones(12) * 0.5)

# 4. Test Curvature violation
engine_sa4 = SingleAgentEngine()
setattr(engine_sa4.boundary, "curvature", lambda theta, **kwargs: 100.0)
with pytest.raises(GovernanceError, match="Boundary curvature.*exceeded hard limit"):
engine_sa4.tick(task_value=0.5, excitation=np.ones(12) * 0.5)

# 5. Test Radius Deviation violation
engine_sa5 = SingleAgentEngine()
setattr(engine_sa5.boundary, "update_boundary", lambda *args, **kwargs: None)
engine_sa5.boundary.radius_deviation[1] = 100.0
with pytest.raises(GovernanceError, match="Radius deviation.*exceeded hard limit"):
engine_sa5.tick(task_value=0.5, excitation=np.ones(12) * 0.5)
33 changes: 33 additions & 0 deletions radial_membrane_ai/tests/test_workloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,3 +606,36 @@ def mc_custom_apply(eng: Any, step: Any, tgt: Any) -> None:

setattr(engine_mc_any, "_apply_actions", mc_custom_apply)
_ = engine_mc.run(mc_workload)


def test_invalid_workload_validation_error() -> None:
"""
Tests that a workload with a non-existent agent/cluster ID causes
WorkloadConfigurationError to be raised when executed.
"""
from radial_membrane_ai.exceptions import WorkloadConfigurationError

# We define a workload targeting a non-existent entity and verify validation catches it.
invalid_step = WorkloadStep(
agent_actions={
"bad_action": Action.change_regime("non_existent_agent_99", "STOCHASTIC")
}
)
invalid_workload = Workload(
name="Invalid Workload Test",
description="Should fail validation",
target=SimulationTarget.MULTI_AGENT,
regime_expectation=KernelRegimeType.BALANCED,
stability_expectation=StabilityBand.GREEN,
coherence_expectation=0.5,
envelope_expectation=PolicyEnvelope(),
steps=[invalid_step]
)

engine = WorkloadEngine()
with pytest.raises(WorkloadConfigurationError, match="references invalid entity"):
engine.run(invalid_workload)

# Test run_trace also runs the same validation
with pytest.raises(WorkloadConfigurationError, match="references invalid entity"):
engine.run_trace(invalid_workload)
79 changes: 76 additions & 3 deletions radial_membrane_ai/ufo_engine/multi_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def __init__(
n_agents: int = 3,
cost_weights: CostWeights | None = None,
band_config: StabilityBandConfig | None = None,
custom_agents: List[UFOAgent] | None = None
custom_agents: List[UFOAgent] | None = None,
seed: int = 0
) -> None:
"""
Initializes the Multi-Agent Engine.
Expand Down Expand Up @@ -105,7 +106,7 @@ def __init__(
self.residual_history: List[float] = []

# Call deterministic environment seeding
set_deterministic_env()
set_deterministic_env(seed)

# Kernel Regime Expansion Layer components
self.regime_manager = RegimeManager()
Expand Down Expand Up @@ -711,9 +712,28 @@ def tick(self, task_value: float, excitation: np.ndarray) -> str:
)

# Post-tick invariant assertions and near-violation warning logs
hard_energy_limit = 15.0
hard_tension_limit = 10.0
latest_tension = self.temporal_state.accumulated_tension if self.temporal_state is not None else 0.0
hard_stiffness_limit = 2.0
hard_curvature_limit = 50.0
hard_deviation_limit = 5.0

# Check Lyapunov energy
latest_energy = self.h_hol_history[-1] if self.h_hol_history else 0.0
if latest_energy > hard_energy_limit:
from radial_membrane_ai.exceptions import GovernanceError
raise GovernanceError(
f"Governed bound violated: Multi-agent energy ({latest_energy:.4f}) "
f"exceeded hard limit ({hard_energy_limit})."
)
elif latest_energy >= 0.95 * hard_energy_limit:
self.interventions.append(
f"Near-violation warning: Multi-agent energy ({latest_energy:.4f}) "
f"is within 5% of hard limit ({hard_energy_limit})."
)

# Check Tension
latest_tension = self.temporal_state.accumulated_tension if self.temporal_state is not None else 0.0
if latest_tension > hard_tension_limit:
from radial_membrane_ai.exceptions import GovernanceError
raise GovernanceError(
Expand All @@ -726,6 +746,59 @@ def tick(self, task_value: float, excitation: np.ndarray) -> str:
f"is within 5% of hard limit ({hard_tension_limit})."
)

# Check Stiffness, Curvature, Deviation across active agents
active_agents = [a for a in self.agents if a.shard.state != ShardState.QUARANTINED]
if active_agents:
max_stiffness = float(max(s.stiffness for a in active_agents for s in a.membrane.strings))
if max_stiffness > hard_stiffness_limit:
from radial_membrane_ai.exceptions import GovernanceError
raise GovernanceError(
f"Governed bound violated: Stiffness ({max_stiffness:.4f}) "
f"exceeded hard limit ({hard_stiffness_limit})."
)
elif max_stiffness >= 0.95 * hard_stiffness_limit:
self.interventions.append(
f"Near-violation warning: Stiffness ({max_stiffness:.4f}) "
f"is within 5% of hard limit ({hard_stiffness_limit})."
)

max_curvature = float(max(
abs(a.boundary.curvature(s.theta)) for a in active_agents for s in a.membrane.strings
))
if max_curvature > hard_curvature_limit:
from radial_membrane_ai.exceptions import GovernanceError
raise GovernanceError(
f"Governed bound violated: Boundary curvature ({max_curvature:.4f}) "
f"exceeded hard limit ({hard_curvature_limit})."
)
elif max_curvature >= 0.95 * hard_curvature_limit:
self.interventions.append(
f"Near-violation warning: Boundary curvature ({max_curvature:.4f}) "
f"is within 5% of hard limit ({hard_curvature_limit})."
)

max_dev = float(max(abs(dev) for a in active_agents for dev in a.boundary.radius_deviation.values()))
if max_dev > hard_deviation_limit:
from radial_membrane_ai.exceptions import GovernanceError
raise GovernanceError(
f"Governed bound violated: Radius deviation ({max_dev:.4f}) "
f"exceeded hard limit ({hard_deviation_limit})."
)
elif max_dev >= 0.95 * hard_deviation_limit:
self.interventions.append(
f"Near-violation warning: Radius deviation ({max_dev:.4f}) "
f"is within 5% of hard limit ({hard_deviation_limit})."
)

# Check Coherence
latest_coh = self.c_mesh_history[-1] if self.c_mesh_history else 1.0
if latest_coh < 0.0:
from radial_membrane_ai.exceptions import GovernanceError
raise GovernanceError(
f"Governed bound violated: Coherence ({latest_coh:.4f}) "
f"fell below lower limit (0.0)."
)

finally:
# Restore agent boundaries get_radius scale
for agent in self.agents:
Expand Down
Loading
Loading