Development Guide
Guide for developing, debugging, and verifying ZeroProofML models.
Development Setup
Clone and Install
git clone https://github.com/domezsolt/zeroproofml.git
cd zeroproofml
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev,torch]"
Run Tests
# All tests
pytest
# Specific module
pytest tests/scm/
# With coverage
pytest --cov=zeroproof --cov-report=html
# Fast (skip slow tests)
pytest -m "not slow"
Projective Training Internals
Tuple Lifecycle
Understanding how ⟨N,D⟩ tuples flow through training:
1. Encoding
from zeroproof.autodiff.projective import encode
# Finite: x → ⟨x, 1⟩
sample = encode(scm_real(3.14))
assert sample.numerator == 3.14
assert sample.denominator == 1.0
# Bottom: ⊥ → ⟨1, 0⟩
bottom_sample = encode(scm_bottom())
assert bottom_sample.denominator == 0.0
2. Detached Renormalization
from zeroproof.autodiff.projective import renormalize
# Prevent gradients leaking through norm
N, D = renormalize(N, D, gamma=1e-9)
# Equivalent to:
# S = stop_gradient(√(N² + D²) + γ)
# N', D' = N/S, D/S
3. Gradient Flow
Autograd runs on the renormalized tuples. Losses should use decoded values after renormalization:
# ✅ Correct
N, D = renormalize(N, D)
decoded = decode(N, D)
loss = mse(decoded, targets)
# ❌ Wrong (biases tuple scale)
decoded = decode(N, D) # Before renorm
loss = mse(decoded, targets)
N, D = renormalize(N, D)
4. Decoding
from zeroproof.autodiff.projective import decode
# Inference: strict threshold
decoded = decode(sample, tau=1e-6)
Denominator Anchoring
For finite-target regression, anchor denominators near 1:
# Bias toward finite chart
Q = 1.0 + delta_Q # delta_Q learned
P, Q = renormalize(P, Q)
# This prevents drift to tiny-but-nonzero D values
# that amplify decoded ratios
Minimal Training Skeleton
from zeroproof.autodiff.projective import encode, renormalize, decode
# Forward
N, D = model(x) # Model outputs projective tuple
N, D = renormalize(N, D, gamma=1e-9)
# Decode for loss
decoded = decode(N, D, tau=1e-4)
loss = loss_fn(decoded, targets)
# Backward
loss.backward()
optimizer.step()
Debug Logging
Python Logging
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("zeroproof").setLevel(logging.DEBUG)
Per-Step Metrics
Capture training metrics with log_hook:
import json
from pathlib import Path
from zeroproof.training import TrainingConfig
log_path = Path("runs/metrics.jsonl")
log_path.parent.mkdir(parents=True, exist_ok=True)
def log_hook(metrics):
with log_path.open("a") as f:
f.write(json.dumps(metrics) + "\n")
config = TrainingConfig(log_hook=log_hook)
Available metrics:
loss: Total training losscoverage: Fraction of non-⊥ predictionsepoch: Current epochstep: Global step count
Debugging ⊥ Propagation
Inspect bottom masks:
from zeroproof.layers import SCMRationalLayer
layer = SCMRationalLayer(3, 2)
output, bottom_mask = layer(x)
print(f"Bottom rate: {bottom_mask.float().mean():.3f}")
print(f"Bottom indices: {bottom_mask.nonzero().squeeze()}")
Log payload and mask:
# For vectorized ops
result, mask = scm_div_torch(N, D, N_mask, D_mask)
logger.debug(f"Payload: {result}")
logger.debug(f"Mask: {mask}")
logger.debug(f"Coverage: {(~mask).float().mean():.3f}")
Verification
SCM Semantics
Verify meadow axioms hold:
from zeroproof.scm.ops import scm_add, scm_mul, scm_div
from zeroproof.scm.value import scm_real, scm_bottom
# Absorption
x = scm_real(5.0)
b = scm_bottom()
assert scm_add(x, b).is_bottom
assert scm_mul(x, b).is_bottom
# Total inverse
assert scm_div(x, scm_real(0.0)).is_bottom
# Identity
assert scm_add(x, scm_real(0.0)).value == 5.0
assert scm_mul(x, scm_real(1.0)).value == 5.0
Gradient Policy Verification
from zeroproof.autodiff.policies import apply_policy, GradientPolicy
# CLAMP zeroes ⊥ gradients
grad = apply_policy(gradient=2.0, is_bottom=True, policy=GradientPolicy.CLAMP)
assert grad == 0.0
# PROJECT masks ⊥ paths
grad = apply_policy(gradient=2.0, is_bottom=True, policy=GradientPolicy.PROJECT)
assert grad == 0.0
# PASSTHROUGH preserves
grad = apply_policy(gradient=2.0, is_bottom=True, policy=GradientPolicy.PASSTHROUGH)
assert grad == 2.0
Coverage Verification
Test coverage on held-out data:
from zeroproof.losses.coverage import coverage_metric
model.eval()
with torch.no_grad():
_, bottom_mask, _ = model(test_data)
coverage = coverage_metric(bottom_mask)
assert coverage >= 0.95, f"Coverage too low: {coverage:.3f}"
Common Issues
High ⊥ Rate During Training
Symptoms: Coverage drops below 50%
Debug steps:
-
Check target lifting:
Y_n, Y_d = lift_targets(targets) print(f"Singular targets: {(Y_d.abs() < 1e-3).sum()}") -
Verify margin loss is active:
loss_margin = margin_loss(D, tau_train=1e-4) print(f"Margin loss: {loss_margin.item():.6f}") -
Increase rejection loss weight:
loss_fn = SCMTrainingLoss(lambda_rejection=0.1) # Was 0.01
NaN in Gradients
Symptoms: loss.backward() produces NaN
Debug steps:
-
Check renormalization:
# Ensure gamma > 0 N, D = renormalize(N, D, gamma=1e-9) -
Verify gradient policy:
with gradient_policy(GradientPolicy.PROJECT): loss.backward() -
Enable gradient clipping:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
Inconsistent Signs
Symptoms: +∞ and -∞ predictions unstable
Debug steps:
-
Enable sign consistency loss:
loss_sign = sign_consistency_loss(N, D, Y_n, Y_d, tau_sing=1e-3) -
Check weak sign operator:
from zeroproof.scm.sign import weak_sign signs = weak_sign(values, mask, tau_sign=1e-6) -
Verify singular targets are correctly lifted
Testing Patterns
Unit Test Structure
import pytest
import torch
from zeroproof.layers import SCMRationalLayer
class TestSCMRational:
def test_finite_inputs(self):
layer = SCMRationalLayer(3, 2)
x = torch.randn(32, 10)
output, bottom_mask = layer(x)
assert output.shape == (32, 1)
assert bottom_mask.shape == (32, 1)
assert not bottom_mask.all() # Not all bottom
def test_singular_inputs(self):
layer = SCMRationalLayer(3, 2)
x = torch.zeros(32, 10) # Might trigger ⊥
output, bottom_mask = layer(x)
# Verify bottom propagation
if bottom_mask.any():
assert torch.isnan(output[bottom_mask]).all()
Integration Tests
def test_end_to_end_training():
# Setup
model = create_model()
optimizer = torch.optim.Adam(model.parameters())
loss_fn = SCMTrainingLoss()
# Generate data
x = torch.linspace(-1, 1, 100).unsqueeze(-1)
y = 1.0 / (x + 0.1) # Has singularity
# Train
for epoch in range(10):
N, D = model(x)
loss = loss_fn((N, D), lift_targets(y))
loss.backward()
optimizer.step()
# Verify
model.eval()
with torch.no_grad():
N, D = model(x)
decoded, bottom, gap = strict_inference(N, D)
coverage = (~bottom & ~gap).float().mean()
assert coverage > 0.8
Contribution Guidelines
Code Style
# Format
black zeroproof/ tests/
# Lint
flake8 zeroproof/
# Type check
mypy zeroproof/
Pre-Commit Hooks
pip install pre-commit
pre-commit install
Pull Requests
- Branch from
main - Add tests for new features
- Update documentation
- Ensure CI passes
- Request review
Next Steps
- Performance Guide - Optimize your models
- Experiments - Benchmark protocols
- API Reference - Full API documentation