felix-framework / tests /validation /validate_mathematics.py
jkbennitt
Clean hf-space branch and prepare for HuggingFace Spaces deployment
fb867c3
#!/usr/bin/env python3
"""
Mathematical validation script for the Felix Framework.
This script validates mathematical properties derived in the formal documentation
against the implementation, ensuring theoretical consistency and correctness.
Validates:
1. Parametric helix equations and properties
2. Geometric invariants (curvature, torsion)
3. Agent distribution functions
4. Communication complexity bounds
5. Attention focusing mechanism properties
Mathematical references:
- docs/mathematical_model.md: Formal mathematical specifications
- docs/hypothesis_mathematics.md: Statistical formulations and proofs
"""
import math
import numpy as np
from typing import List, Tuple
import time
from src.core.helix_geometry import HelixGeometry
from src.agents.agent import generate_spawn_times, create_openscad_agents
from src.communication import CentralPost
class MathematicalValidator:
"""Validates mathematical properties against implementation."""
def __init__(self, tolerance: float = 1e-10):
"""Initialize validator with numerical tolerance."""
self.tolerance = tolerance
self.helix = HelixGeometry(33.0, 0.001, 33.0, 33) # OpenSCAD parameters
self.validation_results = {}
def validate_parametric_equations(self) -> bool:
"""
Validate parametric helix equations from mathematical_model.md Section 1.2.
Tests:
1. Boundary conditions: r(0) and r(1)
2. Radius tapering function R(t)
3. Angular progression θ(t)
4. Height function z(t) = Ht
"""
print("=== Validating Parametric Equations ===")
# Test boundary conditions
pos_0 = self.helix.get_position(0.0)
pos_1 = self.helix.get_position(1.0)
# At t=0: should be at (R_bottom, 0, 0)
expected_0 = (self.helix.bottom_radius, 0.0, 0.0)
diff_0 = max(abs(a - b) for a, b in zip(pos_0, expected_0))
# At t=1: should be at (R_top, 0, height) after n full turns
# After 33 turns, angle = 33 * 2π, so cos(33*2π) = cos(0) = 1
expected_1 = (self.helix.top_radius, 0.0, self.helix.height)
diff_1 = max(abs(a - b) for a, b in zip(pos_1, expected_1))
boundary_valid = diff_0 < self.tolerance and diff_1 < self.tolerance
print(f"Boundary conditions: {'✅' if boundary_valid else '❌'}")
print(f" t=0: {pos_0} vs {expected_0}, diff={diff_0:.2e}")
print(f" t=1: {pos_1} vs {expected_1}, diff={diff_1:.2e}")
# Test radius function R(t) = R_bottom * (R_top/R_bottom)^t
radius_valid = True
for t in [0.0, 0.25, 0.5, 0.75, 1.0]:
z = self.helix.height * t
actual_radius = self.helix.get_radius(z)
expected_radius = self.helix.bottom_radius * pow(
self.helix.top_radius / self.helix.bottom_radius, t
)
diff = abs(actual_radius - expected_radius)
if diff > self.tolerance:
radius_valid = False
print(f" R({t:.2f}): {actual_radius:.6f} vs {expected_radius:.6f}, diff={diff:.2e}")
print(f"Radius function: {'✅' if radius_valid else '❌'}")
# Test monotonicity of R(t) - should be strictly increasing
monotonic_valid = True
prev_radius = 0.0
for t in np.linspace(0, 1, 100):
z = self.helix.height * t
radius = self.helix.get_radius(z)
if radius <= prev_radius and t > 0:
monotonic_valid = False
break
prev_radius = radius
print(f"Radius monotonicity: {'✅' if monotonic_valid else '❌'}")
self.validation_results['parametric_equations'] = {
'boundary_conditions': boundary_valid,
'radius_function': radius_valid,
'monotonicity': monotonic_valid,
'overall': boundary_valid and radius_valid and monotonic_valid
}
return boundary_valid and radius_valid and monotonic_valid
def validate_geometric_properties(self) -> bool:
"""
Validate geometric properties from mathematical_model.md Section 3.
Tests:
1. Arc length approximation convergence
2. Tangent vector properties
3. Smoothness (differentiability)
"""
print("\n=== Validating Geometric Properties ===")
# Test arc length approximation convergence
# Should converge as number of segments increases
segments_list = [100, 500, 1000, 2000]
arc_lengths = []
for segments in segments_list:
length = self.helix.approximate_arc_length(segments=segments)
arc_lengths.append(length)
# Check convergence: difference between consecutive approximations should decrease
convergence_valid = True
for i in range(1, len(arc_lengths)):
diff_curr = abs(arc_lengths[i] - arc_lengths[i-1])
if i > 1:
diff_prev = abs(arc_lengths[i-1] - arc_lengths[i-2])
if diff_curr >= diff_prev: # Should be decreasing
convergence_valid = False
print(f" Arc length ({segments_list[i]} segments): {arc_lengths[i]:.3f}, "
f"diff from previous: {diff_curr:.3f}")
print(f"Arc length convergence: {'✅' if convergence_valid else '❌'}")
# Test tangent vector properties
tangent_valid = True
for t in [0.1, 0.3, 0.5, 0.7, 0.9]:
tangent = self.helix.get_tangent_vector(t)
# Tangent vector should be normalized (length ≈ 1)
length = math.sqrt(sum(component**2 for component in tangent))
if abs(length - 1.0) > self.tolerance:
tangent_valid = False
print(f" Tangent at t={t}: length={length:.6f}")
print(f"Tangent vector normalization: {'✅' if tangent_valid else '❌'}")
# Test smoothness by checking continuity of positions
smoothness_valid = True
epsilon = 1e-6
for t in np.linspace(0.1, 0.9, 20):
pos1 = self.helix.get_position(t - epsilon)
pos2 = self.helix.get_position(t + epsilon)
# Distance should be small for small epsilon
distance = math.sqrt(sum((a - b)**2 for a, b in zip(pos1, pos2)))
expected_distance = 2 * epsilon * self.helix.approximate_arc_length(segments=1000)
if distance > 10 * expected_distance: # Allow some tolerance
smoothness_valid = False
print(f"Smoothness (continuity): {'✅' if smoothness_valid else '❌'}")
self.validation_results['geometric_properties'] = {
'arc_length_convergence': convergence_valid,
'tangent_normalization': tangent_valid,
'smoothness': smoothness_valid,
'overall': convergence_valid and tangent_valid and smoothness_valid
}
return convergence_valid and tangent_valid and smoothness_valid
def validate_agent_distribution(self) -> bool:
"""
Validate agent distribution functions from mathematical_model.md Section 4.
Tests:
1. Uniform spawn time distribution U(0,1)
2. Agent density evolution properties
3. Statistical properties of large samples
"""
print("\n=== Validating Agent Distribution ===")
# Test spawn time distribution
N = 10000
spawn_times = generate_spawn_times(N, seed=12345)
# Test uniformity using Kolmogorov-Smirnov test approximation
sorted_times = sorted(spawn_times)
max_deviation = 0.0
for i, t in enumerate(sorted_times):
empirical_cdf = (i + 1) / N
theoretical_cdf = t # For U(0,1), CDF(t) = t
deviation = abs(empirical_cdf - theoretical_cdf)
max_deviation = max(max_deviation, deviation)
# For U(0,1), critical value at α=0.05 is approximately 1.36/√N
critical_value = 1.36 / math.sqrt(N)
uniformity_valid = max_deviation < critical_value
print(f"Spawn time uniformity: {'✅' if uniformity_valid else '❌'}")
print(f" Max KS deviation: {max_deviation:.6f}, critical: {critical_value:.6f}")
# Test mean and variance
mean_time = np.mean(spawn_times)
var_time = np.var(spawn_times)
# For U(0,1): mean = 0.5, variance = 1/12 ≈ 0.0833
mean_valid = abs(mean_time - 0.5) < 3 * math.sqrt(1/12/N) # 3-sigma test
var_valid = abs(var_time - 1/12) < 0.01 # Allow reasonable tolerance
print(f"Mean (expected 0.5): {mean_time:.6f}, valid: {'✅' if mean_valid else '❌'}")
print(f"Variance (expected 0.0833): {var_time:.6f}, valid: {'✅' if var_valid else '❌'}")
# Test reproducibility with same seed
spawn_times_2 = generate_spawn_times(N, seed=12345)
reproducibility_valid = spawn_times == spawn_times_2
print(f"Reproducibility with same seed: {'✅' if reproducibility_valid else '❌'}")
self.validation_results['agent_distribution'] = {
'uniformity': uniformity_valid,
'mean': mean_valid,
'variance': var_valid,
'reproducibility': reproducibility_valid,
'overall': uniformity_valid and mean_valid and var_valid and reproducibility_valid
}
return uniformity_valid and mean_valid and var_valid and reproducibility_valid
def validate_attention_focusing(self) -> bool:
"""
Validate attention focusing mechanism from hypothesis_mathematics.md Section H3.
Tests:
1. Attention density A(t) = k / (2πR(t))
2. Monotonic increase: dA/dt > 0
3. Exponential growth toward narrow end
"""
print("\n=== Validating Attention Focusing Mechanism ===")
# Calculate attention density at different positions
attention_densities = []
t_values = np.linspace(0.1, 0.9, 20)
for t in t_values:
z = self.helix.height * t
radius = self.helix.get_radius(z)
# Attention density A(t) = 1 / (2π * R(t))
attention = 1.0 / (2 * math.pi * radius)
attention_densities.append(attention)
# Test monotonic decrease (attention decreases as radius increases toward top)
monotonic_decrease = True
for i in range(1, len(attention_densities)):
if attention_densities[i] >= attention_densities[i-1]:
monotonic_decrease = False
print(f" Non-monotonic at i={i}: A[{i-1}]={attention_densities[i-1]:.6f}, "
f"A[{i}]={attention_densities[i]:.6f}")
break
print(f"Attention density monotonic decrease (correct): {'✅' if monotonic_decrease else '❌'}")
# Test exponential decay rate (attention decreases exponentially)
# dA/dt should be negative and proportional to -A(t) * ln(R_top/R_bottom)
ln_ratio = math.log(self.helix.top_radius / self.helix.bottom_radius)
exponential_decay_valid = True
for i in range(1, len(attention_densities) - 1):
# Numerical derivative
dt = t_values[i+1] - t_values[i-1]
dA_dt = (attention_densities[i+1] - attention_densities[i-1]) / dt
# Expected: dA/dt = -A(t) * ln_ratio (negative because attention decreases)
expected_derivative = -attention_densities[i] * ln_ratio
relative_error = abs(dA_dt - expected_derivative) / abs(expected_derivative) if expected_derivative != 0 else 1
if relative_error > 0.15: # Allow 15% numerical error for derivative approximation
exponential_decay_valid = False
if i < 5: # Print first few for debugging
print(f" t={t_values[i]:.2f}: dA/dt={dA_dt:.3f}, "
f"expected={expected_derivative:.3f}, error={relative_error:.2%}")
print(f"Exponential decay rate: {'✅' if exponential_decay_valid else '❌'}")
# Test focusing factor: ratio of attention at bottom vs top (bottom has higher attention)
focusing_ratio = attention_densities[0] / attention_densities[-1] if attention_densities[-1] > 0 else 0
expected_ratio = (self.helix.top_radius / self.helix.bottom_radius) # R_top/R_bottom ratio
focusing_valid = focusing_ratio > 100 # Should show significant focusing at bottom
print(f"Attention at bottom (t=0.1): {attention_densities[0]:.6f}")
print(f"Attention at top (t=0.9): {attention_densities[-1]:.6f}")
print(f"Focusing ratio (A_bottom/A_top): {focusing_ratio:.1f}")
print(f"Significant focusing (>100x): {'✅' if focusing_valid else '❌'}")
self.validation_results['attention_focusing'] = {
'monotonic_decrease': monotonic_decrease,
'exponential_decay': exponential_decay_valid,
'significant_focusing': focusing_valid,
'overall': monotonic_decrease and exponential_decay_valid and focusing_valid
}
return monotonic_decrease and exponential_decay_valid and focusing_valid
def validate_communication_complexity(self) -> bool:
"""
Validate communication complexity from hypothesis_mathematics.md Section H2.
Tests:
1. O(N) scaling for spoke-based system
2. Message count bounds
3. Maximum communication distance
"""
print("\n=== Validating Communication Complexity ===")
# Test scaling with different agent counts
agent_counts = [10, 20, 50, 100]
message_counts = []
processing_times = []
for N in agent_counts:
central_post = CentralPost(max_agents=N, enable_metrics=True)
agents = create_openscad_agents(self.helix, number_of_nodes=N, random_seed=42069)
# Register agents and time the operation
start_time = time.perf_counter()
for agent in agents:
central_post.register_agent(agent)
registration_time = time.perf_counter() - start_time
processing_times.append(registration_time)
message_counts.append(N) # Each agent creates one connection
# Test O(N) scaling - processing time should be roughly linear
linear_scaling_valid = True
for i in range(1, len(agent_counts)):
ratio_agents = agent_counts[i] / agent_counts[i-1]
ratio_time = processing_times[i] / processing_times[i-1]
# Allow factor of 3 deviation from linear scaling
if ratio_time > 3 * ratio_agents or ratio_time < ratio_agents / 3:
linear_scaling_valid = False
print(f" N={agent_counts[i]}: time={processing_times[i]:.6f}s, "
f"scaling ratio={ratio_time:.2f} (expected ≈{ratio_agents:.2f})")
print(f"Linear scaling O(N): {'✅' if linear_scaling_valid else '❌'}")
# Test maximum communication distance
max_spoke_distance = self.helix.top_radius
actual_distances = []
for t in np.linspace(0, 1, 100):
pos = self.helix.get_position(t)
# Distance from agent to central axis at same height
distance = math.sqrt(pos[0]**2 + pos[1]**2) # Should equal R(t)
actual_distances.append(distance)
max_actual_distance = max(actual_distances)
distance_bound_valid = abs(max_actual_distance - max_spoke_distance) < self.tolerance
print(f"Maximum spoke distance: {max_actual_distance:.6f} "
f"(expected {max_spoke_distance:.6f})")
print(f"Distance bound valid: {'✅' if distance_bound_valid else '❌'}")
# Test message complexity O(N) vs theoretical O(N²) mesh
complexity_advantage = True
for N in agent_counts:
spoke_messages = N # Each agent to central post
mesh_messages = N * (N - 1) // 2 # All pairs
efficiency_ratio = mesh_messages / spoke_messages
expected_ratio = (N - 1) / 2
if abs(efficiency_ratio - expected_ratio) > 0.1:
complexity_advantage = False
print(f" N={N}: spoke={spoke_messages}, mesh={mesh_messages}, "
f"advantage={efficiency_ratio:.1f}x")
print(f"Message complexity advantage: {'✅' if complexity_advantage else '❌'}")
self.validation_results['communication_complexity'] = {
'linear_scaling': linear_scaling_valid,
'distance_bounds': distance_bound_valid,
'complexity_advantage': complexity_advantage,
'overall': linear_scaling_valid and distance_bound_valid and complexity_advantage
}
return linear_scaling_valid and distance_bound_valid and complexity_advantage
def validate_numerical_precision(self) -> bool:
"""
Validate numerical precision and stability.
Tests:
1. Consistency with OpenSCAD validation
2. Numerical stability across parameter ranges
3. Error accumulation in iterative calculations
"""
print("\n=== Validating Numerical Precision ===")
# Test against OpenSCAD validation (should be < 1e-12)
from validate_openscad import validate_implementation
openscad_valid = validate_implementation()
print(f"OpenSCAD validation: {'✅' if openscad_valid else '❌'}")
# Test numerical stability at extreme parameter values
stability_valid = True
# Test very small t values
small_t_values = [1e-10, 1e-8, 1e-6, 1e-4]
for t in small_t_values:
try:
pos = self.helix.get_position(t)
# Should be close to (R_bottom, 0, 0)
if not all(math.isfinite(x) for x in pos):
stability_valid = False
except Exception:
stability_valid = False
# Test values very close to 1
large_t_values = [1 - 1e-10, 1 - 1e-8, 1 - 1e-6, 1 - 1e-4]
for t in large_t_values:
try:
pos = self.helix.get_position(t)
if not all(math.isfinite(x) for x in pos):
stability_valid = False
except Exception:
stability_valid = False
print(f"Numerical stability: {'✅' if stability_valid else '❌'}")
# Test error accumulation in arc length calculation
# Compare high-precision vs standard precision
arc_length_high = self.helix.approximate_arc_length(segments=10000)
arc_length_std = self.helix.approximate_arc_length(segments=1000)
relative_error = abs(arc_length_high - arc_length_std) / arc_length_high
error_acceptable = relative_error < 0.01 # 1% tolerance for numerical approximation
print(f"Arc length error: {relative_error:.4%} (high vs standard precision)")
print(f"Error accumulation acceptable: {'✅' if error_acceptable else '❌'}")
self.validation_results['numerical_precision'] = {
'openscad_validation': openscad_valid,
'numerical_stability': stability_valid,
'error_accumulation': error_acceptable,
'overall': openscad_valid and stability_valid and error_acceptable
}
return openscad_valid and stability_valid and error_acceptable
def run_full_validation(self) -> bool:
"""Run complete mathematical validation suite."""
print("=" * 60)
print("FELIX FRAMEWORK MATHEMATICAL VALIDATION")
print("=" * 60)
validators = [
('Parametric Equations', self.validate_parametric_equations),
('Geometric Properties', self.validate_geometric_properties),
('Agent Distribution', self.validate_agent_distribution),
('Attention Focusing', self.validate_attention_focusing),
('Communication Complexity', self.validate_communication_complexity),
('Numerical Precision', self.validate_numerical_precision),
]
all_valid = True
results_summary = []
for name, validator in validators:
try:
result = validator()
results_summary.append((name, result))
if not result:
all_valid = False
except Exception as e:
print(f"ERROR in {name}: {e}")
results_summary.append((name, False))
all_valid = False
# Print summary
print("\n" + "=" * 60)
print("VALIDATION SUMMARY")
print("=" * 60)
for name, result in results_summary:
status = "✅ PASSED" if result else "❌ FAILED"
print(f"{name:<25}: {status}")
overall_status = "✅ ALL VALIDATIONS PASSED" if all_valid else "❌ SOME VALIDATIONS FAILED"
print(f"\nOverall Result: {overall_status}")
if all_valid:
print("\n🎉 Mathematical implementation is validated!")
print(" Ready for hypothesis testing and research publication.")
else:
print("\n⚠️ Mathematical validation failures detected.")
print(" Review implementation before proceeding with experiments.")
return all_valid
if __name__ == "__main__":
validator = MathematicalValidator(tolerance=1e-10)
success = validator.run_full_validation()
# Save validation results for research documentation
import json
# Convert numpy booleans to regular booleans for JSON serialization
def convert_numpy_types(obj):
if hasattr(obj, 'item'):
return obj.item()
elif isinstance(obj, dict):
return {k: convert_numpy_types(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_numpy_types(v) for v in obj]
return obj
serializable_results = convert_numpy_types(validator.validation_results)
with open("mathematical_validation_results.json", "w") as f:
json.dump(serializable_results, f, indent=2)
print(f"\nValidation results saved to: mathematical_validation_results.json")
exit(0 if success else 1)