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

222 lines
14 KiB
Python

"""UNREGISTERED input consumer; no I/O, brain execution, or training.
The frozen L/AK interface is discrete float32 Bernoulli (one request per PN
and tick), not a continuous-time Poisson sampler. PN/rate mapping is supplied
and externally authenticated by the caller; this module never selects odors,
rebuilds mappings, rescales rates, clips probabilities, or resets brain RNGs.
generate_cs_requests lazily uses the original CUDA Torch random operation.
CPU mocks test its contract only, not CUDA sequence equivalence. A full
(steps, PN-columns) matrix is drawn once, then the explicit half-open CS
window masks requests. A 100 ms CS in a 120 ms epoch is NOT the full old L
120 ms input. Different draw shapes need not share identical random prefixes.
The caller must register source/rate authority, device/environment, budgets,
stream aliases, and a separate bounded CUDA equivalence test before real use.
Returned request/source arrays are owned read-only copies, not immutable
capabilities; the consumer must recheck their recorded hashes before use.
"""
from __future__ import annotations
import hashlib
import re
import numpy as np
N_NEURONS = 166700
L_SOURCE_SHA = 'e97d58d8c1f44920d0e953f8f5831ddc86fa0198784e86460e315de4116ac553'
AK_ENCODER_SHA = '0061689abb1832e963718bd836bc8a50009ee38b258143a753b4d59e63414925'
def require(value, message):
if not value:
raise ValueError(message)
def array_identity(value):
return dict(dtype=value.dtype.str, shape=list(value.shape), order='C-logical-bytes',
sha256=hashlib.sha256(value.tobytes(order='C')).hexdigest())
def _vector(value, dtype, name):
require(isinstance(value, np.ndarray) and value.dtype == np.dtype(dtype) and value.ndim == 1,
name + ' must be an exact one-dimensional ' + np.dtype(dtype).name + ' array')
def _targets(value):
_vector(value, np.int32, 'targets')
require(np.all((value >= 0) & (value < N_NEURONS)), 'Target outside canonical global neuron range')
require(len(np.unique(value)) == len(value), 'Duplicate PN columns rejected; never merge rates or deduplicate RNG columns')
def _clock(steps, start, stop):
require(type(steps) is int and 0 < steps <= np.iinfo(np.int32).max, 'steps must be a positive int32-range integer')
require(type(start) is type(stop) is int and 0 <= start <= stop <= steps, 'Invalid explicit half-open CS tick window')
def probabilities_f32(targets, rates_hz, *, dt_ms=0.1):
"""Preserve the L expression's separate float32 multiply then divide."""
_targets(targets); _vector(rates_hz, np.float32, 'rates_hz')
require(rates_hz.shape == targets.shape and np.all(np.isfinite(rates_hz)) and np.all(rates_hz >= 0), 'Invalid PN rate vector')
require(type(dt_ms) in (int, float) and dt_ms == 0.1, 'Only the frozen dt_ms=0.1 contract is supported')
probability = np.divide(np.multiply(rates_hz, np.float32(dt_ms), dtype=np.float32), np.float32(1000.0), dtype=np.float32)
original = rates_hz * dt_ms / 1000.0
require(original.dtype == np.float32 and original.tobytes() == probability.tobytes(), 'Installed NumPy changed the frozen rate expression')
require(np.all(np.isfinite(probability)) and np.all((probability >= 0) & (probability <= 1)), 'Probability outside [0,1]; no clipping or rate adjustment')
return probability
def forced_from_uniforms(uniforms, probability):
"""Pure arithmetic fixture helper, NOT a replacement CUDA generator."""
_vector(probability, np.float32, 'probability')
require(isinstance(uniforms, np.ndarray) and uniforms.dtype == np.float32 and uniforms.ndim == 2 and
uniforms.shape[1] == len(probability), 'Uniform fixture requires exact float32 [ticks,columns]')
require(np.all(np.isfinite(uniforms)) and np.all((uniforms >= 0) & (uniforms < 1)) and
np.all(np.isfinite(probability)) and np.all((probability >= 0) & (probability <= 1)), 'Invalid uniform/probability fixture')
return uniforms < probability[None, :]
def requests_from_forced(forced, targets, *, cs_start_tick, cs_stop_tick):
"""Sort resulting requests only; never reorder the pre-sampling columns."""
_targets(targets)
require(isinstance(forced, np.ndarray) and forced.dtype == np.bool_ and forced.ndim == 2 and
forced.shape[1] == len(targets), 'Forced samples must be Boolean [steps,original PN columns]')
steps = forced.shape[0]; _clock(steps, cs_start_tick, cs_stop_tick)
ticks, columns = np.nonzero(forced[cs_start_tick:cs_stop_tick])
ticks = ticks.astype(np.int32) + np.int32(cs_start_tick)
neurons = targets[columns].copy()
order = np.lexsort((neurons, ticks))
requests = {'tick': ticks[order], 'neuron_idx': neurons[order]}
validate_requests(requests, steps)
for value in requests.values(): value.setflags(write=False)
generated = int(np.count_nonzero(forced)); retained = len(ticks)
return dict(requests=requests, audit=dict(steps=steps, CS_window=[cs_start_tick, cs_stop_tick],
full_matrix_boolean_hits=generated, retained_CS_requests=retained,
outside_CS_boolean_hits_masked=generated - retained,
original_PN_column_order_preserved=True, sorted_only_after_sampling=True,
duplicate_requests_removed=0, probabilities_clipped=False,
returned_requests_read_only=True, consumer_must_recheck_array_hashes=True,
forced_identity=array_identity(forced), targets_identity=array_identity(targets),
requests_identity={k: array_identity(v) for k, v in requests.items()}))
def _load_torch():
import torch
return torch
def generate_cs_requests(targets, rates_hz, *, seed, steps, cs_start_tick, cs_stop_tick,
device, max_uniform_draws, dt_ms=0.1):
"""Future explicit CUDA input generation; no implicit CPU fallback.
``seed`` is the caller's full semantic seed (Python int in [0,2**63)).
The generator is private to this call. No offset/modulo/truncation or
global/manual brain seed is used. Aliases must reuse the same complete
sampling descriptor, including steps/column order/window, not seed alone.
The caller owns authorization, resource admission, and external rate pins.
``device`` and ``max_uniform_draws`` must be explicitly supplied. The
latter bounds steps * PN columns, not total process/GPU memory. Retaining
initial_seed proves only integer delivery, not CUDA high-bit influence.
"""
require(type(seed) is int and 0 <= seed < 2**63, 'Exact full 63-bit Python integer seed required')
_clock(steps, cs_start_tick, cs_stop_tick)
probability = probabilities_f32(targets, rates_hz, dt_ms=dt_ms)
require(type(device) is str and re.fullmatch('cuda:[0-9]+', device), 'Explicit indexed CUDA device required; no CPU fallback')
require(type(max_uniform_draws) is int and max_uniform_draws > 0, 'Explicit positive integer uniform-draw budget required')
uniform_draws = steps * len(targets)
require(uniform_draws <= max_uniform_draws, 'Full matrix exceeds caller uniform-draw budget; no chunking or clipping')
torch = _load_torch()
require(torch.cuda.is_available() and torch.get_default_dtype() == torch.float32, 'Original CUDA/default-float32 sampler environment required')
actual_device = torch.device(device)
generator = torch.Generator(device=actual_device)
generator.manual_seed(seed)
require(generator.initial_seed() == seed, 'Generator did not retain the complete semantic seed')
p_gpu = torch.as_tensor(probability, dtype=torch.float32, device=actual_device)
# Exactly one complete draw. No chunking, per-PN loop, or tail-dependent redraw.
uniforms = torch.rand((steps, len(targets)), generator=generator, device=actual_device)
require(uniforms.dtype == torch.float32 and str(uniforms.device) == str(actual_device), 'Sampler dtype/device changed')
forced = (uniforms < p_gpu[None, :]).cpu().numpy()
result = requests_from_forced(forced, targets, cs_start_tick=cs_start_tick, cs_stop_tick=cs_stop_tick)
# Optional identifiers are recorded only when the installed Torch exposes
# them. An indexed device name is not presented as a UUID or external pin.
identity = {'uuid': None, 'pci_bus_id': None, 'query_error': None}
try:
properties = torch.cuda.get_device_properties(actual_device)
for key in ('uuid', 'pci_bus_id'):
value = getattr(properties, key, None)
if value is not None: identity[key] = str(value)
except (AttributeError, RuntimeError) as exc:
identity['query_error'] = type(exc).__name__ + ': ' + str(exc)
result['audit'].update(schema='phase4an-conditioning-input-v1-unregistered',
seed=seed, seed_bits=63, seed_hex=f'{seed:016x}', generator_initial_seed=generator.initial_seed(),
seed_conversion='exact integer; no truncation/modulo/offset', private_generator=True,
sampler='Torch CUDA rand default float32; strict U < float32 probability',
generator_type=type(generator).__module__ + '.' + type(generator).__qualname__,
numpy_version=str(np.__version__), torch_version=str(torch.__version__), torch_cuda_version=torch.version.cuda,
execution_device=str(uniforms.device), device_name=str(torch.cuda.get_device_name(actual_device)),
device_capability=list(torch.cuda.get_device_capability(actual_device)),
device_identity=identity, device_identity_verified_against_external_pin=False,
matrix_draw_calls=1, uniform_matrix_shape=[steps, len(targets)], uniform_scalar_draws=uniform_draws,
max_uniform_draws=max_uniform_draws, uniform_matrix_bytes=uniform_draws * np.dtype(np.float32).itemsize,
budget_scope='Uniform scalar count only, not total process or GPU memory',
sampled_before_window_mask=True, dt_ms=dt_ms, dt_float32_identity=array_identity(np.asarray([dt_ms], np.float32)),
rate_units='Hz', probability_identity=array_identity(probability), rates_identity=array_identity(rates_hz),
input_mapping_authority_verified_by_this_module=False,
reference_L_source_sha256=L_SOURCE_SHA, reference_AK_encoder_source_sha256=AK_ENCODER_SHA,
full_old_L_input_identity_claimed=False, CUDA_sequence_equivalence_tested_here=False,
CUDA_high_seed_bit_sequence_influence_tested_here=False,
timing_scope='Full epoch matrix drawn once, then explicit CS window; old L supplied PN requests over its full duration',
RNG_prefix_invariance_across_shapes_claimed=False, continuous_time_Poisson_claimed=False,
brain_reset_called=False, brain_executed=False, training_executed=False, protocol_registered=False)
return result
def validate_requests(requests, steps):
_clock(steps, 0, steps)
require(type(requests) is dict and set(requests) == {'tick', 'neuron_idx'}, 'Exact sparse request schema required')
for key, value in requests.items(): _vector(value, np.int32, key)
tick, neuron = requests['tick'], requests['neuron_idx']
require(tick.shape == neuron.shape and np.all((tick >= 0) & (tick < steps)) and
np.all((neuron >= 0) & (neuron < N_NEURONS)), 'Requests outside local clock/global neuron range')
keys = tick.astype(np.int64) * N_NEURONS + neuron
require(len(keys) < 2 or np.all(keys[1:] > keys[:-1]), 'Requests must already be sorted unique; no silent sorting/deduplication')
return keys
class RequestConflictError(ValueError):
def __init__(self, audit):
super().__init__('CS/US request overlap rejected; explicit boolean_union is required to combine it')
self.audit = audit
def merge_requests(cs, us, *, steps, conflict_policy='error'):
"""Compose two valid streams, recording every cross-source collision.
boolean_union explicitly reproduces a single Boolean request per neuron /
tick, not two deliveries. Separate source attempts are retained in audit
and uint8 source_mask (CS=1, US=2, overlap=3). This is not refractory
acceptance, actual spiking, an artificial D value, or an update rule.
"""
require(conflict_policy in ('error', 'boolean_union'), 'Unknown explicit conflict policy')
cs_keys, us_keys = validate_requests(cs, steps), validate_requests(us, steps)
overlap = np.intersect1d(cs_keys, us_keys, assume_unique=True)
conflicts = [dict(tick=int(k // N_NEURONS), neuron_idx=int(k % N_NEURONS), CS_attempts=1, US_attempts=1,
combined_boolean_requests=1 if conflict_policy == 'boolean_union' else None) for k in overlap]
audit = dict(schema='phase4an-request-composition-v1-unregistered', policy=conflict_policy,
CS_attempts=len(cs_keys), US_attempts=len(us_keys), cross_source_conflict_count=len(overlap), conflicts=conflicts,
duplicate_within_source_allowed=False, refractory_acceptance_evaluated=False, actual_spikes_evaluated=False,
input_identity={name: {k: array_identity(v) for k, v in values.items()} for name, values in (('CS', cs), ('US', us))})
if len(overlap) and conflict_policy == 'error': raise RequestConflictError(audit)
keys = np.union1d(cs_keys, us_keys)
sources = np.zeros(len(keys), np.uint8)
sources[np.searchsorted(keys, cs_keys)] |= np.uint8(1)
sources[np.searchsorted(keys, us_keys)] |= np.uint8(2)
requests = {'tick': (keys // N_NEURONS).astype(np.int32), 'neuron_idx': (keys % N_NEURONS).astype(np.int32)}
validate_requests(requests, steps)
for value in (*requests.values(), sources): value.setflags(write=False)
audit.update(output_requests=len(keys), explicit_boolean_overlap_collapses=len(overlap),
requests_identity={k: array_identity(v) for k, v in requests.items()}, source_mask_identity=array_identity(sources),
returned_arrays_read_only=True, consumer_must_recheck_array_hashes=True,
learning_or_D_command_defined=False, brain_executed=False, protocol_registered=False)
return dict(requests=requests, source_mask=sources, audit=audit)