diff --git a/.coverage b/.coverage index 577b461..3b1fb02 100644 Binary files a/.coverage and b/.coverage differ diff --git a/radial_membrane_ai/governor.py b/radial_membrane_ai/governor.py index cab068c..7b1e2a1 100644 --- a/radial_membrane_ai/governor.py +++ b/radial_membrane_ai/governor.py @@ -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. diff --git a/radial_membrane_ai/membrane.py b/radial_membrane_ai/membrane.py index 2c60bcf..6e02145 100644 --- a/radial_membrane_ai/membrane.py +++ b/radial_membrane_ai/membrane.py @@ -11,6 +11,7 @@ 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 @@ -18,6 +19,25 @@ @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. diff --git a/radial_membrane_ai/tests/test_model.py b/radial_membrane_ai/tests/test_model.py index 7f798c2..f05d9ff 100644 --- a/radial_membrane_ai/tests/test_model.py +++ b/radial_membrane_ai/tests/test_model.py @@ -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: """ @@ -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: """ diff --git a/radial_membrane_ai/tests/test_ufo_engine.py b/radial_membrane_ai/tests/test_ufo_engine.py index 321280a..b70f2d7 100644 --- a/radial_membrane_ai/tests/test_ufo_engine.py +++ b/radial_membrane_ai/tests/test_ufo_engine.py @@ -4,6 +4,7 @@ """ from __future__ import annotations +import pytest import numpy as np from radial_membrane_ai.ufo_engine import ( @@ -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) diff --git a/radial_membrane_ai/tests/test_workloads.py b/radial_membrane_ai/tests/test_workloads.py index 4120296..3221630 100644 --- a/radial_membrane_ai/tests/test_workloads.py +++ b/radial_membrane_ai/tests/test_workloads.py @@ -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) diff --git a/radial_membrane_ai/ufo_engine/multi_agent.py b/radial_membrane_ai/ufo_engine/multi_agent.py index 9c8088a..687f46b 100644 --- a/radial_membrane_ai/ufo_engine/multi_agent.py +++ b/radial_membrane_ai/ufo_engine/multi_agent.py @@ -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. @@ -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() @@ -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( @@ -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: diff --git a/radial_membrane_ai/ufo_engine/multi_cluster.py b/radial_membrane_ai/ufo_engine/multi_cluster.py index c3fd029..8dce943 100644 --- a/radial_membrane_ai/ufo_engine/multi_cluster.py +++ b/radial_membrane_ai/ufo_engine/multi_cluster.py @@ -40,7 +40,7 @@ class MultiClusterEngine: hierarchical Holistic Governor, supporting governed behavioral regimes. """ - def __init__(self) -> None: + def __init__(self, seed: int = 0) -> None: """ Initializes the Multi-Cluster Simulation Engine. """ @@ -60,7 +60,7 @@ def __init__(self) -> None: self.interventions: List[str] = [] # Call deterministic environment seeding - set_deterministic_env() + set_deterministic_env(seed) # Kernel Regime Expansion Layer components self.regime_manager = RegimeManager() @@ -770,9 +770,28 @@ def tick(self, task_value: float, default_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_global_history[-1] if self.h_global_history else 0.0 + if latest_energy > hard_energy_limit: + from radial_membrane_ai.exceptions import GovernanceError + raise GovernanceError( + f"Governed bound violated: Multi-cluster 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-cluster 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( @@ -785,6 +804,60 @@ def tick(self, task_value: float, default_excitation: np.ndarray) -> str: f"is within 5% of hard limit ({hard_tension_limit})." ) + # Check active clusters and agents within them + active_clusters = [c for c in self.clusters.values() if getattr(c, "quarantine_timer", 0) == 0] + active_agents = [a for c in active_clusters for a in c.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_global_history[-1] if self.c_global_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: for cluster in self.clusters.values(): for agent in cluster.agents: diff --git a/radial_membrane_ai/ufo_engine/single_agent.py b/radial_membrane_ai/ufo_engine/single_agent.py index e9dd3ae..9742423 100644 --- a/radial_membrane_ai/ufo_engine/single_agent.py +++ b/radial_membrane_ai/ufo_engine/single_agent.py @@ -64,7 +64,8 @@ def __init__( band_config: StabilityBandConfig | None = None, r_max: float = 2.0, r_growth_rate: float = 0.3, - r_relaxation: float = 0.2 + r_relaxation: float = 0.2, + seed: int = 0 ) -> None: """ Initializes the single-agent engine. @@ -97,7 +98,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() @@ -595,6 +596,9 @@ def tick( # Post-tick invariant assertions and near-violation warning logs hard_energy_limit = 15.0 hard_tension_limit = 10.0 + hard_stiffness_limit = 2.0 + hard_curvature_limit = 50.0 + hard_deviation_limit = 5.0 # Check Lyapunov energy latest_energy = self.v_history[-1] if self.v_history else 0.0 @@ -624,6 +628,66 @@ def tick( f"is within 5% of hard limit ({hard_tension_limit})." ) + # Check Stiffness + max_stiffness = ( + float(max(s.stiffness for s in self.membrane.strings)) + if self.membrane.strings else 0.0 + ) + 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})." + ) + + # Check Channel Coherence + min_coherence = self.compute_local_coherence() + if min_coherence < 0.0: + from radial_membrane_ai.exceptions import GovernanceError + raise GovernanceError( + f"Governed bound violated: Channel coherence ({min_coherence:.4f}) " + f"fell below lower limit (0.0)." + ) + + # Check Boundary Curvature + max_curvature = ( + float(max(abs(self.boundary.curvature(s.theta)) for s in self.membrane.strings)) + if self.membrane.strings else 0.0 + ) + 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})." + ) + + # Check Radius Deviation + max_dev = ( + float(max(abs(dev) for dev in self.boundary.radius_deviation.values())) + if self.boundary.radius_deviation else 0.0 + ) + 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})." + ) + self._record_state() finally: diff --git a/radial_membrane_ai/visualization/visualizer.py b/radial_membrane_ai/visualization/visualizer.py index 097f313..8ced2eb 100644 --- a/radial_membrane_ai/visualization/visualizer.py +++ b/radial_membrane_ai/visualization/visualizer.py @@ -20,11 +20,15 @@ from radial_membrane_ai.workloads.engine import WorkloadTrace -def extract_geom_and_vchannel(membrane: Any, boundary: Any) -> Tuple[MembraneGeometrySnapshot, VChannelSnapshot]: +def extract_geom_and_vchannel( + membrane: Any, + boundary: Any, + samples: int = 100 +) -> Tuple[MembraneGeometrySnapshot, VChannelSnapshot]: """Helper to extract geometry and V-channel snapshots from any membrane & boundary.""" - # Sample 100 angles for boundary coordinates + # Sample configured number of angles for boundary coordinates boundary_coords = [] - for theta in np.linspace(0, 2 * math.pi, 100, endpoint=False): + for theta in np.linspace(0, 2 * math.pi, samples, endpoint=False): r = boundary.get_radius(theta) if hasattr(boundary, 'get_radius') else 1.0 boundary_coords.append((r * math.cos(theta), r * math.sin(theta))) @@ -85,6 +89,8 @@ class MeshVisualizer: Renders diagnostic dashboard frames and timelines across agents, clusters, and the global mesh. """ + def __init__(self, samples_resolution: int = 100) -> None: + self.samples_resolution = samples_resolution def render_agent_state(self, agent_state: Any) -> VisualizationFrame: """Renders single agent state snapshot at a single timestep.""" @@ -99,7 +105,7 @@ def render_agent_state(self, agent_state: Any) -> VisualizationFrame: if not (membrane and boundary): raise ValueError("State does not have valid membrane or boundary attributes.") - geom, vch = extract_geom_and_vchannel(membrane, boundary) + geom, vch = extract_geom_and_vchannel(membrane, boundary, samples=self.samples_resolution) # Base metrics curvature = float(max(geom.curvature_map)) if geom.curvature_map else 0.0