230 lines
15 KiB
Python
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()
|