Files
digital-drosophila/models/male-cns-lif/olfactory-conditioning-lab-v1/test_conditioning_preflight_inputs.py
T
2026-09-30 17:36:27 +08:00

230 lines
15 KiB
Python

"""Pure/synthetic preflight tests: no actual rates, CUDA draws, or brain run."""
import copy
import hashlib
import io
import json
from pathlib import Path
import tempfile
import types
import unittest
from unittest.mock import patch
import numpy as np
import pandas as pd
import conditioning_preflight_inputs as p
def synthetic_tables():
names = ['synthetic-' + str(i) for i in range(12)]
names[6], names[7] = '4-ethylphenol', '3-hexanone'
ids = ['synthetic-id-' + str(i) for i in range(12)]
panel = pd.DataFrame(dict(odor_id=ids, odor_name=names,
split=['calibration'] * 6 + ['heldout'] * 6, mapped_response_sum=np.arange(1, 13, dtype=float),
formal_label=[''] * 6 + ['ODOR_A', 'ODOR_B'] + [''] * 4))
selection = dict(selected_before_KC_simulation=True, glomeruli_order=['G2', 'G1'],
ODOR_A=dict(odor_id=ids[6], odor_name=names[6]), ODOR_B=dict(odor_id=ids[7], odor_name=names[7]))
vectors = {oid: np.array([.5, .25], np.float32) for oid in ids}
mapping = pd.DataFrame(dict(glomerulus=['G2', 'G2', 'G2', 'G1', 'G1'],
mapping_confidence=['exact', 'alias-normalized', 'exact', 'exact', 'unmatched'],
neuron_idx=[19., 3., 19., 8., 100.]))
config = dict(odor_encoder=dict(max_total_rate_per_glomerulus_hz=400., modes=['physiological', 'matched_energy']))
return selection, panel, vectors, mapping, config
def sparse(rows):
values = np.asarray(rows, np.int32).reshape(-1, 2)
return {'tick': values[:, 0].copy(), 'neuron_idx': values[:, 1].copy()}
class PreflightTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.plan = p.build_preflight_plan()
cls.epoch = cls.plan['selected_epochs'][0]
source_parent = Path(__file__).resolve().parent.parent
candidates = (source_parent / 'phase4l.py', source_parent / 'models/male-cns-lif/phase4l.py')
path = next((candidate for candidate in candidates if candidate.is_file()), None)
if path is None:
raise FileNotFoundError('Pinned L source missing at both known server/local paths: ' + repr(candidates))
cls.source_raw = path.read_bytes()
if hashlib.sha256(cls.source_raw).hexdigest() != p.SOURCE_PINS[p.SOURCE_REL]:
raise ValueError('Selected server/local L source differs from the frozen SHA')
cls.drive, cls.ast_sha = p._extract_drive(cls.source_raw, str(path))
cls.tables, _ = p._rate_tables(*synthetic_tables(), cls.drive)
def test_fixed_full_plan_master_first_two_real_epochs_and_12_calls(self):
full = self.plan['full_trial_plan']
self.assertEqual(full['plan']['master'], 20260930)
self.assertEqual(full['payload_sha256'], 'a8e8620875e8213f3cc3b4f153fc5c06e1351bf1d91c7edaf4f18c1e2cdef62e')
self.assertEqual([e['epoch_index'] for e in self.plan['selected_epochs']], [0, 1])
self.assertEqual([e['cs']['cue'] for e in self.plan['selected_epochs']], ['A', 'A'])
self.assertEqual([e['cs']['seed'] for e in self.plan['selected_epochs']], [5717173809506784725, 4342084263790487177])
self.assertTrue(all(e['us']['requested'] for e in self.plan['selected_epochs']))
self.assertEqual(self.plan['total_calls'], 12)
self.assertEqual(sum(r['steps'] for r in self.plan['calls']), 14400)
self.assertEqual(len({r['row_id'] for r in self.plan['calls']}), 12)
self.assertFalse(self.plan['complete_conditioning_authorized'])
def test_plan_rows_share_inputs_but_keep_D_and_freeze_distinct(self):
for row in self.plan['calls']:
self.assertEqual(row['epoch_id'], self.plan['selected_epochs'][row['input_epoch_index']]['epoch_id'])
self.assertEqual(row['d_command'], 0. if row['arm'] == 'D0' else 1.)
self.assertEqual(row['frozen'], row['arm'] == 'frozen')
self.assertEqual(row['previous_delta_row_id'], None if row['input_epoch_index'] == 0 else
f"{row['arm']}--{row['diagnostic_mode']}--epoch-000")
self.assertEqual(self.plan['input_generation_calls'], 2)
def test_whole_plan_validator_rejects_wrong_master_and_row_mutation(self):
# Reuse an already independently rebuilt expected fixture here; the
# production validator calls its full builder, not a count-only gate.
with patch.object(p, 'build_preflight_plan', return_value=self.plan) as rebuild:
self.assertTrue(p.validate_preflight_plan(copy.deepcopy(self.plan)))
bad = copy.deepcopy(self.plan); bad['master'] = 461
with self.assertRaises(ValueError): p.validate_preflight_plan(bad)
bad = copy.deepcopy(self.plan); bad['calls'][0]['d_command'] = 0.
with self.assertRaises(ValueError): p.validate_preflight_plan(bad)
self.assertEqual(rebuild.call_count, 3)
def test_actual_frozen_function_AST_preserves_mapping_order_rates_and_dtype(self):
table = self.tables['A']
np.testing.assert_array_equal(table['targets'], [19, 3, 8])
self.assertEqual(table['targets'].dtype, np.int32)
np.testing.assert_array_equal(table['rates_hz'], np.array([100, 100, 100], np.float32))
self.assertEqual(table['source_panel_row'], 6)
self.assertEqual(table['source_panel_split'], 'heldout') # original static label only
self.assertFalse(table['targets'].flags.writeable)
self.assertFalse(table['rates_hz'].flags.writeable)
self.assertEqual(hashlib.sha256(self.source_raw).hexdigest(), p.SOURCE_PINS[p.SOURCE_REL])
self.assertEqual(len(self.ast_sha), 64)
def test_static_mapping_fails_closed_on_selection_order_schema_or_duplicate_PN(self):
for kind in ('name', 'split', 'vector', 'duplicate'):
args = list(synthetic_tables())
if kind == 'name': args[0]['ODOR_A']['odor_name'] = 'different'
if kind == 'split': args[1].loc[0, 'split'] = 'heldout'
if kind == 'vector': args[2][args[0]['ODOR_A']['odor_id']] = np.array([np.nan, .25], np.float32)
if kind == 'duplicate': args[3].loc[3, 'neuron_idx'] = 19.
with self.assertRaises(ValueError): p._rate_tables(*args, type(self).drive)
def test_loader_checks_all_static_bytes_then_uses_original_AST_no_old_import(self):
selection, panel, vectors, mapping, config = synthetic_tables()
buffer = io.BytesIO(); np.savez(buffer, glomeruli=np.array(selection['glomeruli_order']), **vectors)
payload = {
p.SOURCE_REL: self.source_raw, p.CONFIG_REL: json.dumps(config).encode(),
p.STATIC + 'odor-encoder/frozen-odor-panel.parquet': b'SYNTHETIC-PANEL',
p.STATIC + 'odor-encoder/mapped-glomerular-vectors.npz': buffer.getvalue(),
p.STATIC + 'odor-encoder/selection.json': json.dumps(selection).encode(),
p.STATIC + 'receptor-mapping/olfactory-mapping.parquet': b'SYNTHETIC-MAPPING',
}
pins = {k: hashlib.sha256(v).hexdigest() for k, v in payload.items()}
with tempfile.TemporaryDirectory() as td:
for name, raw in payload.items():
target = Path(td) / name; target.parent.mkdir(parents=True, exist_ok=True); target.write_bytes(raw)
# Synthetic config is JSON (a YAML subset); local runtime lacks
# PyYAML. This mocks parsing only, not any scientific input/RNG.
yaml_mock = types.SimpleNamespace(safe_load=json.loads)
with patch.object(p, 'SOURCE_PINS', pins), patch.object(pd, 'read_parquet', side_effect=[panel, mapping]), \
patch.dict('sys.modules', {'yaml': yaml_mock}):
result = p.load_rate_tables(td)
self.assertEqual(result['audit']['static_sources'], pins)
self.assertFalse(result['audit']['historical_dynamic_results_read'])
self.assertEqual(result['audit']['random_draws'], 0)
self.assertEqual(result['tables']['B']['odor_name'], '3-hexanone')
(Path(td) / p.CONFIG_REL).write_bytes(b'CHANGED')
with patch.object(p, 'SOURCE_PINS', pins), patch.object(pd, 'read_parquet') as parse, \
patch.dict('sys.modules', {'yaml': yaml_mock}), self.assertRaises(ValueError):
p.load_rate_tables(td)
parse.assert_not_called()
def test_generate_delegates_full_shape_full_seed_and_shared_US_without_real_draw(self):
fake = dict(requests=sparse([[2, 19], [999, 8]]), audit={'synthetic_sampler': True})
dan = np.array([80, 90], np.int32)
with patch.object(p.inputs, 'generate_cs_requests', return_value=fake) as sampler:
result = p.generate_epoch_inputs(self.epoch, self.tables, dan, device='cuda:1', max_uniform_draws=3600)
args, kwargs = sampler.call_args
self.assertIs(args[0], self.tables['A']['targets'])
self.assertEqual(kwargs, dict(seed=5717173809506784725, steps=1200, cs_start_tick=0, cs_stop_tick=1000,
device='cuda:1', max_uniform_draws=3600))
np.testing.assert_array_equal(result['requests']['tick'], [2, 900, 900, 999])
np.testing.assert_array_equal(result['requests']['neuron_idx'], [19, 80, 90, 8])
self.assertTrue(result['audit']['US_shared_across_arms'])
self.assertFalse(result['audit']['brain_reset_called'])
def test_explicit_conflict_union_and_default_error_preserve_US_attempts(self):
fake = dict(requests=sparse([[900, 19]]), audit={'synthetic_sampler': True})
with patch.object(p.inputs, 'generate_cs_requests', return_value=fake):
with self.assertRaises(p.inputs.RequestConflictError):
p.generate_epoch_inputs(self.epoch, self.tables, np.array([19], np.int32), device='cuda:0', max_uniform_draws=3600)
value = p.generate_epoch_inputs(self.epoch, self.tables, np.array([19], np.int32),
device='cuda:0', max_uniform_draws=3600, conflict_policy='boolean_union')
self.assertEqual(value['audit']['composition']['cross_source_conflict_count'], 1)
np.testing.assert_array_equal(value['source_mask'], [3])
def test_no_US_epoch_still_draws_CS_but_no_DAN_pulse(self):
epoch = copy.deepcopy(self.epoch)
epoch['us'].update(requested=False, tick=None, request_rule='none'); epoch['D_command'] = 0.
with patch.object(p.inputs, 'generate_cs_requests', return_value=dict(requests=sparse([]), audit={})):
value = p.generate_epoch_inputs(epoch, self.tables, np.array([80], np.int32), device='cuda:0', max_uniform_draws=3600)
self.assertEqual(len(value['us_requests']['tick']), 0)
def test_invalid_targets_and_mutated_rates_rejected_before_sampling(self):
with patch.object(p.inputs, 'generate_cs_requests') as sampler:
for dan in (np.array([2, 2], np.int32), np.array([9, 2], np.int32), np.array([], np.int32), np.array([2], np.int64)):
with self.assertRaises(ValueError):
p.generate_epoch_inputs(self.epoch, self.tables, dan, device='cuda:0', max_uniform_draws=3600)
tables = copy.deepcopy(self.tables); tables['A']['rates_hz'][0] = 3.
with self.assertRaises(ValueError):
p.generate_epoch_inputs(self.epoch, tables, np.array([80], np.int32), device='cuda:0', max_uniform_draws=3600)
sampler.assert_not_called()
def original_spec(self):
return dict(name='old-case', kind='fullgraph', steps=1000, dt_ms=.1, duration_ms=100.,
model={'immutable': 'synthetic model'}, graph_dir='/old/graph', apl_bundle_dir='/old/apl',
coefficients_path='/old/coeff.json', science_python='/old/venv/bin/python',
apl_delay_steps=18, precision='float64', apl_precision='float32', record_history=False,
training=False, validation_cohort='old_cohort', odor_index=0, expected_external_events=123.,
odor_id='old-id', odor_name='old-name', source_panel_row=0, source_panel_split='calibration',
rate_sum_hz=999., target_count=100, seed=22, budget_root='/old/experiment', output_budget_bytes=100)
def test_derive_spec_changes_only_explicit_input_scope_and_clock_budget_fields(self):
old = self.original_spec(); before = copy.deepcopy(old)
value, audit = p.derive_case_spec(old, self.plan['calls'][0], self.epoch, self.tables['A'],
brain_reset_seed=20260930, budget_root='/new/preflight', output_budget_bytes=10000)
self.assertEqual(old, before)
self.assertEqual((value['steps'], value['duration_ms'], value['max_steps']), (1200, 120., 1200))
self.assertEqual(value['seed'], 20260930)
self.assertEqual(value['cs_input_seed'], 5717173809506784725)
for key in ('model', 'graph_dir', 'apl_bundle_dir', 'coefficients_path', 'science_python'):
self.assertEqual(value[key], old[key])
self.assertFalse(value['within_trial_weight_updates'])
self.assertEqual(value['odor_name'], '4-ethylphenol')
self.assertNotIn('validation_cohort', value); self.assertNotIn('training', value)
self.assertEqual(set(audit['removed_stale_input_metadata']), {'validation_cohort', 'training', 'odor_index', 'expected_external_events'})
self.assertFalse(audit['full_conditioning_authorized'])
json.dumps(audit, allow_nan=False)
def test_seed_or_neural_continuation_or_budget_alias_invalid(self):
args = dict(brain_reset_seed=3, budget_root='/new/preflight', output_budget_bytes=10000)
for seed in (-1, 2**31, self.epoch['cs']['seed'], True, np.int32(3)):
with self.assertRaises(ValueError):
p.derive_case_spec(self.original_spec(), self.plan['calls'][0], self.epoch, self.tables['A'], **{**args, 'brain_reset_seed': seed})
for root in ('/old/experiment/new', '/old', '/new/../old', 'relative'):
with self.assertRaises(ValueError):
p.derive_case_spec(self.original_spec(), self.plan['calls'][0], self.epoch, self.tables['A'], **{**args, 'budget_root': root})
old = self.original_spec(); old['parent_checkpoint_manifest_sha256'] = 'a' * 64
with self.assertRaises(ValueError): p.derive_case_spec(old, self.plan['calls'][0], self.epoch, self.tables['A'], **args)
def test_row_command_mismatch_and_rate_label_mismatch_fail_closed(self):
row = copy.deepcopy(self.plan['calls'][0]); row['d_command'] = 0.
with self.assertRaises(ValueError):
p.derive_case_spec(self.original_spec(), row, self.epoch, self.tables['A'],
brain_reset_seed=3, budget_root='/new/preflight', output_budget_bytes=10000)
with self.assertRaises(ValueError):
p.derive_case_spec(self.original_spec(), self.plan['calls'][0], self.epoch, self.tables['B'],
brain_reset_seed=3, budget_root='/new/preflight', output_budget_bytes=10000)
if __name__ == '__main__':
unittest.main()