bio-experiment / tests /test_simulator.py
Ev3Dev's picture
Upload folder using huggingface_hub
4db0438 verified
"""Tests for the latent-state simulator modules."""
import pytest
from models import ActionType, ExperimentAction, OutputType
from server.simulator.latent_state import (
CellPopulation,
ExperimentProgress,
FullLatentState,
LatentBiologicalState,
ResourceState,
TechnicalState,
)
from server.simulator.noise import NoiseModel
from server.simulator.output_generator import OutputGenerator
from server.simulator.transition import TransitionEngine
def _make_state() -> FullLatentState:
return FullLatentState(
biology=LatentBiologicalState(
cell_populations=[
CellPopulation(name="A", proportion=0.6, marker_genes=["G1"]),
CellPopulation(name="B", proportion=0.4, marker_genes=["G2"]),
],
true_de_genes={"disease_vs_healthy": {"G1": 2.0, "G2": -1.5}},
true_pathways={"apoptosis": 0.7},
true_markers=["G1"],
causal_mechanisms=["G1-driven apoptosis"],
n_true_cells=5000,
),
technical=TechnicalState(dropout_rate=0.1, doublet_rate=0.04),
progress=ExperimentProgress(),
resources=ResourceState(budget_total=50_000, time_limit_days=90),
)
class TestNoiseModel:
def test_deterministic_with_seed(self):
n1 = NoiseModel(seed=42)
n2 = NoiseModel(seed=42)
assert n1.sample_qc_metric(0.5, 0.1) == n2.sample_qc_metric(0.5, 0.1)
def test_false_positives(self):
n = NoiseModel(seed=0)
fps = n.generate_false_positives(1000, 0.01)
assert all(g.startswith("FP_GENE_") for g in fps)
def test_quality_degradation_bounded(self):
n = NoiseModel(seed=0)
for _ in range(100):
q = n.quality_degradation(0.9, [0.8, 0.7])
assert 0.0 <= q <= 1.0
class TestOutputGenerator:
def test_collect_sample(self):
noise = NoiseModel(seed=1)
gen = OutputGenerator(noise)
s = _make_state()
action = ExperimentAction(
action_type=ActionType.COLLECT_SAMPLE,
parameters={"n_samples": 4},
)
out = gen.generate(action, s, 1)
assert out.output_type == OutputType.SAMPLE_COLLECTION_RESULT
assert out.data["n_samples"] == 4
def test_de_includes_true_genes(self):
noise = NoiseModel(seed=42)
gen = OutputGenerator(noise)
s = _make_state()
s.progress.data_normalized = True
action = ExperimentAction(
action_type=ActionType.DIFFERENTIAL_EXPRESSION,
parameters={"comparison": "disease_vs_healthy"},
)
out = gen.generate(action, s, 5)
assert out.output_type == OutputType.DE_RESULT
gene_names = [g["gene"] for g in out.data["top_genes"]]
assert "G1" in gene_names or "G2" in gene_names
class TestTransitionEngine:
def test_progress_flags_set(self):
noise = NoiseModel(seed=0)
engine = TransitionEngine(noise)
s = _make_state()
action = ExperimentAction(action_type=ActionType.COLLECT_SAMPLE)
result = engine.step(s, action)
assert result.next_state.progress.samples_collected is True
def test_hard_violation_blocks(self):
noise = NoiseModel(seed=0)
engine = TransitionEngine(noise)
s = _make_state()
result = engine.step(
s,
ExperimentAction(action_type=ActionType.COLLECT_SAMPLE),
hard_violations=["test_block"],
)
assert result.output.success is False
assert result.output.output_type == OutputType.FAILURE_REPORT
def test_resource_deduction(self):
noise = NoiseModel(seed=0)
engine = TransitionEngine(noise)
s = _make_state()
action = ExperimentAction(action_type=ActionType.SEQUENCE_CELLS)
s.progress.library_prepared = True
result = engine.step(s, action)
assert result.next_state.resources.budget_used == 15_000
def test_conclusion_ends_episode(self):
noise = NoiseModel(seed=0)
engine = TransitionEngine(noise)
s = _make_state()
s.progress.de_performed = True
action = ExperimentAction(action_type=ActionType.SYNTHESIZE_CONCLUSION)
result = engine.step(s, action)
assert result.done is True