@kernel.chat/kbot 3.43.0 → 3.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2472 @@
1
+ // kbot Neuroscience & Cognitive Science Tools
2
+ // Pure TypeScript implementations, zero external dependencies.
3
+ // Covers brain atlas, EEG analysis, cognitive models, biological neural networks,
4
+ // neurotransmitters, psychophysics, connectomics, task design, neuroimaging, and learning models.
5
+ import { registerTool } from './index.js';
6
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
7
+ function fmt(n, digits = 6) {
8
+ if (n === 0)
9
+ return '0';
10
+ if (!isFinite(n))
11
+ return String(n);
12
+ const abs = Math.abs(n);
13
+ if (abs >= 1e6 || abs < 1e-3)
14
+ return n.toExponential(digits);
15
+ return n.toPrecision(digits);
16
+ }
17
+ function safeJSON(s) {
18
+ try {
19
+ return JSON.parse(s);
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ const BRAIN_ATLAS = {
26
+ // ── Frontal Lobe ──
27
+ prefrontal_cortex: { name: 'Prefrontal Cortex', location: 'Anterior frontal lobe', brodmann: 'BA 9, 10, 11, 12, 46, 47', functions: ['Executive function', 'Decision making', 'Working memory', 'Personality expression', 'Social behavior'], disorders: ['ADHD', 'Schizophrenia', 'Depression', 'OCD', 'Antisocial personality disorder'], connections: ['Thalamus', 'Basal ganglia', 'Amygdala', 'Hippocampus', 'Cingulate cortex'] },
28
+ dlpfc: { name: 'Dorsolateral Prefrontal Cortex', location: 'Lateral surface of frontal lobe', brodmann: 'BA 9, 46', functions: ['Working memory', 'Cognitive flexibility', 'Planning', 'Abstract reasoning'], disorders: ['ADHD', 'Schizophrenia', 'Depression'], connections: ['Parietal cortex', 'Thalamus', 'Basal ganglia', 'Premotor cortex'] },
29
+ vlpfc: { name: 'Ventrolateral Prefrontal Cortex', location: 'Inferior frontal gyrus', brodmann: 'BA 44, 45, 47', functions: ['Response inhibition', 'Language processing', 'Semantic retrieval'], disorders: ['ADHD', 'OCD', 'Aphasia'], connections: ['Temporal cortex', 'Amygdala', 'Insula'] },
30
+ ofc: { name: 'Orbitofrontal Cortex', location: 'Orbital surface of frontal lobe', brodmann: 'BA 11, 47', functions: ['Reward processing', 'Emotion regulation', 'Value-based decision making'], disorders: ['OCD', 'Addiction', 'Psychopathy', 'Eating disorders'], connections: ['Amygdala', 'Ventral striatum', 'Hippocampus', 'Insula'] },
31
+ primary_motor: { name: 'Primary Motor Cortex', location: 'Precentral gyrus', brodmann: 'BA 4', functions: ['Voluntary movement execution', 'Motor control', 'Somatotopic body mapping'], disorders: ['Hemiplegia', 'ALS', 'Stroke motor deficits'], connections: ['Premotor cortex', 'Supplementary motor area', 'Basal ganglia', 'Cerebellum', 'Spinal cord'] },
32
+ premotor: { name: 'Premotor Cortex', location: 'Posterior frontal lobe, anterior to M1', brodmann: 'BA 6', functions: ['Motor planning', 'Movement preparation', 'Motor learning'], disorders: ['Apraxia', 'Alien hand syndrome'], connections: ['Primary motor cortex', 'Parietal cortex', 'Basal ganglia', 'Cerebellum'] },
33
+ sma: { name: 'Supplementary Motor Area', location: 'Medial surface of frontal lobe', brodmann: 'BA 6 (medial)', functions: ['Movement sequencing', 'Bimanual coordination', 'Internally generated movements'], disorders: ['Akinetic mutism', 'Alien hand syndrome'], connections: ['Primary motor cortex', 'Basal ganglia', 'Cingulate cortex'] },
34
+ fef: { name: 'Frontal Eye Fields', location: 'Premotor cortex, anterior to M1', brodmann: 'BA 8', functions: ['Saccadic eye movements', 'Visual attention', 'Gaze control'], disorders: ['Oculomotor apraxia', 'Neglect'], connections: ['Superior colliculus', 'Parietal cortex', 'Visual cortex'] },
35
+ brocas_area: { name: "Broca's Area", location: 'Left inferior frontal gyrus', brodmann: 'BA 44, 45', functions: ['Speech production', 'Language processing', 'Syntactic processing'], disorders: ["Broca's aphasia", 'Stuttering', 'Apraxia of speech'], connections: ["Wernicke's area (arcuate fasciculus)", 'Primary motor cortex', 'Insula', 'Basal ganglia'] },
36
+ acc: { name: 'Anterior Cingulate Cortex', location: 'Medial frontal lobe, above corpus callosum', brodmann: 'BA 24, 25, 32, 33', functions: ['Error monitoring', 'Conflict detection', 'Motivation', 'Pain processing', 'Autonomic regulation'], disorders: ['Depression', 'OCD', 'PTSD', 'Chronic pain', 'Apathy'], connections: ['Prefrontal cortex', 'Amygdala', 'Insula', 'Motor cortex', 'Periaqueductal gray'] },
37
+ // ── Parietal Lobe ──
38
+ primary_somatosensory: { name: 'Primary Somatosensory Cortex', location: 'Postcentral gyrus', brodmann: 'BA 1, 2, 3', functions: ['Touch perception', 'Proprioception', 'Temperature sensing', 'Pain localization'], disorders: ['Cortical sensory loss', 'Astereognosis', 'Agraphesthesia'], connections: ['Thalamus (VPL/VPM)', 'Motor cortex', 'Posterior parietal cortex'] },
39
+ posterior_parietal: { name: 'Posterior Parietal Cortex', location: 'Superior parietal lobule', brodmann: 'BA 5, 7', functions: ['Spatial awareness', 'Sensorimotor integration', 'Reaching and grasping', 'Attention'], disorders: ['Optic ataxia', 'Hemispatial neglect', "Balint's syndrome"], connections: ['Motor cortex', 'Prefrontal cortex', 'Visual cortex', 'Cerebellum'] },
40
+ ips: { name: 'Intraparietal Sulcus', location: 'Lateral parietal lobe', brodmann: 'BA 7, 39, 40', functions: ['Numerical cognition', 'Spatial attention', 'Eye-hand coordination', 'Magnitude processing'], disorders: ['Dyscalculia', 'Neglect', "Gerstmann's syndrome"], connections: ['Frontal eye fields', 'Prefrontal cortex', 'Visual cortex'] },
41
+ angular_gyrus: { name: 'Angular Gyrus', location: 'Inferior parietal lobule', brodmann: 'BA 39', functions: ['Semantic processing', 'Reading', 'Number processing', 'Spatial cognition', 'Memory retrieval'], disorders: ["Gerstmann's syndrome", 'Alexia', 'Anomia'], connections: ["Wernicke's area", 'Prefrontal cortex', 'Visual cortex', 'Hippocampus'] },
42
+ supramarginal_gyrus: { name: 'Supramarginal Gyrus', location: 'Inferior parietal lobule', brodmann: 'BA 40', functions: ['Phonological processing', 'Empathy', 'Tactile recognition', 'Language comprehension'], disorders: ['Conduction aphasia', 'Ideomotor apraxia'], connections: ["Broca's area", "Wernicke's area", 'Somatosensory cortex'] },
43
+ precuneus: { name: 'Precuneus', location: 'Medial parietal lobe', brodmann: 'BA 7, 31', functions: ['Self-referential processing', 'Episodic memory', 'Visuospatial imagery', 'Consciousness'], disorders: ["Alzheimer's disease (early hypometabolism)", 'Depersonalization'], connections: ['Posterior cingulate', 'Prefrontal cortex', 'Lateral parietal', 'Thalamus'] },
44
+ // ── Temporal Lobe ──
45
+ primary_auditory: { name: 'Primary Auditory Cortex', location: "Heschl's gyrus, superior temporal lobe", brodmann: 'BA 41, 42', functions: ['Sound processing', 'Tonotopic frequency mapping', 'Auditory perception'], disorders: ['Cortical deafness', 'Auditory agnosia', 'Tinnitus'], connections: ['Medial geniculate nucleus', 'Auditory association cortex', 'Frontal cortex'] },
46
+ wernickes_area: { name: "Wernicke's Area", location: 'Posterior superior temporal gyrus', brodmann: 'BA 22', functions: ['Language comprehension', 'Semantic processing', 'Speech perception'], disorders: ["Wernicke's aphasia", 'Word deafness'], connections: ["Broca's area (arcuate fasciculus)", 'Angular gyrus', 'Inferior frontal gyrus'] },
47
+ superior_temporal_sulcus: { name: 'Superior Temporal Sulcus', location: 'Lateral temporal lobe', brodmann: 'BA 21, 22', functions: ['Social perception', 'Biological motion processing', 'Theory of mind', 'Audiovisual integration'], disorders: ['Autism spectrum disorder', 'Social cognition deficits'], connections: ['Fusiform gyrus', 'Amygdala', 'Prefrontal cortex'] },
48
+ fusiform_gyrus: { name: 'Fusiform Gyrus', location: 'Inferior temporal lobe', brodmann: 'BA 37', functions: ['Face recognition', 'Word recognition', 'Color processing', 'Object recognition'], disorders: ['Prosopagnosia', 'Alexia', 'Achromatopsia'], connections: ['Visual cortex', 'Amygdala', 'Prefrontal cortex', 'Superior temporal sulcus'] },
49
+ inferior_temporal: { name: 'Inferior Temporal Cortex', location: 'Inferior temporal gyrus', brodmann: 'BA 20', functions: ['Object recognition', 'Visual memory', 'Face processing', 'Category-specific knowledge'], disorders: ['Visual agnosia', 'Semantic dementia'], connections: ['Visual cortex', 'Hippocampus', 'Prefrontal cortex'] },
50
+ middle_temporal: { name: 'Middle Temporal Gyrus', location: 'Lateral temporal lobe', brodmann: 'BA 21', functions: ['Semantic memory', 'Language processing', 'Sentence comprehension'], disorders: ['Semantic dementia', 'Anomia'], connections: ['Inferior frontal gyrus', 'Angular gyrus', 'Hippocampus'] },
51
+ entorhinal_cortex: { name: 'Entorhinal Cortex', location: 'Medial temporal lobe, parahippocampal region', brodmann: 'BA 28, 34', functions: ['Memory encoding', 'Spatial navigation (grid cells)', 'Olfactory processing', 'Gateway to hippocampus'], disorders: ["Alzheimer's disease (earliest pathology)", 'Temporal lobe epilepsy'], connections: ['Hippocampus', 'Perirhinal cortex', 'Prefrontal cortex', 'Amygdala'] },
52
+ parahippocampal_gyrus: { name: 'Parahippocampal Gyrus', location: 'Medial temporal lobe', brodmann: 'BA 27, 28, 35, 36', functions: ['Scene recognition', 'Spatial context encoding', 'Navigation', 'Memory consolidation'], disorders: ["Alzheimer's disease", 'Topographical disorientation'], connections: ['Hippocampus', 'Entorhinal cortex', 'Retrosplenial cortex', 'Visual cortex'] },
53
+ // ── Occipital Lobe ──
54
+ primary_visual: { name: 'Primary Visual Cortex (V1)', location: 'Calcarine sulcus, occipital pole', brodmann: 'BA 17', functions: ['Basic visual processing', 'Edge detection', 'Orientation selectivity', 'Retinotopic mapping'], disorders: ['Cortical blindness', 'Scotoma', 'Anton syndrome'], connections: ['LGN (thalamus)', 'V2', 'V3', 'V4', 'V5/MT'] },
55
+ v2: { name: 'Visual Area V2', location: 'Surrounding V1', brodmann: 'BA 18', functions: ['Contour processing', 'Texture segregation', 'Illusory contours', 'Depth processing'], disorders: ['Visual field defects'], connections: ['V1', 'V3', 'V4', 'V5/MT'] },
56
+ v4: { name: 'Visual Area V4', location: 'Fusiform and lingual gyri', brodmann: 'BA 19 (ventral)', functions: ['Color perception', 'Shape recognition', 'Visual attention modulation'], disorders: ['Achromatopsia', 'Color agnosia'], connections: ['V1', 'V2', 'Inferior temporal cortex', 'Frontal eye fields'] },
57
+ v5_mt: { name: 'Visual Area V5/MT', location: 'Posterior temporal/parietal junction', brodmann: 'BA 19, 37', functions: ['Motion perception', 'Speed detection', 'Optic flow', 'Smooth pursuit eye movements'], disorders: ['Akinetopsia (motion blindness)', 'Motion perception deficits'], connections: ['V1', 'V2', 'V3', 'Posterior parietal cortex', 'Frontal eye fields'] },
58
+ // ── Insular Cortex ──
59
+ insula: { name: 'Insular Cortex', location: 'Deep to lateral sulcus, between frontal/temporal lobes', brodmann: 'BA 13, 14, 15, 16', functions: ['Interoception', 'Taste', 'Pain processing', 'Emotional awareness', 'Empathy', 'Autonomic regulation'], disorders: ['Anxiety disorders', 'Addiction', 'Eating disorders', 'Chronic pain'], connections: ['ACC', 'Amygdala', 'Orbitofrontal cortex', 'Thalamus', 'Somatosensory cortex'] },
60
+ // ── Limbic System ──
61
+ hippocampus: { name: 'Hippocampus', location: 'Medial temporal lobe', functions: ['Episodic memory formation', 'Spatial navigation (place cells)', 'Memory consolidation', 'Pattern separation/completion'], disorders: ["Alzheimer's disease", 'Temporal lobe epilepsy', 'Amnesia', 'PTSD'], connections: ['Entorhinal cortex', 'Subiculum', 'Mammillary bodies', 'Prefrontal cortex', 'Amygdala'] },
62
+ amygdala: { name: 'Amygdala', location: 'Anterior medial temporal lobe', functions: ['Fear conditioning', 'Emotional memory', 'Threat detection', 'Social cognition', 'Reward processing'], disorders: ['PTSD', 'Anxiety disorders', 'Phobias', 'Autism', 'Psychopathy'], connections: ['Prefrontal cortex', 'Hippocampus', 'Thalamus', 'Hypothalamus', 'ACC', 'Insula'] },
63
+ cingulate_cortex: { name: 'Cingulate Cortex', location: 'Medial surface, surrounding corpus callosum', brodmann: 'BA 23, 24, 25, 29, 30, 31, 32, 33', functions: ['Error monitoring', 'Conflict resolution', 'Emotion regulation', 'Pain processing', 'Motivation'], disorders: ['Depression', 'OCD', 'PTSD', 'Akinetic mutism'], connections: ['Prefrontal cortex', 'Amygdala', 'Hippocampus', 'Parietal cortex', 'Motor cortex'] },
64
+ posterior_cingulate: { name: 'Posterior Cingulate Cortex', location: 'Posterior medial surface', brodmann: 'BA 23, 31', functions: ['Default mode network hub', 'Self-referential thought', 'Autobiographical memory', 'Internally directed cognition'], disorders: ["Alzheimer's disease", 'Depression'], connections: ['Precuneus', 'Medial prefrontal cortex', 'Hippocampus', 'Lateral parietal cortex'] },
65
+ fornix: { name: 'Fornix', location: 'White matter tract, below corpus callosum', functions: ['Memory pathway (hippocampus to mammillary bodies)', 'Cholinergic projections'], disorders: ['Anterograde amnesia', "Korsakoff's syndrome"], connections: ['Hippocampus', 'Mammillary bodies', 'Anterior thalamus', 'Septal nuclei'] },
66
+ mammillary_bodies: { name: 'Mammillary Bodies', location: 'Posterior hypothalamus', functions: ['Memory processing', 'Recollective memory', 'Spatial memory'], disorders: ["Korsakoff's syndrome", "Wernicke's encephalopathy", 'Amnesia'], connections: ['Hippocampus (fornix)', 'Anterior thalamus', 'Tegmentum'] },
67
+ septal_nuclei: { name: 'Septal Nuclei', location: 'Medial forebrain, below corpus callosum', functions: ['Pleasure/reward', 'Arousal', 'Cholinergic modulation of hippocampus'], disorders: ['Rage (septal lesions)', 'Memory impairment'], connections: ['Hippocampus', 'Amygdala', 'Hypothalamus', 'Habenula'] },
68
+ // ── Basal Ganglia ──
69
+ caudate: { name: 'Caudate Nucleus', location: 'Medial to internal capsule', functions: ['Goal-directed behavior', 'Learning', 'Memory', 'Cognitive flexibility'], disorders: ['Huntington\'s disease', 'OCD', 'ADHD', 'Tourette syndrome'], connections: ['Prefrontal cortex', 'Thalamus', 'Substantia nigra', 'Putamen'] },
70
+ putamen: { name: 'Putamen', location: 'Lateral to internal capsule', functions: ['Motor control', 'Motor learning', 'Reinforcement learning', 'Habit formation'], disorders: ['Parkinson\'s disease', 'Huntington\'s disease', 'Dystonia'], connections: ['Motor cortex', 'Thalamus', 'Substantia nigra', 'Globus pallidus'] },
71
+ globus_pallidus: { name: 'Globus Pallidus', location: 'Medial to putamen', functions: ['Motor inhibition', 'Movement regulation', 'Basal ganglia output'], disorders: ['Parkinson\'s disease', 'Dystonia', 'Hemiballismus'], connections: ['Putamen', 'Caudate', 'Subthalamic nucleus', 'Thalamus', 'Substantia nigra'] },
72
+ subthalamic_nucleus: { name: 'Subthalamic Nucleus', location: 'Below thalamus, above substantia nigra', functions: ['Motor control (indirect pathway)', 'Impulse control', 'Decision making'], disorders: ['Hemiballismus', 'Parkinson\'s disease (DBS target)'], connections: ['Globus pallidus', 'Substantia nigra', 'Motor cortex'] },
73
+ substantia_nigra: { name: 'Substantia Nigra', location: 'Midbrain', functions: ['Dopamine production', 'Movement initiation', 'Reward signaling'], disorders: ['Parkinson\'s disease', 'Schizophrenia', 'Addiction'], connections: ['Striatum (caudate/putamen)', 'Globus pallidus', 'Thalamus', 'Prefrontal cortex'] },
74
+ nucleus_accumbens: { name: 'Nucleus Accumbens', location: 'Ventral striatum', functions: ['Reward processing', 'Motivation', 'Pleasure', 'Addiction circuitry', 'Reinforcement learning'], disorders: ['Addiction', 'Depression', 'Anhedonia', 'Schizophrenia'], connections: ['VTA', 'Prefrontal cortex', 'Amygdala', 'Hippocampus'] },
75
+ ventral_tegmental_area: { name: 'Ventral Tegmental Area (VTA)', location: 'Midbrain, medial to substantia nigra', functions: ['Dopamine reward signaling', 'Motivation', 'Salience', 'Learning from reward/punishment'], disorders: ['Addiction', 'Depression', 'Schizophrenia', 'ADHD'], connections: ['Nucleus accumbens', 'Prefrontal cortex', 'Amygdala', 'Hippocampus'] },
76
+ // ── Thalamus & Epithalamus ──
77
+ thalamus: { name: 'Thalamus', location: 'Central diencephalon', functions: ['Sensory relay', 'Motor relay', 'Consciousness gating', 'Sleep/wake regulation', 'Attention'], disorders: ['Thalamic pain syndrome', 'Fatal familial insomnia', 'Delirium'], connections: ['All cortical areas', 'Basal ganglia', 'Cerebellum', 'Brainstem'] },
78
+ lgn: { name: 'Lateral Geniculate Nucleus', location: 'Thalamus (lateral posterior)', functions: ['Visual relay (retina to V1)', 'Visual processing', 'Attentional modulation of vision'], disorders: ['Hemianopia', 'Visual field defects'], connections: ['Retina', 'V1', 'Superior colliculus', 'Pulvinar'] },
79
+ mgn: { name: 'Medial Geniculate Nucleus', location: 'Thalamus (medial posterior)', functions: ['Auditory relay', 'Auditory processing', 'Tonotopic mapping'], disorders: ['Central auditory processing disorder'], connections: ['Inferior colliculus', 'Primary auditory cortex', 'Amygdala'] },
80
+ pulvinar: { name: 'Pulvinar', location: 'Posterior thalamus', functions: ['Visual attention', 'Visual salience', 'Multimodal integration'], disorders: ['Visual neglect', 'Attention deficits'], connections: ['Visual cortex', 'Parietal cortex', 'Superior colliculus', 'Frontal eye fields'] },
81
+ habenula: { name: 'Habenula', location: 'Epithalamus', functions: ['Negative reward signaling', 'Aversion processing', 'Disappointment', 'Modulating monoamine systems'], disorders: ['Depression', 'Addiction'], connections: ['VTA', 'Raphe nuclei', 'Basal ganglia', 'Prefrontal cortex'] },
82
+ // ── Hypothalamus ──
83
+ hypothalamus: { name: 'Hypothalamus', location: 'Ventral diencephalon, below thalamus', functions: ['Homeostasis', 'Hunger', 'Thirst', 'Temperature regulation', 'Circadian rhythm', 'Hormone release', 'Autonomic control'], disorders: ['Diabetes insipidus', 'Hypothalamic obesity', 'Sleep disorders', 'Hormone imbalances'], connections: ['Pituitary gland', 'Amygdala', 'Brainstem', 'Hippocampus', 'Septal nuclei'] },
84
+ scn: { name: 'Suprachiasmatic Nucleus', location: 'Anterior hypothalamus, above optic chiasm', functions: ['Master circadian clock', 'Sleep-wake cycle regulation', 'Melatonin secretion timing'], disorders: ['Circadian rhythm disorders', 'Jet lag', 'Seasonal affective disorder'], connections: ['Retina (retinohypothalamic tract)', 'Pineal gland', 'Other hypothalamic nuclei'] },
85
+ arcuate_nucleus: { name: 'Arcuate Nucleus', location: 'Mediobasal hypothalamus', functions: ['Appetite regulation', 'Growth hormone release', 'Gonadotropin control', 'Energy balance'], disorders: ['Obesity', 'Anorexia', 'Growth hormone deficiency'], connections: ['Pituitary gland', 'Lateral hypothalamus', 'Nucleus tractus solitarius'] },
86
+ // ── Brainstem ──
87
+ midbrain: { name: 'Midbrain (Mesencephalon)', location: 'Between pons and diencephalon', functions: ['Visual/auditory reflexes', 'Eye movements', 'Motor coordination relay', 'Dopamine production'], disorders: ['Parkinson\'s disease', 'Progressive supranuclear palsy', "Weber's syndrome"], connections: ['Thalamus', 'Cerebellum', 'Pons', 'Basal ganglia'] },
88
+ superior_colliculus: { name: 'Superior Colliculus', location: 'Dorsal midbrain (tectum)', functions: ['Visual orienting', 'Saccadic eye movements', 'Multimodal spatial mapping'], disorders: ['Saccade deficits'], connections: ['Retina', 'Visual cortex', 'Frontal eye fields', 'Pulvinar'] },
89
+ inferior_colliculus: { name: 'Inferior Colliculus', location: 'Dorsal midbrain, below SC', functions: ['Auditory relay', 'Sound localization', 'Startle reflex'], disorders: ['Central auditory processing disorder'], connections: ['Lateral lemniscus', 'MGN', 'Superior colliculus'] },
90
+ periaqueductal_gray: { name: 'Periaqueductal Gray', location: 'Midbrain, surrounding cerebral aqueduct', functions: ['Pain modulation (descending)', 'Fear response', 'Defensive behavior', 'Vocalization', 'Autonomic regulation'], disorders: ['Chronic pain', 'PTSD', 'Panic disorder'], connections: ['Amygdala', 'Hypothalamus', 'Prefrontal cortex', 'Raphe nuclei', 'Spinal cord'] },
91
+ red_nucleus: { name: 'Red Nucleus', location: 'Midbrain tegmentum', functions: ['Motor coordination', 'Rubrospinal tract', 'Limb movement (primitive)'], disorders: ['Holmes tremor', 'Cerebellar ataxia'], connections: ['Cerebellum', 'Motor cortex', 'Spinal cord'] },
92
+ pons: { name: 'Pons', location: 'Brainstem, between midbrain and medulla', functions: ['Relay between cerebrum and cerebellum', 'Sleep regulation (REM)', 'Respiration', 'Facial sensation/motor'], disorders: ['Locked-in syndrome', 'Central pontine myelinolysis', 'Trigeminal neuralgia'], connections: ['Cerebellum', 'Medulla', 'Midbrain', 'Cortex'] },
93
+ locus_coeruleus: { name: 'Locus Coeruleus', location: 'Dorsal pons', functions: ['Norepinephrine production', 'Arousal', 'Attention', 'Stress response', 'Sleep-wake transition'], disorders: ['PTSD', 'Anxiety', 'ADHD', "Alzheimer's disease", 'Depression'], connections: ['Widespread cortical projections', 'Amygdala', 'Hippocampus', 'Hypothalamus', 'Cerebellum'] },
94
+ raphe_nuclei: { name: 'Raphe Nuclei', location: 'Midline brainstem (midbrain to medulla)', functions: ['Serotonin production', 'Mood regulation', 'Sleep', 'Pain modulation', 'Appetite'], disorders: ['Depression', 'Anxiety', 'Insomnia', 'Migraine', 'OCD'], connections: ['Widespread cortical projections', 'Hippocampus', 'Amygdala', 'Hypothalamus', 'Spinal cord'] },
95
+ medulla: { name: 'Medulla Oblongata', location: 'Lowest brainstem, continuous with spinal cord', functions: ['Cardiovascular regulation', 'Respiration', 'Swallowing', 'Vomiting', 'Autonomic reflexes'], disorders: ['Wallenberg syndrome', 'Central sleep apnea', 'Autonomic failure'], connections: ['Spinal cord', 'Pons', 'Cerebellum', 'Cranial nerve nuclei'] },
96
+ nts: { name: 'Nucleus Tractus Solitarius', location: 'Dorsal medulla', functions: ['Visceral sensory processing', 'Taste', 'Baroreceptor reflex', 'Chemoreceptor input', 'Vagal afferents'], disorders: ['Autonomic dysregulation', 'Baroreceptor failure'], connections: ['Vagus nerve', 'Hypothalamus', 'Amygdala', 'Parabrachial nucleus'] },
97
+ // ── Cerebellum ──
98
+ cerebellum: { name: 'Cerebellum', location: 'Posterior fossa, below occipital lobe', functions: ['Motor coordination', 'Balance', 'Motor learning', 'Timing', 'Cognitive sequencing', 'Error correction'], disorders: ['Cerebellar ataxia', 'Dysmetria', 'Intention tremor', 'Cerebellar cognitive affective syndrome'], connections: ['Pons', 'Thalamus', 'Motor cortex', 'Vestibular nuclei', 'Red nucleus', 'Inferior olive'] },
99
+ cerebellar_vermis: { name: 'Cerebellar Vermis', location: 'Midline cerebellum', functions: ['Posture', 'Balance', 'Gait', 'Emotional regulation', 'Eye movements'], disorders: ['Truncal ataxia', 'Gait ataxia', 'Cerebellar cognitive affective syndrome'], connections: ['Vestibular nuclei', 'Fastigial nucleus', 'Brainstem'] },
100
+ cerebellar_hemispheres: { name: 'Cerebellar Hemispheres', location: 'Lateral cerebellum', functions: ['Limb coordination', 'Motor planning', 'Cognitive processing', 'Language'], disorders: ['Appendicular ataxia', 'Dysmetria', 'Intention tremor'], connections: ['Dentate nucleus', 'Thalamus', 'Motor cortex', 'Prefrontal cortex'] },
101
+ dentate_nucleus: { name: 'Dentate Nucleus', location: 'Deep cerebellar nuclei (lateral)', functions: ['Motor planning output', 'Cognitive cerebellar function', 'Timing'], disorders: ['Cerebellar ataxia', 'Action tremor'], connections: ['Cerebellar cortex', 'Thalamus (VL)', 'Red nucleus', 'Motor cortex'] },
102
+ // ── White Matter Tracts ──
103
+ corpus_callosum: { name: 'Corpus Callosum', location: 'Midline, connecting hemispheres', functions: ['Interhemispheric communication', 'Bimanual coordination', 'Bilateral integration'], disorders: ['Split-brain syndrome', 'Alien hand syndrome', 'Agenesis of corpus callosum'], connections: ['All corresponding cortical areas bilaterally'] },
104
+ arcuate_fasciculus: { name: 'Arcuate Fasciculus', location: 'Lateral white matter', functions: ['Language pathway (Broca-Wernicke connection)', 'Phonological processing', 'Repetition'], disorders: ['Conduction aphasia', 'Dyslexia'], connections: ["Broca's area", "Wernicke's area", 'Inferior parietal lobule'] },
105
+ cingulum_bundle: { name: 'Cingulum Bundle', location: 'Surrounding cingulate cortex', functions: ['Emotion regulation pathway', 'Memory circuits', 'Default mode connectivity'], disorders: ['Depression', "Alzheimer's disease", 'OCD'], connections: ['Cingulate cortex', 'Hippocampus', 'Entorhinal cortex', 'Prefrontal cortex'] },
106
+ uncinate_fasciculus: { name: 'Uncinate Fasciculus', location: 'Connecting frontal and temporal lobes', functions: ['Emotional memory', 'Social cognition', 'Naming', 'Semantic memory'], disorders: ['Psychopathy', 'Semantic dementia', 'Anxiety disorders'], connections: ['Orbitofrontal cortex', 'Temporal pole', 'Amygdala'] },
107
+ internal_capsule: { name: 'Internal Capsule', location: 'Between caudate/thalamus and putamen/globus pallidus', functions: ['Motor pathway (corticospinal)', 'Sensory pathway (thalamocortical)', 'Corticopontine fibers'], disorders: ['Lacunar stroke (pure motor/sensory)', 'Hemiplegia'], connections: ['Motor cortex', 'Somatosensory cortex', 'Thalamus', 'Brainstem'] },
108
+ corticospinal_tract: { name: 'Corticospinal Tract', location: 'From cortex through internal capsule, brainstem, to spinal cord', functions: ['Voluntary movement', 'Fine motor control', 'Upper motor neuron pathway'], disorders: ['Upper motor neuron lesion', 'Spasticity', 'ALS'], connections: ['Primary motor cortex', 'Premotor cortex', 'Brainstem pyramids', 'Spinal cord anterior horn'] },
109
+ // ── Other Key Structures ──
110
+ pineal_gland: { name: 'Pineal Gland', location: 'Epithalamus, posterior to third ventricle', functions: ['Melatonin secretion', 'Circadian rhythm regulation', 'Sleep onset'], disorders: ['Circadian rhythm disorders', 'Pineal tumors', 'Precocious puberty'], connections: ['Suprachiasmatic nucleus', 'Sympathetic chain'] },
111
+ pituitary_gland: { name: 'Pituitary Gland', location: 'Sella turcica, below hypothalamus', functions: ['Master endocrine gland', 'Hormone secretion (GH, ACTH, TSH, LH, FSH, PRL, ADH, oxytocin)'], disorders: ['Pituitary adenoma', 'Hypopituitarism', 'Acromegaly', "Cushing's disease"], connections: ['Hypothalamus (portal system & neurohypophysis)'] },
112
+ reticular_formation: { name: 'Reticular Formation', location: 'Core of brainstem (medulla to midbrain)', functions: ['Arousal', 'Consciousness', 'Sleep-wake cycle', 'Pain modulation', 'Motor tone'], disorders: ['Coma', 'Vegetative state', 'Narcolepsy'], connections: ['Thalamus (intralaminar)', 'Cortex (widespread)', 'Spinal cord', 'Cranial nerve nuclei'] },
113
+ basal_forebrain: { name: 'Basal Forebrain (Nucleus Basalis of Meynert)', location: 'Ventral forebrain', functions: ['Acetylcholine production', 'Cortical arousal', 'Attention', 'Memory formation'], disorders: ["Alzheimer's disease", 'Lewy body dementia', 'Memory impairment'], connections: ['All cortical areas (cholinergic)', 'Hippocampus', 'Amygdala'] },
114
+ claustrum: { name: 'Claustrum', location: 'Thin sheet between insula and putamen', functions: ['Consciousness integration (proposed)', 'Cross-modal binding', 'Salience detection', 'Attention'], disorders: ['Possibly related to consciousness disorders'], connections: ['All cortical areas (bidirectional)', 'Insula', 'Prefrontal cortex'] },
115
+ bed_nucleus_stria_terminalis: { name: 'Bed Nucleus of the Stria Terminalis', location: 'Extended amygdala, near caudate head', functions: ['Sustained anxiety', 'Stress response', 'Fear generalization', 'HPA axis regulation'], disorders: ['Generalized anxiety disorder', 'PTSD', 'Addiction'], connections: ['Amygdala', 'Hypothalamus', 'VTA', 'Prefrontal cortex'] },
116
+ zona_incerta: { name: 'Zona Incerta', location: 'Subthalamic region', functions: ['Sensory gating', 'Visceral functions', 'Sleep', 'Locomotion'], disorders: ['Pain syndromes (DBS target)', 'Essential tremor'], connections: ['Thalamus', 'Brainstem', 'Cortex', 'Cerebellum'] },
117
+ lateral_hypothalamus: { name: 'Lateral Hypothalamus', location: 'Lateral hypothalamic area', functions: ['Hunger/feeding', 'Reward processing', 'Orexin/hypocretin production', 'Arousal', 'Motivation'], disorders: ['Narcolepsy', 'Obesity', 'Aphagia (if lesioned)'], connections: ['VTA', 'Nucleus accumbens', 'Cortex', 'Brainstem arousal centers'] },
118
+ ventral_pallidum: { name: 'Ventral Pallidum', location: 'Below anterior commissure', functions: ['Hedonic processing', 'Reward output', 'Motivation', 'Limbic motor interface'], disorders: ['Addiction', 'Anhedonia'], connections: ['Nucleus accumbens', 'VTA', 'Thalamus', 'Prefrontal cortex'] },
119
+ pedunculopontine_nucleus: { name: 'Pedunculopontine Nucleus', location: 'Dorsolateral pons/midbrain junction', functions: ['Locomotion initiation', 'REM sleep', 'Arousal', 'Cholinergic modulation'], disorders: ['Parkinson\'s gait freezing', 'Falls', 'REM sleep behavior disorder'], connections: ['Basal ganglia', 'Thalamus', 'Brainstem reticular formation', 'Spinal cord'] },
120
+ parabrachial_nucleus: { name: 'Parabrachial Nucleus', location: 'Dorsolateral pons', functions: ['Taste relay', 'Visceral sensation', 'Pain', 'Breathing regulation', 'Arousal from CO2'], disorders: ['Central sleep apnea', 'Gustatory dysfunction'], connections: ['NTS', 'Thalamus', 'Amygdala', 'Hypothalamus', 'Insula'] },
121
+ vestibular_nuclei: { name: 'Vestibular Nuclei', location: 'Pontomedullary junction', functions: ['Balance', 'Head position sensing', 'Vestibulo-ocular reflex', 'Postural control'], disorders: ['Vertigo', 'Nystagmus', "Meniere's disease", 'Vestibular neuritis'], connections: ['Vestibular nerve', 'Cerebellum', 'Oculomotor nuclei', 'Spinal cord', 'Thalamus'] },
122
+ inferior_olive: { name: 'Inferior Olive', location: 'Ventral medulla', functions: ['Cerebellar learning signal (climbing fibers)', 'Motor timing', 'Error correction'], disorders: ['Palatal myoclonus', 'Olivopontocerebellar atrophy'], connections: ['Cerebellum (climbing fibers)', 'Red nucleus', 'Spinal cord'] },
123
+ dorsal_horn: { name: 'Spinal Dorsal Horn', location: 'Posterior spinal cord gray matter', functions: ['Pain processing', 'Temperature', 'Touch relay', 'Gate control of pain', 'Nociceptive modulation'], disorders: ['Chronic pain', 'Central sensitization', 'Syringomyelia'], connections: ['Peripheral afferents', 'Thalamus (spinothalamic)', 'Brainstem', 'Descending modulatory pathways'] },
124
+ olfactory_bulb: { name: 'Olfactory Bulb', location: 'Anterior cranial fossa, above cribriform plate', functions: ['Smell processing', 'Odor discrimination', 'First relay in olfactory pathway'], disorders: ['Anosmia', 'Parosmia', "Parkinson's disease (early sign)", 'COVID-19 anosmia'], connections: ['Olfactory epithelium', 'Piriform cortex', 'Entorhinal cortex', 'Amygdala', 'Orbitofrontal cortex'] },
125
+ };
126
+ const NEUROTRANSMITTERS = {
127
+ glutamate: { name: 'Glutamate', type: 'Amino acid (excitatory)', receptors: ['NMDA', 'AMPA', 'Kainate', 'mGluR1-8'], functions: ['Primary excitatory neurotransmission', 'Synaptic plasticity (LTP)', 'Learning and memory', 'Neural development'], disorders: ['Epilepsy', "Alzheimer's disease", 'Stroke (excitotoxicity)', 'Schizophrenia', 'ALS'], drugs: ['Memantine (NMDA antagonist)', 'Ketamine', 'Lamotrigine', 'Topiramate', 'Perampanel (AMPA antagonist)'], synthesis: 'Glutamine → Glutamate (via glutaminase)' },
128
+ gaba: { name: 'GABA (Gamma-Aminobutyric Acid)', type: 'Amino acid (inhibitory)', receptors: ['GABA-A (ionotropic, Cl⁻)', 'GABA-B (metabotropic, K⁺/Ca²⁺)', 'GABA-C'], functions: ['Primary inhibitory neurotransmission', 'Anxiety reduction', 'Sleep promotion', 'Muscle relaxation', 'Seizure prevention'], disorders: ['Epilepsy', 'Anxiety disorders', 'Insomnia', 'Huntington\'s disease', 'Spasticity'], drugs: ['Benzodiazepines (GABA-A PAM)', 'Barbiturates', 'Gabapentin', 'Baclofen (GABA-B)', 'Vigabatrin', 'Zolpidem', 'Alcohol (GABA-A PAM)'], synthesis: 'Glutamate → GABA (via glutamic acid decarboxylase / GAD)' },
129
+ dopamine: { name: 'Dopamine', type: 'Monoamine (catecholamine)', receptors: ['D1 (excitatory, Gs)', 'D2 (inhibitory, Gi)', 'D3', 'D4', 'D5'], functions: ['Reward and motivation', 'Motor control', 'Working memory', 'Attention', 'Learning from reinforcement', 'Hormone regulation'], disorders: ['Parkinson\'s disease', 'Schizophrenia', 'ADHD', 'Addiction', 'Depression', 'Restless leg syndrome'], drugs: ['L-DOPA', 'Pramipexole (D2/D3 agonist)', 'Haloperidol (D2 antagonist)', 'Methylphenidate (DAT blocker)', 'Amphetamine', 'Cocaine (DAT blocker)', 'Aripiprazole (D2 partial agonist)'], synthesis: 'Tyrosine → L-DOPA (tyrosine hydroxylase) → Dopamine (DOPA decarboxylase)' },
130
+ norepinephrine: { name: 'Norepinephrine (Noradrenaline)', type: 'Monoamine (catecholamine)', receptors: ['α1 (Gq)', 'α2 (Gi, presynaptic autoreceptor)', 'β1 (Gs)', 'β2 (Gs)', 'β3 (Gs)'], functions: ['Arousal and alertness', 'Fight-or-flight response', 'Attention', 'Mood regulation', 'Blood pressure', 'Memory consolidation'], disorders: ['PTSD', 'Depression', 'ADHD', 'Anxiety', 'Orthostatic hypotension', 'Panic disorder'], drugs: ['Atomoxetine (NRI)', 'Venlafaxine (SNRI)', 'Duloxetine (SNRI)', 'Clonidine (α2 agonist)', 'Propranolol (β-blocker)', 'Desipramine (NRI)'], synthesis: 'Dopamine → Norepinephrine (dopamine β-hydroxylase)' },
131
+ serotonin: { name: 'Serotonin (5-HT)', type: 'Monoamine (indolamine)', receptors: ['5-HT1A-F', '5-HT2A-C', '5-HT3 (ionotropic)', '5-HT4', '5-HT5', '5-HT6', '5-HT7 (14+ subtypes total)'], functions: ['Mood regulation', 'Sleep-wake cycle', 'Appetite', 'Pain modulation', 'Gut motility (95% in GI)', 'Platelet aggregation', 'Social behavior'], disorders: ['Depression', 'Anxiety', 'OCD', 'Migraine', 'IBS', 'Eating disorders', 'PTSD'], drugs: ['SSRIs (fluoxetine, sertraline)', 'Triptans (5-HT1B/D agonist)', 'Ondansetron (5-HT3 antagonist)', 'Buspirone (5-HT1A partial agonist)', 'Psilocybin (5-HT2A agonist)', 'LSD (5-HT2A agonist)', 'MDMA (SERT reversal)'], synthesis: 'Tryptophan → 5-HTP (tryptophan hydroxylase) → Serotonin (aromatic amino acid decarboxylase)' },
132
+ acetylcholine: { name: 'Acetylcholine (ACh)', type: 'Ester', receptors: ['Nicotinic (nAChR, ionotropic)', 'Muscarinic M1-M5 (metabotropic)'], functions: ['Neuromuscular junction', 'Memory and learning', 'Arousal', 'Attention', 'REM sleep', 'Parasympathetic nervous system'], disorders: ["Alzheimer's disease", 'Myasthenia gravis', 'Parkinson\'s disease dementia', 'Lewy body dementia'], drugs: ['Donepezil (AChE inhibitor)', 'Nicotine (nAChR agonist)', 'Atropine (mAChR antagonist)', 'Physostigmine (AChE inhibitor)', 'Botulinum toxin (blocks ACh release)', 'Neostigmine', 'Galantamine'], synthesis: 'Choline + Acetyl-CoA → ACh (choline acetyltransferase / ChAT)' },
133
+ epinephrine: { name: 'Epinephrine (Adrenaline)', type: 'Monoamine (catecholamine)', receptors: ['α1', 'α2', 'β1', 'β2', 'β3 (same as norepinephrine)'], functions: ['Fight-or-flight response', 'Heart rate increase', 'Bronchodilation', 'Glycogenolysis', 'Acute stress response'], disorders: ['Pheochromocytoma', 'Anaphylaxis', 'Cardiac arrest'], drugs: ['Epinephrine (adrenaline) injection', 'EpiPen'], synthesis: 'Norepinephrine → Epinephrine (PNMT in adrenal medulla)' },
134
+ histamine: { name: 'Histamine', type: 'Monoamine (imidazolamine)', receptors: ['H1 (Gq)', 'H2 (Gs)', 'H3 (Gi, presynaptic autoreceptor)', 'H4 (Gi)'], functions: ['Wakefulness', 'Arousal', 'Appetite regulation', 'Gastric acid secretion', 'Inflammatory/immune response', 'Cognitive function'], disorders: ['Insomnia', 'Allergies', 'GERD', 'Narcolepsy', 'Motion sickness'], drugs: ['Diphenhydramine (H1 antagonist)', 'Ranitidine (H2 antagonist)', 'Pitolisant (H3 antagonist/inverse agonist)', 'Modafinil (indirect)', 'Meclizine'], synthesis: 'Histidine → Histamine (histidine decarboxylase)' },
135
+ glycine: { name: 'Glycine', type: 'Amino acid (inhibitory)', receptors: ['Glycine receptor (GlyR, Cl⁻ ionotropic)', 'NMDA receptor (co-agonist site)'], functions: ['Inhibitory neurotransmission (brainstem/spinal cord)', 'NMDA receptor co-activation', 'Motor reflex modulation', 'Pain processing'], disorders: ['Hyperekplexia (startle disease)', 'Spasticity', 'Glycine encephalopathy'], drugs: ['Strychnine (GlyR antagonist, poison)', 'D-cycloserine (NMDA glycine site partial agonist)'], synthesis: 'Serine → Glycine (serine hydroxymethyltransferase)' },
136
+ endorphins: { name: 'Endorphins (β-endorphin)', type: 'Neuropeptide (opioid)', receptors: ['μ (mu) opioid receptor', 'δ (delta) opioid receptor', 'κ (kappa) opioid receptor'], functions: ['Pain relief (analgesia)', 'Euphoria', 'Stress response', 'Reward', 'Immune modulation'], disorders: ['Chronic pain', 'Addiction', 'Depression'], drugs: ['Morphine (μ agonist)', 'Naloxone (opioid antagonist)', 'Naltrexone', 'Fentanyl', 'Buprenorphine (partial μ agonist)'], synthesis: 'Pro-opiomelanocortin (POMC) → β-endorphin (proteolytic cleavage)' },
137
+ enkephalins: { name: 'Enkephalins (Met/Leu-enkephalin)', type: 'Neuropeptide (opioid)', receptors: ['δ (delta) opioid receptor (primary)', 'μ (mu) opioid receptor'], functions: ['Pain modulation', 'Reward', 'Stress response', 'Gastrointestinal regulation'], disorders: ['Chronic pain', 'Addiction'], drugs: ['Opioid analgesics (indirect)', 'Enkephalinase inhibitors (experimental)'], synthesis: 'Proenkephalin → Enkephalins (proteolytic cleavage)' },
138
+ dynorphins: { name: 'Dynorphins', type: 'Neuropeptide (opioid)', receptors: ['κ (kappa) opioid receptor (primary)'], functions: ['Pain modulation', 'Dysphoria', 'Stress response', 'Addiction aversion', 'Spinal analgesia'], disorders: ['Addiction', 'Depression', 'Chronic pain'], drugs: ['Salvinorin A (κ agonist)', 'Nor-BNI (κ antagonist, experimental)'], synthesis: 'Prodynorphin → Dynorphins (proteolytic cleavage)' },
139
+ substance_p: { name: 'Substance P', type: 'Neuropeptide (tachykinin)', receptors: ['NK1 (neurokinin-1) receptor'], functions: ['Pain transmission', 'Inflammation (neurogenic)', 'Nausea/vomiting', 'Stress and anxiety', 'Mood regulation'], disorders: ['Chronic pain', 'Fibromyalgia', 'Depression', 'Nausea'], drugs: ['Aprepitant (NK1 antagonist, antiemetic)', 'Fosaprepitant'], synthesis: 'Preprotachykinin A → Substance P (proteolytic cleavage)' },
140
+ neuropeptide_y: { name: 'Neuropeptide Y (NPY)', type: 'Neuropeptide', receptors: ['Y1 (Gi)', 'Y2 (Gi, presynaptic)', 'Y4', 'Y5'], functions: ['Appetite stimulation (orexigenic)', 'Anxiety reduction', 'Stress resilience', 'Vasoconstriction', 'Circadian rhythm'], disorders: ['Obesity', 'Anxiety', 'Epilepsy', 'PTSD'], drugs: ['NPY receptor antagonists (experimental anti-obesity)'], synthesis: 'Pre-pro-NPY → NPY (proteolytic cleavage)' },
141
+ oxytocin: { name: 'Oxytocin', type: 'Neuropeptide', receptors: ['Oxytocin receptor (OXTR, Gq)'], functions: ['Social bonding', 'Trust', 'Maternal behavior', 'Uterine contraction', 'Milk ejection', 'Pair bonding', 'Stress reduction'], disorders: ['Autism', 'Social anxiety', 'Postpartum depression', 'Attachment disorders'], drugs: ['Pitocin (synthetic oxytocin)', 'Atosiban (OXTR antagonist, tocolytic)', 'Intranasal oxytocin (experimental)'], synthesis: 'Synthesized in paraventricular and supraoptic nuclei of hypothalamus' },
142
+ vasopressin: { name: 'Vasopressin (ADH)', type: 'Neuropeptide', receptors: ['V1a (Gq, vascular)', 'V1b (Gq, pituitary)', 'V2 (Gs, renal)'], functions: ['Water reabsorption', 'Blood pressure regulation', 'Social recognition', 'Aggression', 'Pair bonding', 'Stress response'], disorders: ['Diabetes insipidus', 'SIADH', 'Autism (social deficits)'], drugs: ['Desmopressin (V2 agonist)', 'Conivaptan (V1a/V2 antagonist)', 'Tolvaptan (V2 antagonist)'], synthesis: 'Synthesized in paraventricular and supraoptic nuclei of hypothalamus' },
143
+ orexin: { name: 'Orexin (Hypocretin)', type: 'Neuropeptide', receptors: ['OX1R (Gq)', 'OX2R (Gq/Gi)'], functions: ['Wakefulness promotion', 'Appetite regulation', 'Reward seeking', 'Arousal', 'Energy homeostasis', 'Stabilize sleep-wake transitions'], disorders: ['Narcolepsy type 1 (orexin deficiency)', 'Insomnia', 'Addiction'], drugs: ['Suvorexant (dual OX1/OX2 antagonist, insomnia)', 'Lemborexant', 'Modafinil (indirect orexin activation)'], synthesis: 'Prepro-orexin → Orexin-A and Orexin-B (in lateral hypothalamus only)' },
144
+ crf: { name: 'Corticotropin-Releasing Factor (CRF)', type: 'Neuropeptide', receptors: ['CRF1 (Gs)', 'CRF2 (Gs)'], functions: ['HPA axis activation', 'Stress response initiation', 'Anxiety', 'Appetite suppression (acute stress)', 'Immune modulation'], disorders: ['Depression', 'Anxiety', 'PTSD', "Cushing's disease", 'Anorexia nervosa'], drugs: ['Antalarmin (CRF1 antagonist, experimental)', 'Pexacerfont (experimental)'], synthesis: 'Synthesized in paraventricular nucleus of hypothalamus' },
145
+ cholecystokinin: { name: 'Cholecystokinin (CCK)', type: 'Neuropeptide', receptors: ['CCK-A (CCK1, Gq, peripheral)', 'CCK-B (CCK2, Gq, central)'], functions: ['Satiety signaling', 'Anxiety/panic', 'Pain modulation', 'Memory', 'Gallbladder contraction', 'Pancreatic enzyme secretion'], disorders: ['Panic disorder', 'Schizophrenia', 'Eating disorders'], drugs: ['CCK-4 (panicogenic, research)', 'Devazepide (CCK-A antagonist, experimental)'], synthesis: 'Preprocholecystokinin → CCK (proteolytic cleavage, multiple active forms)' },
146
+ nitric_oxide: { name: 'Nitric Oxide (NO)', type: 'Gasotransmitter', receptors: ['Soluble guanylyl cyclase (sGC) → cGMP'], functions: ['Vasodilation', 'Retrograde neurotransmission', 'Synaptic plasticity (LTP)', 'Immune defense', 'Neurotoxicity (excess)'], disorders: ['Migraine', 'Stroke', 'Erectile dysfunction', 'Neurodegenerative diseases'], drugs: ['Sildenafil (PDE5 inhibitor, preserves NO/cGMP)', 'Nitroglycerin (NO donor)', 'L-NAME (NOS inhibitor, research)'], synthesis: 'L-Arginine → NO + Citrulline (nitric oxide synthase / NOS: nNOS, eNOS, iNOS)' },
147
+ adenosine: { name: 'Adenosine', type: 'Purine', receptors: ['A1 (Gi, inhibitory)', 'A2A (Gs, excitatory)', 'A2B (Gs)', 'A3 (Gi)'], functions: ['Sleep pressure accumulation', 'Neuroprotection', 'Vasodilation', 'Anti-inflammatory', 'Modulation of neurotransmitter release'], disorders: ['Insomnia', 'Epilepsy', 'Parkinson\'s disease', 'Pain'], drugs: ['Caffeine (A1/A2A antagonist)', 'Theophylline (antagonist)', 'Istradefylline (A2A antagonist, Parkinson\'s)', 'Adenosine injection (antiarrhythmic)'], synthesis: 'ATP → ADP → AMP → Adenosine (5\'-nucleotidase); accumulates with neural activity' },
148
+ atp_purinergic: { name: 'ATP (Purinergic signaling)', type: 'Purine', receptors: ['P2X1-7 (ionotropic)', 'P2Y1-14 (metabotropic)'], functions: ['Fast excitatory transmission', 'Pain signaling', 'Microglia activation', 'Neuron-glia communication', 'Bladder control'], disorders: ['Chronic pain', 'Migraine', 'Urinary incontinence', 'Neuroinflammation'], drugs: ['Suramin (P2 antagonist)', 'Clopidogrel (P2Y12 antagonist)', 'Ticagrelor'], synthesis: 'Released from synaptic vesicles; co-released with other neurotransmitters' },
149
+ endocannabinoids: { name: 'Endocannabinoids (Anandamide, 2-AG)', type: 'Lipid', receptors: ['CB1 (Gi, CNS)', 'CB2 (Gi, immune)', 'TRPV1 (anandamide)'], functions: ['Retrograde signaling', 'Synaptic plasticity', 'Pain modulation', 'Appetite regulation', 'Mood', 'Memory extinction', 'Neuroprotection'], disorders: ['Chronic pain', 'Anxiety', 'PTSD', 'Epilepsy', 'Multiple sclerosis', 'Obesity'], drugs: ['THC (CB1 partial agonist)', 'CBD (indirect)', 'Rimonabant (CB1 inverse agonist, withdrawn)', 'Epidiolex (CBD, epilepsy)', 'Nabilone (synthetic THC)'], synthesis: 'Anandamide: NAPE-PLD pathway on demand; 2-AG: DAGLα pathway; synthesized postsynaptically' },
150
+ taurine: { name: 'Taurine', type: 'Amino acid (inhibitory)', receptors: ['Glycine receptors', 'GABA-A receptors (weak)', 'Taurine receptors (proposed)'], functions: ['Inhibitory neuromodulation', 'Osmoregulation', 'Calcium homeostasis', 'Neuroprotection', 'Retinal development'], disorders: ['Epilepsy', 'Retinal degeneration', 'Cardiomyopathy'], drugs: ['Taurine supplement', 'Acamprosate (partial GABA-A/taurine mechanism)'], synthesis: 'Cysteine → Cysteinesulfinic acid → Hypotaurine → Taurine (cysteine dioxygenase pathway)' },
151
+ d_serine: { name: 'D-Serine', type: 'Amino acid (co-agonist)', receptors: ['NMDA receptor (glycine site co-agonist)'], functions: ['NMDA receptor activation', 'Synaptic plasticity', 'Learning and memory', 'Neurodevelopment'], disorders: ['Schizophrenia (NMDA hypofunction)', 'ALS', "Alzheimer's disease"], drugs: ['D-serine (experimental schizophrenia adjunct)', 'D-cycloserine (partial agonist)'], synthesis: 'L-Serine → D-Serine (serine racemase); primarily in astrocytes' },
152
+ agmatine: { name: 'Agmatine', type: 'Amino acid derivative', receptors: ['Imidazoline receptors (I1, I2)', 'α2-adrenergic receptors', 'NMDA receptor (blocker)', 'Nicotinic receptors'], functions: ['Neuromodulation', 'Pain reduction', 'Neuroprotection', 'Insulin secretion', 'Nitric oxide modulation'], disorders: ['Depression', 'Neuropathic pain', 'Addiction'], drugs: ['Agmatine supplement (experimental)', 'Moxonidine (I1 agonist)'], synthesis: 'Arginine → Agmatine (arginine decarboxylase)' },
153
+ hydrogen_sulfide: { name: 'Hydrogen Sulfide (H₂S)', type: 'Gasotransmitter', receptors: ['KATP channels', 'NMDA receptor modulation', 'TRPA1'], functions: ['Neuromodulation', 'Vasodilation', 'Anti-inflammatory', 'Synaptic plasticity', 'Neuroprotection (low doses)'], disorders: ['Hypertension', 'Neurodegenerative diseases', 'Inflammation'], drugs: ['NaHS (H₂S donor, research)', 'GYY4137 (slow-release donor)', 'AOAA (CBS inhibitor)'], synthesis: 'Cysteine → H₂S (cystathionine β-synthase / CBS; cystathionine γ-lyase / CSE)' },
154
+ melatonin: { name: 'Melatonin', type: 'Indolamine (hormone/neuromodulator)', receptors: ['MT1 (Gi)', 'MT2 (Gi)'], functions: ['Circadian rhythm entrainment', 'Sleep onset promotion', 'Antioxidant', 'Immune modulation', 'Seasonal reproduction'], disorders: ['Insomnia', 'Circadian rhythm disorders', 'Jet lag', 'Seasonal affective disorder', 'Delayed sleep phase disorder'], drugs: ['Melatonin supplement', 'Ramelteon (MT1/MT2 agonist)', 'Tasimelteon (MT1/MT2 agonist)', 'Agomelatine (MT1/MT2 agonist + 5-HT2C antagonist)'], synthesis: 'Serotonin → N-acetylserotonin (AANAT) → Melatonin (HIOMT); in pineal gland, dark-dependent' },
155
+ carbon_monoxide: { name: 'Carbon Monoxide (CO)', type: 'Gasotransmitter', receptors: ['Soluble guanylyl cyclase (sGC)', 'BKCa channels', 'Heme proteins'], functions: ['Anti-inflammatory', 'Vasodilation', 'Anti-apoptotic', 'Neurotransmission modulation'], disorders: ['CO poisoning (excess)', 'Neuroinflammation'], drugs: ['CO-releasing molecules (CORMs, experimental)'], synthesis: 'Heme → CO + Biliverdin (heme oxygenase / HO-1, HO-2)' },
156
+ };
157
+ const CONNECTOME_REGIONS = [
158
+ { name: 'Prefrontal Cortex', abbreviation: 'PFC', lobe: 'Frontal' },
159
+ { name: 'Primary Motor Cortex', abbreviation: 'M1', lobe: 'Frontal' },
160
+ { name: 'Premotor Cortex', abbreviation: 'PMC', lobe: 'Frontal' },
161
+ { name: 'Primary Somatosensory Cortex', abbreviation: 'S1', lobe: 'Parietal' },
162
+ { name: 'Posterior Parietal Cortex', abbreviation: 'PPC', lobe: 'Parietal' },
163
+ { name: 'Primary Visual Cortex', abbreviation: 'V1', lobe: 'Occipital' },
164
+ { name: 'Visual Association Cortex', abbreviation: 'VAC', lobe: 'Occipital' },
165
+ { name: 'Primary Auditory Cortex', abbreviation: 'A1', lobe: 'Temporal' },
166
+ { name: 'Superior Temporal Sulcus', abbreviation: 'STS', lobe: 'Temporal' },
167
+ { name: 'Inferior Temporal Cortex', abbreviation: 'ITC', lobe: 'Temporal' },
168
+ { name: 'Hippocampus', abbreviation: 'HPC', lobe: 'Temporal (medial)' },
169
+ { name: 'Amygdala', abbreviation: 'AMY', lobe: 'Temporal (medial)' },
170
+ { name: 'Insula', abbreviation: 'INS', lobe: 'Insular' },
171
+ { name: 'Cingulate Cortex', abbreviation: 'CNG', lobe: 'Limbic' },
172
+ { name: 'Thalamus', abbreviation: 'THL', lobe: 'Diencephalon' },
173
+ { name: 'Basal Ganglia', abbreviation: 'BG', lobe: 'Subcortical' },
174
+ { name: 'Cerebellum', abbreviation: 'CBL', lobe: 'Hindbrain' },
175
+ { name: 'Brainstem', abbreviation: 'BS', lobe: 'Hindbrain' },
176
+ { name: 'Hypothalamus', abbreviation: 'HYP', lobe: 'Diencephalon' },
177
+ { name: 'Nucleus Accumbens', abbreviation: 'NAc', lobe: 'Subcortical' },
178
+ ];
179
+ // Adjacency matrix: connectivity[i][j] = strength (0-1, based on published DTI/tract-tracing data, simplified)
180
+ const CONNECTIVITY = [
181
+ // PFC M1 PMC S1 PPC V1 VAC A1 STS ITC HPC AMY INS CNG THL BG CBL BS HYP NAc
182
+ [0.0, 0.3, 0.7, 0.2, 0.6, 0.1, 0.3, 0.1, 0.4, 0.3, 0.5, 0.6, 0.6, 0.8, 0.7, 0.7, 0.2, 0.2, 0.4, 0.7], // PFC
183
+ [0.3, 0.0, 0.8, 0.7, 0.4, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.2, 0.3, 0.6, 0.7, 0.6, 0.5, 0.1, 0.1], // M1
184
+ [0.7, 0.8, 0.0, 0.5, 0.6, 0.1, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.2, 0.4, 0.5, 0.6, 0.5, 0.3, 0.1, 0.1], // PMC
185
+ [0.2, 0.7, 0.5, 0.0, 0.7, 0.2, 0.2, 0.1, 0.2, 0.2, 0.1, 0.1, 0.4, 0.3, 0.7, 0.3, 0.3, 0.3, 0.1, 0.1], // S1
186
+ [0.6, 0.4, 0.6, 0.7, 0.0, 0.3, 0.5, 0.1, 0.3, 0.3, 0.2, 0.2, 0.3, 0.4, 0.5, 0.3, 0.4, 0.2, 0.1, 0.1], // PPC
187
+ [0.1, 0.1, 0.1, 0.2, 0.3, 0.0, 0.8, 0.1, 0.2, 0.4, 0.1, 0.1, 0.1, 0.1, 0.6, 0.1, 0.1, 0.2, 0.0, 0.0], // V1
188
+ [0.3, 0.1, 0.2, 0.2, 0.5, 0.8, 0.0, 0.1, 0.4, 0.6, 0.2, 0.2, 0.2, 0.2, 0.4, 0.1, 0.1, 0.1, 0.0, 0.0], // VAC
189
+ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.5, 0.2, 0.1, 0.2, 0.3, 0.2, 0.6, 0.1, 0.1, 0.2, 0.0, 0.0], // A1
190
+ [0.4, 0.1, 0.2, 0.2, 0.3, 0.2, 0.4, 0.5, 0.0, 0.4, 0.3, 0.4, 0.3, 0.3, 0.4, 0.2, 0.1, 0.1, 0.1, 0.1], // STS
191
+ [0.3, 0.1, 0.1, 0.2, 0.3, 0.4, 0.6, 0.2, 0.4, 0.0, 0.4, 0.3, 0.2, 0.2, 0.4, 0.2, 0.1, 0.1, 0.1, 0.1], // ITC
192
+ [0.5, 0.1, 0.1, 0.1, 0.2, 0.1, 0.2, 0.1, 0.3, 0.4, 0.0, 0.7, 0.2, 0.5, 0.5, 0.2, 0.1, 0.1, 0.3, 0.3], // HPC
193
+ [0.6, 0.1, 0.1, 0.1, 0.2, 0.1, 0.2, 0.2, 0.4, 0.3, 0.7, 0.0, 0.5, 0.5, 0.5, 0.3, 0.1, 0.3, 0.5, 0.4], // AMY
194
+ [0.6, 0.2, 0.2, 0.4, 0.3, 0.1, 0.2, 0.3, 0.3, 0.2, 0.2, 0.5, 0.0, 0.6, 0.5, 0.3, 0.1, 0.3, 0.3, 0.2], // INS
195
+ [0.8, 0.3, 0.4, 0.3, 0.4, 0.1, 0.2, 0.2, 0.3, 0.2, 0.5, 0.5, 0.6, 0.0, 0.5, 0.4, 0.2, 0.2, 0.3, 0.3], // CNG
196
+ [0.7, 0.6, 0.5, 0.7, 0.5, 0.6, 0.4, 0.6, 0.4, 0.4, 0.5, 0.5, 0.5, 0.5, 0.0, 0.6, 0.5, 0.6, 0.5, 0.3], // THL
197
+ [0.7, 0.7, 0.6, 0.3, 0.3, 0.1, 0.1, 0.1, 0.2, 0.2, 0.2, 0.3, 0.3, 0.4, 0.6, 0.0, 0.4, 0.4, 0.2, 0.6], // BG
198
+ [0.2, 0.6, 0.5, 0.3, 0.4, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.2, 0.5, 0.4, 0.0, 0.7, 0.1, 0.1], // CBL
199
+ [0.2, 0.5, 0.3, 0.3, 0.2, 0.2, 0.1, 0.2, 0.1, 0.1, 0.1, 0.3, 0.3, 0.2, 0.6, 0.4, 0.7, 0.0, 0.5, 0.1], // BS
200
+ [0.4, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.1, 0.3, 0.5, 0.3, 0.3, 0.5, 0.2, 0.1, 0.5, 0.0, 0.3], // HYP
201
+ [0.7, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.1, 0.3, 0.4, 0.2, 0.3, 0.3, 0.6, 0.1, 0.1, 0.3, 0.0], // NAc
202
+ ];
203
+ const MNI_ATLAS = [
204
+ { name: 'Left Primary Motor Cortex (hand)', mni: { x: -37, y: -25, z: 62 }, talairach: { x: -36, y: -25, z: 57 }, brodmann: 'BA 4' },
205
+ { name: 'Right Primary Motor Cortex (hand)', mni: { x: 37, y: -25, z: 62 }, talairach: { x: 36, y: -25, z: 57 }, brodmann: 'BA 4' },
206
+ { name: 'Left Primary Somatosensory Cortex', mni: { x: -42, y: -30, z: 55 }, talairach: { x: -41, y: -30, z: 50 }, brodmann: 'BA 1,2,3' },
207
+ { name: 'Left DLPFC', mni: { x: -44, y: 36, z: 20 }, talairach: { x: -43, y: 34, z: 19 }, brodmann: 'BA 9/46' },
208
+ { name: 'Right DLPFC', mni: { x: 44, y: 36, z: 20 }, talairach: { x: 43, y: 34, z: 19 }, brodmann: 'BA 9/46' },
209
+ { name: 'Left VLPFC (Broca\'s area)', mni: { x: -48, y: 20, z: 8 }, talairach: { x: -47, y: 19, z: 8 }, brodmann: 'BA 44/45' },
210
+ { name: 'Medial Prefrontal Cortex', mni: { x: 0, y: 52, z: 6 }, talairach: { x: 0, y: 50, z: 7 }, brodmann: 'BA 10/32' },
211
+ { name: 'Orbitofrontal Cortex', mni: { x: 0, y: 42, z: -16 }, talairach: { x: 0, y: 41, z: -12 }, brodmann: 'BA 11' },
212
+ { name: 'Anterior Cingulate Cortex (dorsal)', mni: { x: 0, y: 24, z: 32 }, talairach: { x: 0, y: 23, z: 30 }, brodmann: 'BA 32' },
213
+ { name: 'Posterior Cingulate Cortex', mni: { x: 0, y: -50, z: 28 }, talairach: { x: 0, y: -49, z: 26 }, brodmann: 'BA 23/31' },
214
+ { name: 'Left Hippocampus', mni: { x: -28, y: -20, z: -12 }, talairach: { x: -27, y: -20, z: -10 } },
215
+ { name: 'Right Hippocampus', mni: { x: 28, y: -20, z: -12 }, talairach: { x: 27, y: -20, z: -10 } },
216
+ { name: 'Left Amygdala', mni: { x: -24, y: -4, z: -18 }, talairach: { x: -23, y: -4, z: -15 } },
217
+ { name: 'Right Amygdala', mni: { x: 24, y: -4, z: -18 }, talairach: { x: 23, y: -4, z: -15 } },
218
+ { name: 'Left Insula (anterior)', mni: { x: -36, y: 16, z: 2 }, talairach: { x: -35, y: 15, z: 3 }, brodmann: 'BA 13' },
219
+ { name: 'Right Insula (anterior)', mni: { x: 36, y: 16, z: 2 }, talairach: { x: 35, y: 15, z: 3 }, brodmann: 'BA 13' },
220
+ { name: 'Left Thalamus', mni: { x: -10, y: -18, z: 8 }, talairach: { x: -10, y: -18, z: 8 } },
221
+ { name: 'Right Thalamus', mni: { x: 10, y: -18, z: 8 }, talairach: { x: 10, y: -18, z: 8 } },
222
+ { name: 'Left Caudate (head)', mni: { x: -12, y: 12, z: 8 }, talairach: { x: -12, y: 11, z: 8 } },
223
+ { name: 'Right Caudate (head)', mni: { x: 12, y: 12, z: 8 }, talairach: { x: 12, y: 11, z: 8 } },
224
+ { name: 'Left Putamen', mni: { x: -26, y: 4, z: 2 }, talairach: { x: -25, y: 4, z: 3 } },
225
+ { name: 'Right Putamen', mni: { x: 26, y: 4, z: 2 }, talairach: { x: 25, y: 4, z: 3 } },
226
+ { name: 'Left Globus Pallidus', mni: { x: -18, y: -2, z: 0 }, talairach: { x: -17, y: -2, z: 1 } },
227
+ { name: 'Left Nucleus Accumbens', mni: { x: -10, y: 10, z: -8 }, talairach: { x: -10, y: 10, z: -6 } },
228
+ { name: 'Right Nucleus Accumbens', mni: { x: 10, y: 10, z: -8 }, talairach: { x: 10, y: 10, z: -6 } },
229
+ { name: 'Primary Visual Cortex (V1)', mni: { x: 0, y: -84, z: 4 }, talairach: { x: 0, y: -82, z: 4 }, brodmann: 'BA 17' },
230
+ { name: 'Left V4 (color)', mni: { x: -30, y: -72, z: -12 }, talairach: { x: -29, y: -70, z: -10 }, brodmann: 'BA 19' },
231
+ { name: 'Left V5/MT (motion)', mni: { x: -44, y: -68, z: 0 }, talairach: { x: -43, y: -66, z: 1 }, brodmann: 'BA 19/37' },
232
+ { name: 'Left Primary Auditory Cortex', mni: { x: -48, y: -22, z: 8 }, talairach: { x: -47, y: -22, z: 8 }, brodmann: 'BA 41' },
233
+ { name: 'Left Wernicke\'s Area', mni: { x: -56, y: -42, z: 14 }, talairach: { x: -55, y: -41, z: 14 }, brodmann: 'BA 22' },
234
+ { name: 'Left Fusiform Gyrus (FFA)', mni: { x: -40, y: -54, z: -18 }, talairach: { x: -39, y: -53, z: -15 }, brodmann: 'BA 37' },
235
+ { name: 'Right Fusiform Gyrus (FFA)', mni: { x: 40, y: -54, z: -18 }, talairach: { x: 39, y: -53, z: -15 }, brodmann: 'BA 37' },
236
+ { name: 'Left Angular Gyrus', mni: { x: -44, y: -62, z: 36 }, talairach: { x: -43, y: -60, z: 34 }, brodmann: 'BA 39' },
237
+ { name: 'Left Supramarginal Gyrus', mni: { x: -56, y: -40, z: 36 }, talairach: { x: -55, y: -39, z: 34 }, brodmann: 'BA 40' },
238
+ { name: 'Precuneus', mni: { x: 0, y: -64, z: 44 }, talairach: { x: 0, y: -62, z: 42 }, brodmann: 'BA 7/31' },
239
+ { name: 'Left SMA', mni: { x: -4, y: -2, z: 60 }, talairach: { x: -4, y: -2, z: 55 }, brodmann: 'BA 6' },
240
+ { name: 'Left Frontal Eye Field', mni: { x: -30, y: -4, z: 52 }, talairach: { x: -29, y: -4, z: 48 }, brodmann: 'BA 8' },
241
+ { name: 'Left Parahippocampal Gyrus', mni: { x: -24, y: -32, z: -12 }, talairach: { x: -23, y: -31, z: -10 }, brodmann: 'BA 36' },
242
+ { name: 'Left Entorhinal Cortex', mni: { x: -22, y: -8, z: -26 }, talairach: { x: -21, y: -8, z: -22 }, brodmann: 'BA 28' },
243
+ { name: 'Hypothalamus', mni: { x: 0, y: -4, z: -10 }, talairach: { x: 0, y: -4, z: -8 } },
244
+ { name: 'Substantia Nigra', mni: { x: -10, y: -16, z: -10 }, talairach: { x: -10, y: -16, z: -8 } },
245
+ { name: 'VTA', mni: { x: -4, y: -16, z: -14 }, talairach: { x: -4, y: -16, z: -12 } },
246
+ { name: 'Periaqueductal Gray', mni: { x: 0, y: -32, z: -6 }, talairach: { x: 0, y: -31, z: -5 } },
247
+ { name: 'Superior Colliculus', mni: { x: 0, y: -32, z: -2 }, talairach: { x: 0, y: -31, z: -1 } },
248
+ { name: 'Cerebellar Vermis', mni: { x: 0, y: -62, z: -30 }, talairach: { x: 0, y: -60, z: -26 } },
249
+ { name: 'Left Cerebellar Hemisphere', mni: { x: -30, y: -66, z: -30 }, talairach: { x: -29, y: -64, z: -26 } },
250
+ { name: 'Right Cerebellar Hemisphere', mni: { x: 30, y: -66, z: -30 }, talairach: { x: 29, y: -64, z: -26 } },
251
+ { name: 'Pons', mni: { x: 0, y: -24, z: -32 }, talairach: { x: 0, y: -24, z: -28 } },
252
+ { name: 'Medulla', mni: { x: 0, y: -36, z: -48 }, talairach: { x: 0, y: -35, z: -43 } },
253
+ { name: 'Left Locus Coeruleus', mni: { x: -4, y: -36, z: -26 }, talairach: { x: -4, y: -35, z: -23 } },
254
+ ];
255
+ // ─── Registration ────────────────────────────────────────────────────────────
256
+ export function registerLabNeuroTools() {
257
+ // ════════════════════════════════════════════════════════════════════════════
258
+ // 1. Brain Atlas
259
+ // ════════════════════════════════════════════════════════════════════════════
260
+ registerTool({
261
+ name: 'brain_atlas',
262
+ description: 'Look up brain regions by name, function, or associated disorder. Returns detailed neuroanatomical information including Brodmann areas, functions, disorders, and connectivity for ~100 brain structures.',
263
+ parameters: {
264
+ query: { type: 'string', description: 'Region name, function, or disorder to search for', required: true },
265
+ search_type: { type: 'string', description: 'Search type: region (by name), function (by function), disorder (by associated disorder). Default: region' },
266
+ },
267
+ tier: 'free',
268
+ async execute(args) {
269
+ const query = String(args.query).toLowerCase();
270
+ const searchType = String(args.search_type || 'region').toLowerCase();
271
+ const matches = [];
272
+ for (const [key, region] of Object.entries(BRAIN_ATLAS)) {
273
+ let match = false;
274
+ if (searchType === 'region' || searchType === 'name') {
275
+ match = key.includes(query) ||
276
+ region.name.toLowerCase().includes(query) ||
277
+ (region.brodmann?.toLowerCase().includes(query) ?? false);
278
+ }
279
+ else if (searchType === 'function') {
280
+ match = region.functions.some(f => f.toLowerCase().includes(query));
281
+ }
282
+ else if (searchType === 'disorder') {
283
+ match = region.disorders.some(d => d.toLowerCase().includes(query));
284
+ }
285
+ else {
286
+ // Search all fields
287
+ match = key.includes(query) ||
288
+ region.name.toLowerCase().includes(query) ||
289
+ region.functions.some(f => f.toLowerCase().includes(query)) ||
290
+ region.disorders.some(d => d.toLowerCase().includes(query));
291
+ }
292
+ if (match)
293
+ matches.push(region);
294
+ }
295
+ if (matches.length === 0) {
296
+ return `No brain regions found matching "${args.query}" (search_type: ${searchType}).\n\nTry broader terms or switch search_type (region/function/disorder).`;
297
+ }
298
+ const parts = [`## Brain Atlas Results (${matches.length} matches for "${args.query}")\n`];
299
+ for (const r of matches.slice(0, 15)) {
300
+ parts.push(`### ${r.name}`);
301
+ parts.push(`- **Location**: ${r.location}`);
302
+ if (r.brodmann)
303
+ parts.push(`- **Brodmann Area**: ${r.brodmann}`);
304
+ parts.push(`- **Functions**: ${r.functions.join(', ')}`);
305
+ parts.push(`- **Associated Disorders**: ${r.disorders.join(', ')}`);
306
+ parts.push(`- **Connections**: ${r.connections.join(', ')}`);
307
+ parts.push('');
308
+ }
309
+ if (matches.length > 15) {
310
+ parts.push(`\n*...and ${matches.length - 15} more matches. Narrow your query for more specific results.*`);
311
+ }
312
+ return parts.join('\n');
313
+ },
314
+ });
315
+ // ════════════════════════════════════════════════════════════════════════════
316
+ // 2. EEG Analyze
317
+ // ════════════════════════════════════════════════════════════════════════════
318
+ registerTool({
319
+ name: 'eeg_analyze',
320
+ description: 'Analyze EEG-like time series data. Computes power spectral density (FFT), band power (delta/theta/alpha/beta/gamma), peak frequency, spectral edge frequency, and clinical band ratios (theta/beta for ADHD, alpha asymmetry).',
321
+ parameters: {
322
+ signal: { type: 'string', description: 'Comma-separated signal samples (amplitude values)', required: true },
323
+ sample_rate: { type: 'number', description: 'Sampling rate in Hz (default: 256)' },
324
+ analysis: { type: 'string', description: 'Analysis type: spectrum, bands, ratios, all (default: all)' },
325
+ },
326
+ tier: 'free',
327
+ async execute(args) {
328
+ const samples = String(args.signal).split(',').map(s => parseFloat(s.trim())).filter(n => !isNaN(n));
329
+ const fs = typeof args.sample_rate === 'number' ? args.sample_rate : 256;
330
+ const analysis = String(args.analysis || 'all').toLowerCase();
331
+ if (samples.length < 8) {
332
+ return 'Error: Need at least 8 samples for spectral analysis.';
333
+ }
334
+ // Zero-pad to next power of 2
335
+ let N = 1;
336
+ while (N < samples.length)
337
+ N *= 2;
338
+ const padded = new Array(N).fill(0);
339
+ for (let i = 0; i < samples.length; i++)
340
+ padded[i] = samples[i];
341
+ // Remove DC offset
342
+ const mean = padded.reduce((a, b) => a + b, 0) / samples.length;
343
+ for (let i = 0; i < samples.length; i++)
344
+ padded[i] -= mean;
345
+ // Apply Hanning window
346
+ for (let i = 0; i < samples.length; i++) {
347
+ padded[i] *= 0.5 * (1 - Math.cos(2 * Math.PI * i / (samples.length - 1)));
348
+ }
349
+ // FFT (Cooley-Tukey radix-2)
350
+ function fft(re, im) {
351
+ const n = re.length;
352
+ // Bit-reversal permutation
353
+ for (let i = 1, j = 0; i < n; i++) {
354
+ let bit = n >> 1;
355
+ for (; j & bit; bit >>= 1)
356
+ j ^= bit;
357
+ j ^= bit;
358
+ if (i < j) {
359
+ [re[i], re[j]] = [re[j], re[i]];
360
+ [im[i], im[j]] = [im[j], im[i]];
361
+ }
362
+ }
363
+ // Butterfly operations
364
+ for (let len = 2; len <= n; len *= 2) {
365
+ const ang = -2 * Math.PI / len;
366
+ const wRe = Math.cos(ang);
367
+ const wIm = Math.sin(ang);
368
+ for (let i = 0; i < n; i += len) {
369
+ let curRe = 1, curIm = 0;
370
+ for (let j = 0; j < len / 2; j++) {
371
+ const uRe = re[i + j], uIm = im[i + j];
372
+ const vRe = re[i + j + len / 2] * curRe - im[i + j + len / 2] * curIm;
373
+ const vIm = re[i + j + len / 2] * curIm + im[i + j + len / 2] * curRe;
374
+ re[i + j] = uRe + vRe;
375
+ im[i + j] = uIm + vIm;
376
+ re[i + j + len / 2] = uRe - vRe;
377
+ im[i + j + len / 2] = uIm - vIm;
378
+ const newCurRe = curRe * wRe - curIm * wIm;
379
+ curIm = curRe * wIm + curIm * wRe;
380
+ curRe = newCurRe;
381
+ }
382
+ }
383
+ }
384
+ }
385
+ const re = [...padded];
386
+ const im = new Array(N).fill(0);
387
+ fft(re, im);
388
+ // Power spectral density (one-sided)
389
+ const nFreqs = Math.floor(N / 2) + 1;
390
+ const freqRes = fs / N;
391
+ const psd = [];
392
+ const freqs = [];
393
+ for (let i = 0; i < nFreqs; i++) {
394
+ freqs.push(i * freqRes);
395
+ let power = (re[i] * re[i] + im[i] * im[i]) / (N * N);
396
+ if (i > 0 && i < N / 2)
397
+ power *= 2; // one-sided
398
+ psd.push(power);
399
+ }
400
+ // Band power computation
401
+ function bandPower(fLow, fHigh) {
402
+ let sum = 0;
403
+ for (let i = 0; i < nFreqs; i++) {
404
+ if (freqs[i] >= fLow && freqs[i] < fHigh)
405
+ sum += psd[i];
406
+ }
407
+ return sum * freqRes;
408
+ }
409
+ const bands = {
410
+ delta: bandPower(0.5, 4),
411
+ theta: bandPower(4, 8),
412
+ alpha: bandPower(8, 13),
413
+ beta: bandPower(13, 30),
414
+ gamma: bandPower(30, Math.min(100, fs / 2)),
415
+ };
416
+ const totalPower = Object.values(bands).reduce((a, b) => a + b, 0);
417
+ // Peak frequency (highest power in 1-50 Hz range)
418
+ let peakFreq = 0;
419
+ let peakPower = 0;
420
+ for (let i = 0; i < nFreqs; i++) {
421
+ if (freqs[i] >= 1 && freqs[i] <= 50 && psd[i] > peakPower) {
422
+ peakPower = psd[i];
423
+ peakFreq = freqs[i];
424
+ }
425
+ }
426
+ // Spectral edge frequency (95% of power)
427
+ let cumPower = 0;
428
+ const totalPSD = psd.reduce((a, b) => a + b, 0) * freqRes;
429
+ let sef95 = freqs[nFreqs - 1];
430
+ for (let i = 0; i < nFreqs; i++) {
431
+ cumPower += psd[i] * freqRes;
432
+ if (cumPower >= 0.95 * totalPSD) {
433
+ sef95 = freqs[i];
434
+ break;
435
+ }
436
+ }
437
+ const parts = ['## EEG Analysis Results\n'];
438
+ parts.push(`- **Samples**: ${samples.length} | **Sample rate**: ${fs} Hz | **Duration**: ${fmt(samples.length / fs, 3)} s`);
439
+ parts.push(`- **Frequency resolution**: ${fmt(freqRes, 3)} Hz | **Nyquist**: ${fs / 2} Hz`);
440
+ parts.push('');
441
+ if (analysis === 'spectrum' || analysis === 'all') {
442
+ parts.push('### Power Spectral Density (top 20 frequencies)');
443
+ const sorted = freqs.map((f, i) => ({ freq: f, power: psd[i] }))
444
+ .filter(p => p.freq >= 0.5 && p.freq <= Math.min(100, fs / 2))
445
+ .sort((a, b) => b.power - a.power)
446
+ .slice(0, 20);
447
+ parts.push('| Frequency (Hz) | Power (uV^2/Hz) | Relative % |');
448
+ parts.push('|---|---|---|');
449
+ for (const p of sorted) {
450
+ parts.push(`| ${fmt(p.freq, 4)} | ${fmt(p.power, 4)} | ${fmt(totalPSD > 0 ? (p.power / totalPSD) * 100 : 0, 3)}% |`);
451
+ }
452
+ parts.push('');
453
+ }
454
+ if (analysis === 'bands' || analysis === 'all') {
455
+ parts.push('### Band Power');
456
+ parts.push('| Band | Range (Hz) | Absolute Power | Relative % | Typical State |');
457
+ parts.push('|---|---|---|---|---|');
458
+ parts.push(`| Delta | 0.5-4 | ${fmt(bands.delta, 4)} | ${fmt(totalPower > 0 ? (bands.delta / totalPower) * 100 : 0, 3)}% | Deep sleep, unconscious |`);
459
+ parts.push(`| Theta | 4-8 | ${fmt(bands.theta, 4)} | ${fmt(totalPower > 0 ? (bands.theta / totalPower) * 100 : 0, 3)}% | Drowsy, meditation, memory |`);
460
+ parts.push(`| Alpha | 8-13 | ${fmt(bands.alpha, 4)} | ${fmt(totalPower > 0 ? (bands.alpha / totalPower) * 100 : 0, 3)}% | Relaxed, eyes closed |`);
461
+ parts.push(`| Beta | 13-30 | ${fmt(bands.beta, 4)} | ${fmt(totalPower > 0 ? (bands.beta / totalPower) * 100 : 0, 3)}% | Alert, active thinking |`);
462
+ parts.push(`| Gamma | 30-100 | ${fmt(bands.gamma, 4)} | ${fmt(totalPower > 0 ? (bands.gamma / totalPower) * 100 : 0, 3)}% | Cognitive binding, consciousness |`);
463
+ parts.push('');
464
+ parts.push(`- **Peak frequency**: ${fmt(peakFreq, 4)} Hz`);
465
+ parts.push(`- **Spectral edge (95%)**: ${fmt(sef95, 4)} Hz`);
466
+ parts.push(`- **Total band power**: ${fmt(totalPower, 4)}`);
467
+ parts.push('');
468
+ }
469
+ if (analysis === 'ratios' || analysis === 'all') {
470
+ parts.push('### Clinical Ratios');
471
+ const thetaBeta = bands.beta > 0 ? bands.theta / bands.beta : Infinity;
472
+ const alphabeta = bands.beta > 0 ? bands.alpha / bands.beta : Infinity;
473
+ const thetaAlpha = bands.alpha > 0 ? bands.theta / bands.alpha : Infinity;
474
+ const deltaAlpha = bands.alpha > 0 ? bands.delta / bands.alpha : Infinity;
475
+ parts.push(`| Ratio | Value | Clinical Relevance |`);
476
+ parts.push(`|---|---|---|`);
477
+ parts.push(`| Theta/Beta | ${fmt(thetaBeta, 3)} | ADHD marker (elevated >3.0 in children) |`);
478
+ parts.push(`| Alpha/Beta | ${fmt(alphabeta, 3)} | Relaxation vs alertness index |`);
479
+ parts.push(`| Theta/Alpha | ${fmt(thetaAlpha, 3)} | Drowsiness index (elevated = drowsy) |`);
480
+ parts.push(`| Delta/Alpha | ${fmt(deltaAlpha, 3)} | Encephalopathy marker (elevated = abnormal) |`);
481
+ parts.push('');
482
+ // Dominant band
483
+ const bandEntries = Object.entries(bands);
484
+ bandEntries.sort((a, b) => b[1] - a[1]);
485
+ parts.push(`**Dominant band**: ${bandEntries[0][0]} (${fmt((bandEntries[0][1] / totalPower) * 100, 3)}% of total power)`);
486
+ }
487
+ return parts.join('\n');
488
+ },
489
+ });
490
+ // ════════════════════════════════════════════════════════════════════════════
491
+ // 3. Cognitive Model
492
+ // ════════════════════════════════════════════════════════════════════════════
493
+ registerTool({
494
+ name: 'cognitive_model',
495
+ description: 'Implement classic cognitive science models: Hick\'s law (RT vs choices), Fitts\'s law (movement time), Stevens\' power law, Weber-Fechner law, signal detection theory (d-prime), drift-diffusion model.',
496
+ parameters: {
497
+ model: { type: 'string', description: 'Model: hicks, fitts, stevens, weber, sdt, ddm', required: true },
498
+ params: { type: 'string', description: 'JSON with model-specific parameters (see description)', required: true },
499
+ },
500
+ tier: 'free',
501
+ async execute(args) {
502
+ const model = String(args.model).toLowerCase();
503
+ const p = safeJSON(String(args.params));
504
+ if (!p)
505
+ return 'Error: params must be valid JSON.';
506
+ const parts = [];
507
+ if (model === 'hicks' || model === 'hick') {
508
+ // Hick's Law: RT = a + b * log2(n)
509
+ const n = Number(p.n || p.choices || 2);
510
+ const a = Number(p.a || p.intercept || 200); // ms
511
+ const b = Number(p.b || p.slope || 150); // ms/bit
512
+ const items = typeof p.items === 'object' && Array.isArray(p.items) ? p.items.map(Number) : [1, 2, 4, 8, 16, 32];
513
+ parts.push("## Hick's Law");
514
+ parts.push(`**Formula**: RT = a + b * log₂(n + 1)`);
515
+ parts.push(`**Parameters**: a = ${a} ms (intercept), b = ${b} ms/bit (slope)`);
516
+ parts.push('');
517
+ parts.push('| Choices (n) | Information (bits) | Predicted RT (ms) |');
518
+ parts.push('|---|---|---|');
519
+ for (const ni of items) {
520
+ const bits = Math.log2(ni + 1);
521
+ const rt = a + b * bits;
522
+ parts.push(`| ${ni} | ${fmt(bits, 3)} | ${fmt(rt, 4)} |`);
523
+ }
524
+ parts.push('');
525
+ parts.push(`**For n=${n}**: RT = ${fmt(a + b * Math.log2(n + 1), 4)} ms (${fmt(Math.log2(n + 1), 3)} bits)`);
526
+ parts.push('');
527
+ parts.push('*Note: Uses Hyman (1953) formulation with n+1 to account for uncertainty*');
528
+ }
529
+ else if (model === 'fitts') {
530
+ // Fitts's Law: MT = a + b * log2(2D/W)
531
+ const D = Number(p.D || p.distance || 100); // distance (px or mm)
532
+ const W = Number(p.W || p.width || 10); // target width
533
+ const a = Number(p.a || p.intercept || 50); // ms
534
+ const b = Number(p.b || p.slope || 150); // ms/bit
535
+ const ID = Math.log2(2 * D / W);
536
+ const MT = a + b * ID;
537
+ parts.push("## Fitts's Law");
538
+ parts.push(`**Formula**: MT = a + b * log₂(2D/W) = a + b * ID`);
539
+ parts.push(`**Parameters**: a = ${a} ms, b = ${b} ms/bit`);
540
+ parts.push(`**Input**: D = ${D}, W = ${W}`);
541
+ parts.push('');
542
+ parts.push(`- **Index of Difficulty (ID)**: ${fmt(ID, 4)} bits`);
543
+ parts.push(`- **Movement Time (MT)**: ${fmt(MT, 4)} ms`);
544
+ parts.push(`- **Throughput (IP)**: ${fmt(ID / (MT / 1000), 4)} bits/s`);
545
+ parts.push('');
546
+ // Generate table for varying distances
547
+ parts.push('### Predicted MT for varying distances');
548
+ parts.push('| Distance | ID (bits) | MT (ms) |');
549
+ parts.push('|---|---|---|');
550
+ for (const d of [50, 100, 200, 400, 800]) {
551
+ const id = Math.log2(2 * d / W);
552
+ parts.push(`| ${d} | ${fmt(id, 3)} | ${fmt(a + b * id, 4)} |`);
553
+ }
554
+ parts.push('');
555
+ parts.push('*Shannon formulation (MacKenzie, 1992): ID = log₂(D/W + 1)*');
556
+ }
557
+ else if (model === 'stevens') {
558
+ // Stevens' Power Law: ψ = k * S^n
559
+ const S = Number(p.S || p.stimulus || p.intensity || 100);
560
+ const n = Number(p.n || p.exponent || 1.0);
561
+ const k = Number(p.k || p.constant || 1.0);
562
+ const modality = String(p.modality || 'custom');
563
+ // Known exponents for sensory modalities
564
+ const exponents = {
565
+ brightness: { n: 0.33, desc: 'Brightness (5° target in dark)' },
566
+ loudness: { n: 0.67, desc: 'Loudness (3000 Hz tone)' },
567
+ vibration: { n: 0.95, desc: 'Vibration (60 Hz on finger)' },
568
+ taste_salt: { n: 1.3, desc: 'Taste (salt)' },
569
+ heaviness: { n: 1.45, desc: 'Heaviness (lifted weights)' },
570
+ length: { n: 1.0, desc: 'Visual length' },
571
+ electric_shock: { n: 3.5, desc: 'Electric shock (60 Hz through fingers)' },
572
+ temperature_warmth: { n: 1.6, desc: 'Temperature (warmth on arm)' },
573
+ temperature_cold: { n: 1.0, desc: 'Temperature (cold on arm)' },
574
+ pressure: { n: 1.1, desc: 'Pressure on palm' },
575
+ smell: { n: 0.6, desc: 'Smell (heptane)' },
576
+ };
577
+ parts.push("## Stevens' Power Law");
578
+ parts.push(`**Formula**: \u03C8 = k \u00D7 S^n`);
579
+ parts.push(`**Parameters**: k = ${k}, n = ${n}`);
580
+ parts.push(`**Stimulus intensity**: S = ${S}`);
581
+ parts.push(`**Perceived magnitude**: \u03C8 = ${fmt(k * Math.pow(S, n), 6)}`);
582
+ parts.push('');
583
+ if (modality !== 'custom' && exponents[modality]) {
584
+ const e = exponents[modality];
585
+ parts.push(`**Modality**: ${e.desc} (exponent = ${e.n})`);
586
+ parts.push(`**Perceived magnitude** (standard exponent): ${fmt(k * Math.pow(S, e.n), 6)}`);
587
+ parts.push('');
588
+ }
589
+ parts.push('### Known Exponents by Modality');
590
+ parts.push('| Modality | Exponent (n) | Effect |');
591
+ parts.push('|---|---|---|');
592
+ for (const [, e] of Object.entries(exponents)) {
593
+ const effect = e.n < 1 ? 'Compressive (diminishing returns)' : e.n === 1 ? 'Linear' : 'Expansive (accelerating)';
594
+ parts.push(`| ${e.desc} | ${e.n} | ${effect} |`);
595
+ }
596
+ }
597
+ else if (model === 'weber' || model === 'weber_fechner' || model === 'fechner') {
598
+ // Weber-Fechner: ΔI/I = k (Weber), ψ = k * ln(S/S0) (Fechner)
599
+ const I = Number(p.I || p.intensity || p.stimulus || 100);
600
+ const deltaI = Number(p.deltaI || p.jnd || 0);
601
+ const k = Number(p.k || p.weber_fraction || 0);
602
+ const S0 = Number(p.S0 || p.threshold || 1);
603
+ const knownFractions = {
604
+ brightness: { k: 0.079, desc: 'Brightness' },
605
+ loudness: { k: 0.048, desc: 'Loudness (1000 Hz)' },
606
+ weight: { k: 0.020, desc: 'Heaviness (lifted weights)' },
607
+ pitch: { k: 0.003, desc: 'Pitch (2000 Hz)' },
608
+ taste_salt: { k: 0.083, desc: 'Taste (salt concentration)' },
609
+ smell: { k: 0.104, desc: 'Smell (rubber)' },
610
+ vibration: { k: 0.036, desc: 'Vibration (230 Hz)' },
611
+ pressure: { k: 0.136, desc: 'Pressure on skin' },
612
+ line_length: { k: 0.029, desc: 'Visual line length' },
613
+ electric_shock: { k: 0.013, desc: 'Electric shock' },
614
+ };
615
+ parts.push('## Weber-Fechner Law');
616
+ parts.push('');
617
+ parts.push("**Weber's Law**: \u0394I / I = k (constant)");
618
+ parts.push("**Fechner's Law**: \u03C8 = c \u00D7 ln(S / S\u2080)");
619
+ parts.push('');
620
+ if (k > 0) {
621
+ parts.push(`**Weber fraction (k)**: ${k}`);
622
+ parts.push(`**Stimulus intensity (I)**: ${I}`);
623
+ parts.push(`**JND (\u0394I)**: ${fmt(I * k, 6)}`);
624
+ }
625
+ else if (deltaI > 0) {
626
+ parts.push(`**Stimulus intensity (I)**: ${I}`);
627
+ parts.push(`**JND (\u0394I)**: ${deltaI}`);
628
+ parts.push(`**Weber fraction (k)**: ${fmt(deltaI / I, 6)}`);
629
+ }
630
+ parts.push('');
631
+ parts.push(`**Fechner perceived magnitude**: \u03C8 = ${fmt(Math.log(I / S0), 6)} (with S\u2080 = ${S0})`);
632
+ parts.push('');
633
+ parts.push('### JND at Different Intensities (k = ' + (k || 0.05) + ')');
634
+ parts.push('| Intensity | JND | Fechner \u03C8 |');
635
+ parts.push('|---|---|---|');
636
+ const wk = k || 0.05;
637
+ for (const intensity of [10, 25, 50, 100, 250, 500, 1000]) {
638
+ parts.push(`| ${intensity} | ${fmt(intensity * wk, 4)} | ${fmt(Math.log(intensity / S0), 4)} |`);
639
+ }
640
+ parts.push('');
641
+ parts.push('### Known Weber Fractions');
642
+ parts.push('| Modality | Weber Fraction | Sensitivity |');
643
+ parts.push('|---|---|---|');
644
+ for (const [, f] of Object.entries(knownFractions)) {
645
+ parts.push(`| ${f.desc} | ${f.k} | ${f.k < 0.05 ? 'High' : f.k < 0.1 ? 'Medium' : 'Low'} |`);
646
+ }
647
+ }
648
+ else if (model === 'sdt' || model === 'signal_detection') {
649
+ // Signal Detection Theory
650
+ const hits = Number(p.hits || 0);
651
+ const misses = Number(p.misses || 0);
652
+ const falseAlarms = Number(p.false_alarms || p.fa || 0);
653
+ const correctRejections = Number(p.correct_rejections || p.cr || 0);
654
+ const signalTrials = hits + misses;
655
+ const noiseTrials = falseAlarms + correctRejections;
656
+ if (signalTrials === 0 || noiseTrials === 0) {
657
+ return 'Error: Need non-zero signal trials (hits+misses) and noise trials (false_alarms+correct_rejections).';
658
+ }
659
+ // Hit rate and false alarm rate (with correction for 0 and 1)
660
+ let HR = hits / signalTrials;
661
+ let FAR = falseAlarms / noiseTrials;
662
+ // Log-linear correction (Hautus, 1995)
663
+ if (HR === 0)
664
+ HR = 0.5 / signalTrials;
665
+ if (HR === 1)
666
+ HR = 1 - 0.5 / signalTrials;
667
+ if (FAR === 0)
668
+ FAR = 0.5 / noiseTrials;
669
+ if (FAR === 1)
670
+ FAR = 1 - 0.5 / noiseTrials;
671
+ // z-score (probit) via rational approximation
672
+ function normInv(p) {
673
+ // Abramowitz & Stegun approximation
674
+ if (p <= 0)
675
+ return -8;
676
+ if (p >= 1)
677
+ return 8;
678
+ const t = p < 0.5 ? p : 1 - p;
679
+ const s = Math.sqrt(-2 * Math.log(t));
680
+ const c0 = 2.515517, c1 = 0.802853, c2 = 0.010328;
681
+ const d1 = 1.432788, d2 = 0.189269, d3 = 0.001308;
682
+ let z = s - (c0 + c1 * s + c2 * s * s) / (1 + d1 * s + d2 * s * s + d3 * s * s * s);
683
+ if (p < 0.5)
684
+ z = -z;
685
+ return z;
686
+ }
687
+ const zHR = normInv(HR);
688
+ const zFAR = normInv(FAR);
689
+ const dPrime = zHR - zFAR;
690
+ const criterion = -0.5 * (zHR + zFAR);
691
+ const beta = Math.exp(-0.5 * (zHR * zHR - zFAR * zFAR));
692
+ const accuracy = (hits + correctRejections) / (signalTrials + noiseTrials);
693
+ // A' (nonparametric sensitivity)
694
+ let aPrime;
695
+ if (HR >= FAR) {
696
+ aPrime = 0.5 + ((HR - FAR) * (1 + HR - FAR)) / (4 * HR * (1 - FAR));
697
+ }
698
+ else {
699
+ aPrime = 0.5 - ((FAR - HR) * (1 + FAR - HR)) / (4 * FAR * (1 - HR));
700
+ }
701
+ parts.push('## Signal Detection Theory Analysis');
702
+ parts.push('');
703
+ parts.push('### Confusion Matrix');
704
+ parts.push('| | Signal Present | Signal Absent |');
705
+ parts.push('|---|---|---|');
706
+ parts.push(`| **"Yes"** | Hits: ${hits} | False Alarms: ${falseAlarms} |`);
707
+ parts.push(`| **"No"** | Misses: ${misses} | Correct Rejections: ${correctRejections} |`);
708
+ parts.push('');
709
+ parts.push('### Rates');
710
+ parts.push(`- **Hit Rate (HR)**: ${fmt(HR, 4)}`);
711
+ parts.push(`- **False Alarm Rate (FAR)**: ${fmt(FAR, 4)}`);
712
+ parts.push(`- **Miss Rate**: ${fmt(1 - HR, 4)}`);
713
+ parts.push(`- **Correct Rejection Rate**: ${fmt(1 - FAR, 4)}`);
714
+ parts.push(`- **Overall Accuracy**: ${fmt(accuracy * 100, 4)}%`);
715
+ parts.push('');
716
+ parts.push('### SDT Measures');
717
+ parts.push(`- **d\' (sensitivity)**: ${fmt(dPrime, 4)} — ${dPrime > 2 ? 'High' : dPrime > 1 ? 'Moderate' : dPrime > 0 ? 'Low' : 'At chance'} discriminability`);
718
+ parts.push(`- **c (criterion)**: ${fmt(criterion, 4)} — ${criterion > 0.5 ? 'Conservative' : criterion < -0.5 ? 'Liberal' : 'Neutral'} bias`);
719
+ parts.push(`- **\u03B2 (likelihood ratio)**: ${fmt(beta, 4)} — ${beta > 1 ? 'Conservative' : beta < 1 ? 'Liberal' : 'Optimal'} bias`);
720
+ parts.push(`- **A\' (nonparametric)**: ${fmt(aPrime, 4)}`);
721
+ parts.push('');
722
+ parts.push('### Interpretation');
723
+ if (dPrime > 2) {
724
+ parts.push('The observer shows strong ability to distinguish signal from noise.');
725
+ }
726
+ else if (dPrime > 1) {
727
+ parts.push('The observer shows moderate discriminability. Performance is above chance but imperfect.');
728
+ }
729
+ else if (dPrime > 0) {
730
+ parts.push('The observer shows weak discriminability. Near-chance performance.');
731
+ }
732
+ else {
733
+ parts.push('The observer cannot distinguish signal from noise (d\' ~ 0).');
734
+ }
735
+ }
736
+ else if (model === 'ddm' || model === 'drift_diffusion') {
737
+ // Drift-Diffusion Model (simplified analytical solution)
738
+ const v = Number(p.v || p.drift || 0.3); // drift rate
739
+ const a = Number(p.a || p.threshold || 1.0); // decision boundary
740
+ const z = Number(p.z || p.bias || 0.5); // starting point (0-1, proportion of a)
741
+ const t0 = Number(p.t0 || p.nondecision || 0.3); // non-decision time (s)
742
+ const s = Number(p.s || p.noise || 0.1); // diffusion coefficient
743
+ const zAbs = z * a;
744
+ // Analytical solutions (for unbiased start, z=a/2)
745
+ // Mean RT for correct responses (approximation)
746
+ const meanRT_correct = t0 + (a / (2 * v)) * Math.tanh(v * a / (2 * s * s));
747
+ // Accuracy (probability of correct response)
748
+ const accuracy = 1 / (1 + Math.exp(-2 * v * a / (s * s)));
749
+ // Simple simulation
750
+ const nSim = 2000;
751
+ const dt = 0.001;
752
+ let correctCount = 0;
753
+ let totalRT = 0;
754
+ let totalCorrectRT = 0;
755
+ let totalErrorRT = 0;
756
+ let errorCount = 0;
757
+ const rtDist = [];
758
+ for (let trial = 0; trial < nSim; trial++) {
759
+ let x = zAbs;
760
+ let t = 0;
761
+ while (x > 0 && x < a && t < 10) {
762
+ x += v * dt + s * Math.sqrt(dt) * gaussianRandom();
763
+ t += dt;
764
+ }
765
+ const rt = t + t0;
766
+ totalRT += rt;
767
+ if (x >= a) {
768
+ correctCount++;
769
+ totalCorrectRT += rt;
770
+ rtDist.push(rt);
771
+ }
772
+ else {
773
+ errorCount++;
774
+ totalErrorRT += rt;
775
+ }
776
+ }
777
+ const simAccuracy = correctCount / nSim;
778
+ const simMeanRT = totalRT / nSim;
779
+ const simMeanCorrectRT = correctCount > 0 ? totalCorrectRT / correctCount : 0;
780
+ const simMeanErrorRT = errorCount > 0 ? totalErrorRT / errorCount : 0;
781
+ parts.push('## Drift-Diffusion Model');
782
+ parts.push('');
783
+ parts.push('### Parameters');
784
+ parts.push(`- **v (drift rate)**: ${v} — strength of evidence accumulation`);
785
+ parts.push(`- **a (boundary)**: ${a} — decision threshold (speed-accuracy tradeoff)`);
786
+ parts.push(`- **z (start point)**: ${z} (${fmt(zAbs, 3)} absolute) — bias`);
787
+ parts.push(`- **t\u2080 (non-decision)**: ${t0} s — encoding + motor time`);
788
+ parts.push(`- **s (noise)**: ${s} — within-trial variability`);
789
+ parts.push('');
790
+ parts.push('### Analytical Predictions');
791
+ parts.push(`- **Predicted accuracy**: ${fmt(accuracy * 100, 4)}%`);
792
+ parts.push(`- **Predicted mean RT**: ${fmt(meanRT_correct * 1000, 4)} ms`);
793
+ parts.push('');
794
+ parts.push(`### Simulation Results (${nSim} trials)`);
795
+ parts.push(`- **Simulated accuracy**: ${fmt(simAccuracy * 100, 4)}%`);
796
+ parts.push(`- **Mean RT (all)**: ${fmt(simMeanRT * 1000, 4)} ms`);
797
+ parts.push(`- **Mean RT (correct)**: ${fmt(simMeanCorrectRT * 1000, 4)} ms`);
798
+ parts.push(`- **Mean RT (error)**: ${fmt(simMeanErrorRT * 1000, 4)} ms`);
799
+ parts.push('');
800
+ // RT distribution summary
801
+ if (rtDist.length > 0) {
802
+ rtDist.sort((a, b) => a - b);
803
+ parts.push('### RT Distribution (correct trials)');
804
+ parts.push(`- **Median**: ${fmt(rtDist[Math.floor(rtDist.length * 0.5)] * 1000, 4)} ms`);
805
+ parts.push(`- **10th percentile**: ${fmt(rtDist[Math.floor(rtDist.length * 0.1)] * 1000, 4)} ms`);
806
+ parts.push(`- **90th percentile**: ${fmt(rtDist[Math.floor(rtDist.length * 0.9)] * 1000, 4)} ms`);
807
+ }
808
+ }
809
+ else {
810
+ return `Unknown model: "${model}". Supported: hicks, fitts, stevens, weber, sdt, ddm`;
811
+ }
812
+ return parts.join('\n');
813
+ },
814
+ });
815
+ // ════════════════════════════════════════════════════════════════════════════
816
+ // 4. Biological Neural Network Simulation
817
+ // ════════════════════════════════════════════════════════════════════════════
818
+ registerTool({
819
+ name: 'neural_network_bio',
820
+ description: 'Simulate biological neural networks. Supports Leaky Integrate-and-Fire (LIF) and FitzHugh-Nagumo (simplified Hodgkin-Huxley) models. Simulate networks of up to 50 neurons with excitatory/inhibitory synapses.',
821
+ parameters: {
822
+ neurons: { type: 'number', description: 'Number of neurons (default: 10, max 50)' },
823
+ connections: { type: 'string', description: 'JSON array: [{from: 0, to: 1, weight: 0.5, type: "excitatory"}]' },
824
+ stimulus: { type: 'string', description: 'JSON: {neuron: 0, current: 10, start_ms: 0, end_ms: 50}', required: true },
825
+ duration_ms: { type: 'number', description: 'Simulation duration in ms (default: 100)' },
826
+ model: { type: 'string', description: 'Neuron model: lif (default), hh (FitzHugh-Nagumo)' },
827
+ },
828
+ tier: 'free',
829
+ async execute(args) {
830
+ const nNeurons = Math.min(Math.max(Number(args.neurons) || 10, 1), 50);
831
+ const duration = Math.min(Number(args.duration_ms) || 100, 1000);
832
+ const modelType = String(args.model || 'lif').toLowerCase();
833
+ const synapses = [];
834
+ if (args.connections) {
835
+ const raw = safeJSON(String(args.connections));
836
+ if (raw && Array.isArray(raw)) {
837
+ for (const c of raw) {
838
+ synapses.push({
839
+ from: Number(c.from || 0),
840
+ to: Number(c.to || 0),
841
+ weight: Number(c.weight || 0.5),
842
+ type: String(c.type || 'excitatory'),
843
+ delay_ms: Number(c.delay_ms || 1),
844
+ });
845
+ }
846
+ }
847
+ }
848
+ const stimRaw = safeJSON(String(args.stimulus));
849
+ const stimuli = [];
850
+ if (stimRaw) {
851
+ const stimArr = Array.isArray(stimRaw) ? stimRaw : [stimRaw];
852
+ for (const s of stimArr) {
853
+ stimuli.push({
854
+ neuron: Number(s.neuron || 0),
855
+ current: Number(s.current || 10),
856
+ start_ms: Number(s.start_ms || 0),
857
+ end_ms: Number(s.end_ms || duration),
858
+ });
859
+ }
860
+ }
861
+ const dt = 0.05; // ms
862
+ const steps = Math.floor(duration / dt);
863
+ // Spike records
864
+ const spikes = [];
865
+ const spikeCountByNeuron = new Array(nNeurons).fill(0);
866
+ if (modelType === 'lif') {
867
+ // Leaky Integrate-and-Fire model
868
+ // dV/dt = -(V - V_rest) / tau + I / C
869
+ const V_rest = -70; // mV
870
+ const V_thresh = -55; // mV
871
+ const V_reset = -75; // mV
872
+ const tau = 10; // ms (membrane time constant)
873
+ const C = 1; // nF (membrane capacitance)
874
+ const refractoryPeriod = 2; // ms
875
+ const V = new Array(nNeurons).fill(V_rest);
876
+ const refractoryTimer = new Array(nNeurons).fill(0);
877
+ const synapticInput = new Array(nNeurons).fill(0);
878
+ // Delayed spike queue
879
+ const spikeQueue = [];
880
+ // Record voltage trace for first few neurons
881
+ const traceNeurons = Math.min(nNeurons, 5);
882
+ const voltageTrace = Array.from({ length: traceNeurons }, () => []);
883
+ const traceInterval = Math.max(1, Math.floor(steps / 500));
884
+ for (let step = 0; step < steps; step++) {
885
+ const t = step * dt;
886
+ // Process delayed spikes arriving at this time
887
+ for (let i = spikeQueue.length - 1; i >= 0; i--) {
888
+ if (spikeQueue[i].time <= t) {
889
+ synapticInput[spikeQueue[i].target] += spikeQueue[i].weight;
890
+ spikeQueue.splice(i, 1);
891
+ }
892
+ }
893
+ for (let n = 0; n < nNeurons; n++) {
894
+ if (refractoryTimer[n] > 0) {
895
+ refractoryTimer[n] -= dt;
896
+ V[n] = V_reset;
897
+ continue;
898
+ }
899
+ // External stimulus current
900
+ let I_ext = 0;
901
+ for (const s of stimuli) {
902
+ if (s.neuron === n && t >= s.start_ms && t <= s.end_ms) {
903
+ I_ext += s.current;
904
+ }
905
+ }
906
+ // Synaptic input
907
+ const I_syn = synapticInput[n];
908
+ synapticInput[n] *= 0.9; // decay
909
+ // Euler integration: dV/dt = -(V - V_rest) / tau + I / C
910
+ const dV = (-(V[n] - V_rest) / tau + (I_ext + I_syn) / C) * dt;
911
+ V[n] += dV;
912
+ // Spike check
913
+ if (V[n] >= V_thresh) {
914
+ spikes.push({ neuron: n, time_ms: t });
915
+ spikeCountByNeuron[n]++;
916
+ V[n] = V_reset;
917
+ refractoryTimer[n] = refractoryPeriod;
918
+ // Propagate to connected neurons
919
+ for (const syn of synapses) {
920
+ if (syn.from === n) {
921
+ const effectiveWeight = syn.type === 'inhibitory' ? -Math.abs(syn.weight) : Math.abs(syn.weight);
922
+ spikeQueue.push({
923
+ target: syn.to,
924
+ weight: effectiveWeight * 20, // scale for mV impact
925
+ time: t + syn.delay_ms,
926
+ });
927
+ }
928
+ }
929
+ }
930
+ }
931
+ // Record voltage trace
932
+ if (step % traceInterval === 0) {
933
+ for (let n = 0; n < traceNeurons; n++) {
934
+ voltageTrace[n].push(V[n]);
935
+ }
936
+ }
937
+ }
938
+ const parts = ['## Biological Neural Network Simulation (LIF Model)\n'];
939
+ parts.push(`**Neurons**: ${nNeurons} | **Synapses**: ${synapses.length} | **Duration**: ${duration} ms | **dt**: ${dt} ms`);
940
+ parts.push(`**Parameters**: V_rest=${V_rest}mV, V_thresh=${V_thresh}mV, V_reset=${V_reset}mV, \u03C4=${tau}ms`);
941
+ parts.push('');
942
+ parts.push('### Spike Summary');
943
+ parts.push(`**Total spikes**: ${spikes.length} | **Mean firing rate**: ${fmt(spikes.length / nNeurons / (duration / 1000), 3)} Hz/neuron`);
944
+ parts.push('');
945
+ parts.push('| Neuron | Spike Count | Firing Rate (Hz) |');
946
+ parts.push('|---|---|---|');
947
+ for (let n = 0; n < nNeurons; n++) {
948
+ if (spikeCountByNeuron[n] > 0 || n < 10) {
949
+ parts.push(`| ${n} | ${spikeCountByNeuron[n]} | ${fmt(spikeCountByNeuron[n] / (duration / 1000), 3)} |`);
950
+ }
951
+ }
952
+ parts.push('');
953
+ // Spike raster (text representation)
954
+ if (spikes.length > 0) {
955
+ parts.push('### Spike Raster (first 200 spikes)');
956
+ const bins = 20;
957
+ const binWidth = duration / bins;
958
+ parts.push(`| Neuron | ${Array.from({ length: bins }, (_, i) => `${fmt(i * binWidth, 2)}`).join(' | ')} |`);
959
+ parts.push(`|---|${Array.from({ length: bins }, () => '---').join(' | ')} |`);
960
+ for (let n = 0; n < Math.min(nNeurons, 15); n++) {
961
+ const nSpikes = spikes.filter(s => s.neuron === n);
962
+ const row = Array.from({ length: bins }, (_, i) => {
963
+ const count = nSpikes.filter(s => s.time_ms >= i * binWidth && s.time_ms < (i + 1) * binWidth).length;
964
+ return count > 0 ? '\u2588' : '\u00B7';
965
+ });
966
+ parts.push(`| ${n} | ${row.join(' | ')} |`);
967
+ }
968
+ }
969
+ // Voltage trace ASCII
970
+ if (voltageTrace[0].length > 0) {
971
+ parts.push('');
972
+ parts.push('### Voltage Trace (Neuron 0)');
973
+ const trace = voltageTrace[0];
974
+ const minV = Math.min(...trace);
975
+ const maxV = Math.max(...trace);
976
+ const range = maxV - minV || 1;
977
+ const height = 8;
978
+ for (let row = height - 1; row >= 0; row--) {
979
+ const threshold = minV + (row / (height - 1)) * range;
980
+ let line = '';
981
+ const step = Math.max(1, Math.floor(trace.length / 60));
982
+ for (let i = 0; i < trace.length; i += step) {
983
+ line += trace[i] >= threshold ? '\u2588' : ' ';
984
+ }
985
+ const label = fmt(threshold, 3).padStart(8);
986
+ parts.push(`${label} |${line}|`);
987
+ }
988
+ }
989
+ return parts.join('\n');
990
+ }
991
+ else {
992
+ // FitzHugh-Nagumo model (simplified Hodgkin-Huxley)
993
+ // dv/dt = v - v^3/3 - w + I
994
+ // dw/dt = (v + a - b*w) / tau
995
+ const a_fhn = 0.7;
996
+ const b_fhn = 0.8;
997
+ const tau_fhn = 12.5;
998
+ const v_thresh = 1.0;
999
+ const v = new Array(nNeurons).fill(-1.2); // membrane voltage-like variable
1000
+ const w = new Array(nNeurons).fill(-0.6); // recovery variable
1001
+ const lastSpikeTime = new Array(nNeurons).fill(-100);
1002
+ const traceNeurons = Math.min(nNeurons, 5);
1003
+ const vTrace = Array.from({ length: traceNeurons }, () => []);
1004
+ const wTrace = Array.from({ length: traceNeurons }, () => []);
1005
+ const traceInterval = Math.max(1, Math.floor(steps / 500));
1006
+ for (let step = 0; step < steps; step++) {
1007
+ const t = step * dt;
1008
+ for (let n = 0; n < nNeurons; n++) {
1009
+ let I_ext = 0;
1010
+ for (const s of stimuli) {
1011
+ if (s.neuron === n && t >= s.start_ms && t <= s.end_ms) {
1012
+ I_ext += s.current;
1013
+ }
1014
+ }
1015
+ // Synaptic input from spikes
1016
+ let I_syn = 0;
1017
+ for (const syn of synapses) {
1018
+ if (syn.to === n) {
1019
+ // Check if the presynaptic neuron spiked recently
1020
+ const preSpikes = spikes.filter(s => s.neuron === syn.from && t - s.time_ms > 0 && t - s.time_ms < 5);
1021
+ for (const ps of preSpikes) {
1022
+ const decayedWeight = syn.weight * Math.exp(-(t - ps.time_ms) / 3);
1023
+ I_syn += syn.type === 'inhibitory' ? -decayedWeight : decayedWeight;
1024
+ }
1025
+ }
1026
+ }
1027
+ const dv = (v[n] - v[n] * v[n] * v[n] / 3 - w[n] + I_ext + I_syn) * dt;
1028
+ const dw = ((v[n] + a_fhn - b_fhn * w[n]) / tau_fhn) * dt;
1029
+ v[n] += dv;
1030
+ w[n] += dw;
1031
+ // Detect spike (upward threshold crossing)
1032
+ if (v[n] >= v_thresh && (t - lastSpikeTime[n]) > 3) {
1033
+ spikes.push({ neuron: n, time_ms: t });
1034
+ spikeCountByNeuron[n]++;
1035
+ lastSpikeTime[n] = t;
1036
+ }
1037
+ }
1038
+ if (step % traceInterval === 0) {
1039
+ for (let n = 0; n < traceNeurons; n++) {
1040
+ vTrace[n].push(v[n]);
1041
+ wTrace[n].push(w[n]);
1042
+ }
1043
+ }
1044
+ }
1045
+ const parts = ['## Biological Neural Network Simulation (FitzHugh-Nagumo Model)\n'];
1046
+ parts.push(`**Neurons**: ${nNeurons} | **Synapses**: ${synapses.length} | **Duration**: ${duration} ms | **dt**: ${dt} ms`);
1047
+ parts.push(`**Parameters**: a=${a_fhn}, b=${b_fhn}, \u03C4=${tau_fhn}`);
1048
+ parts.push('');
1049
+ parts.push('### Spike Summary');
1050
+ parts.push(`**Total spikes**: ${spikes.length} | **Mean rate**: ${fmt(spikes.length / nNeurons / (duration / 1000), 3)} Hz/neuron`);
1051
+ parts.push('');
1052
+ parts.push('| Neuron | Spike Count | Firing Rate (Hz) |');
1053
+ parts.push('|---|---|---|');
1054
+ for (let n = 0; n < nNeurons; n++) {
1055
+ if (spikeCountByNeuron[n] > 0 || n < 10) {
1056
+ parts.push(`| ${n} | ${spikeCountByNeuron[n]} | ${fmt(spikeCountByNeuron[n] / (duration / 1000), 3)} |`);
1057
+ }
1058
+ }
1059
+ if (vTrace[0].length > 0) {
1060
+ parts.push('');
1061
+ parts.push('### Phase Portrait Data (Neuron 0)');
1062
+ parts.push('| v (membrane) | w (recovery) |');
1063
+ parts.push('|---|---|');
1064
+ const step = Math.max(1, Math.floor(vTrace[0].length / 20));
1065
+ for (let i = 0; i < vTrace[0].length; i += step) {
1066
+ parts.push(`| ${fmt(vTrace[0][i], 4)} | ${fmt(wTrace[0][i], 4)} |`);
1067
+ }
1068
+ }
1069
+ return parts.join('\n');
1070
+ }
1071
+ },
1072
+ });
1073
+ // ════════════════════════════════════════════════════════════════════════════
1074
+ // 5. Neurotransmitter Lookup
1075
+ // ════════════════════════════════════════════════════════════════════════════
1076
+ registerTool({
1077
+ name: 'neurotransmitter_lookup',
1078
+ description: 'Encyclopedia of ~30 neurotransmitter systems. Search by name, function, or associated disorder. Returns type, receptors, functions, disorders, drugs, and synthesis pathway.',
1079
+ parameters: {
1080
+ query: { type: 'string', description: 'Neurotransmitter name, function, or disorder to search for', required: true },
1081
+ },
1082
+ tier: 'free',
1083
+ async execute(args) {
1084
+ const query = String(args.query).toLowerCase();
1085
+ const matches = [];
1086
+ for (const [key, nt] of Object.entries(NEUROTRANSMITTERS)) {
1087
+ const match = key.includes(query) ||
1088
+ nt.name.toLowerCase().includes(query) ||
1089
+ nt.type.toLowerCase().includes(query) ||
1090
+ nt.functions.some(f => f.toLowerCase().includes(query)) ||
1091
+ nt.disorders.some(d => d.toLowerCase().includes(query)) ||
1092
+ nt.drugs.some(d => d.toLowerCase().includes(query)) ||
1093
+ nt.receptors.some(r => r.toLowerCase().includes(query));
1094
+ if (match)
1095
+ matches.push(nt);
1096
+ }
1097
+ if (matches.length === 0) {
1098
+ return `No neurotransmitters found matching "${args.query}".\n\nAvailable: ${Object.values(NEUROTRANSMITTERS).map(n => n.name).join(', ')}`;
1099
+ }
1100
+ const parts = [`## Neurotransmitter Lookup (${matches.length} matches for "${args.query}")\n`];
1101
+ for (const nt of matches) {
1102
+ parts.push(`### ${nt.name}`);
1103
+ parts.push(`- **Type**: ${nt.type}`);
1104
+ parts.push(`- **Receptors**: ${nt.receptors.join('; ')}`);
1105
+ parts.push(`- **Functions**: ${nt.functions.join(', ')}`);
1106
+ parts.push(`- **Associated Disorders**: ${nt.disorders.join(', ')}`);
1107
+ parts.push(`- **Key Drugs**: ${nt.drugs.join('; ')}`);
1108
+ parts.push(`- **Synthesis**: ${nt.synthesis}`);
1109
+ parts.push('');
1110
+ }
1111
+ return parts.join('\n');
1112
+ },
1113
+ });
1114
+ // ════════════════════════════════════════════════════════════════════════════
1115
+ // 6. Psychophysics Calculator
1116
+ // ════════════════════════════════════════════════════════════════════════════
1117
+ registerTool({
1118
+ name: 'psychophysics_calc',
1119
+ description: 'Psychophysics calculations: JND, Weber fraction, staircase procedure analysis, psychometric function fitting (logistic/Weibull), threshold estimation.',
1120
+ parameters: {
1121
+ method: { type: 'string', description: 'Method: jnd, weber, staircase, psychometric_function', required: true },
1122
+ data: { type: 'string', description: 'JSON with method-specific data', required: true },
1123
+ params: { type: 'string', description: 'JSON with optional parameters (e.g., function type, step sizes)' },
1124
+ },
1125
+ tier: 'free',
1126
+ async execute(args) {
1127
+ const method = String(args.method).toLowerCase();
1128
+ const data = safeJSON(String(args.data));
1129
+ const params = args.params ? safeJSON(String(args.params)) : null;
1130
+ if (!data)
1131
+ return 'Error: data must be valid JSON.';
1132
+ const parts = [];
1133
+ if (method === 'jnd') {
1134
+ // Just Noticeable Difference
1135
+ const standard = Number(data.standard || data.reference || 100);
1136
+ const weberFraction = Number(data.weber_fraction || data.k || 0.05);
1137
+ const nSteps = Number(data.steps || 10);
1138
+ const jnd = standard * weberFraction;
1139
+ parts.push('## Just Noticeable Difference (JND)');
1140
+ parts.push(`**Standard stimulus**: ${standard}`);
1141
+ parts.push(`**Weber fraction (k)**: ${weberFraction}`);
1142
+ parts.push(`**JND (\u0394I)**: ${fmt(jnd, 6)}`);
1143
+ parts.push('');
1144
+ parts.push('### JND Steps');
1145
+ parts.push('| Step | Stimulus Value | Cumulative Change |');
1146
+ parts.push('|---|---|---|');
1147
+ let current = standard;
1148
+ for (let i = 0; i <= nSteps; i++) {
1149
+ parts.push(`| ${i} | ${fmt(current, 6)} | ${i === 0 ? '0' : fmt(current - standard, 6)} |`);
1150
+ current += current * weberFraction;
1151
+ }
1152
+ parts.push('');
1153
+ parts.push(`*After ${nSteps} JND steps: stimulus = ${fmt(current - current * weberFraction, 6)} (${fmt(((current - current * weberFraction) / standard - 1) * 100, 3)}% increase)*`);
1154
+ }
1155
+ else if (method === 'weber') {
1156
+ // Weber fraction from data
1157
+ const pairs = data.pairs;
1158
+ if (!pairs || !Array.isArray(pairs)) {
1159
+ return 'Error: "pairs" must be an array of {standard, jnd} objects.';
1160
+ }
1161
+ parts.push('## Weber Fraction Analysis');
1162
+ parts.push('| Standard | JND | Weber Fraction |');
1163
+ parts.push('|---|---|---|');
1164
+ let sumK = 0;
1165
+ for (const pair of pairs) {
1166
+ const k = pair.jnd / pair.standard;
1167
+ sumK += k;
1168
+ parts.push(`| ${pair.standard} | ${pair.jnd} | ${fmt(k, 6)} |`);
1169
+ }
1170
+ const meanK = sumK / pairs.length;
1171
+ parts.push('');
1172
+ parts.push(`**Mean Weber fraction**: ${fmt(meanK, 6)}`);
1173
+ // Check if Weber's law holds (constant k)
1174
+ const fractions = pairs.map(p => p.jnd / p.standard);
1175
+ const sd = Math.sqrt(fractions.reduce((s, k) => s + (k - meanK) ** 2, 0) / fractions.length);
1176
+ const cv = sd / meanK;
1177
+ parts.push(`**Standard deviation**: ${fmt(sd, 6)}`);
1178
+ parts.push(`**Coefficient of variation**: ${fmt(cv * 100, 3)}%`);
1179
+ parts.push(`**Weber's law holds**: ${cv < 0.2 ? 'Yes (CV < 20%)' : 'Questionable (CV >= 20%)'}`);
1180
+ }
1181
+ else if (method === 'staircase') {
1182
+ // Staircase procedure analysis
1183
+ const trials = data.trials;
1184
+ if (!trials || !Array.isArray(trials)) {
1185
+ return 'Error: "trials" must be an array of {level, correct} objects.';
1186
+ }
1187
+ const stepUp = Number(params?.step_up || params?.step_down || 1);
1188
+ const stepDown = Number(params?.step_down || params?.step_up || 1);
1189
+ const targetProp = Number(params?.target || 0.707); // 1-down/1-up converges to 50%, 2-down/1-up to 70.7%
1190
+ // Find reversals
1191
+ const reversals = [];
1192
+ let lastDirection = null;
1193
+ for (let i = 1; i < trials.length; i++) {
1194
+ const direction = trials[i].level > trials[i - 1].level ? 'up' : trials[i].level < trials[i - 1].level ? 'down' : lastDirection;
1195
+ if (direction && lastDirection && direction !== lastDirection) {
1196
+ reversals.push(trials[i].level);
1197
+ }
1198
+ if (direction)
1199
+ lastDirection = direction;
1200
+ }
1201
+ // Threshold estimate: mean of last N reversals
1202
+ const nReversals = Math.min(reversals.length, 6);
1203
+ const thresholdReversals = reversals.slice(-nReversals);
1204
+ const threshold = thresholdReversals.length > 0
1205
+ ? thresholdReversals.reduce((a, b) => a + b, 0) / thresholdReversals.length
1206
+ : NaN;
1207
+ parts.push('## Staircase Procedure Analysis');
1208
+ parts.push(`**Trials**: ${trials.length} | **Reversals**: ${reversals.length}`);
1209
+ parts.push(`**Step up**: ${stepUp} | **Step down**: ${stepDown}`);
1210
+ parts.push(`**Target performance**: ${fmt(targetProp * 100, 3)}%`);
1211
+ parts.push('');
1212
+ parts.push(`### Threshold Estimate`);
1213
+ parts.push(`**Threshold** (mean of last ${nReversals} reversals): **${isNaN(threshold) ? 'N/A' : fmt(threshold, 4)}**`);
1214
+ parts.push('');
1215
+ if (reversals.length > 0) {
1216
+ parts.push('### Reversal Points');
1217
+ parts.push('| Reversal # | Level |');
1218
+ parts.push('|---|---|');
1219
+ for (let i = 0; i < reversals.length; i++) {
1220
+ parts.push(`| ${i + 1} | ${fmt(reversals[i], 4)} |`);
1221
+ }
1222
+ }
1223
+ // Performance summary
1224
+ const correctCount = trials.filter(t => t.correct).length;
1225
+ parts.push('');
1226
+ parts.push(`### Performance: ${correctCount}/${trials.length} correct (${fmt(correctCount / trials.length * 100, 3)}%)`);
1227
+ }
1228
+ else if (method === 'psychometric_function' || method === 'psychometric') {
1229
+ // Psychometric function fitting
1230
+ const points = data.points;
1231
+ if (!points || !Array.isArray(points)) {
1232
+ return 'Error: "points" must be an array of {level, n_correct, n_total} objects.';
1233
+ }
1234
+ const funcType = String(params?.function || 'logistic').toLowerCase();
1235
+ const guessRate = Number(params?.guess_rate || 0.5); // for 2AFC
1236
+ const lapseRate = Number(params?.lapse_rate || 0.02);
1237
+ // Fit psychometric function via grid search
1238
+ // Logistic: p = guess + (1-guess-lapse) * 1/(1+exp(-slope*(x-threshold)))
1239
+ // Weibull: p = guess + (1-guess-lapse) * (1-exp(-(x/threshold)^slope))
1240
+ let bestThresh = 0;
1241
+ let bestSlope = 1;
1242
+ let bestLL = -Infinity;
1243
+ const levels = points.map(p => p.level);
1244
+ const minLevel = Math.min(...levels);
1245
+ const maxLevel = Math.max(...levels);
1246
+ const range = maxLevel - minLevel || 1;
1247
+ for (let ti = 0; ti <= 50; ti++) {
1248
+ const thresh = minLevel + (ti / 50) * range;
1249
+ for (let si = 1; si <= 40; si++) {
1250
+ const slope = si * 0.5;
1251
+ let ll = 0;
1252
+ for (const pt of points) {
1253
+ let p;
1254
+ if (funcType === 'weibull') {
1255
+ p = guessRate + (1 - guessRate - lapseRate) * (1 - Math.exp(-Math.pow(Math.max(pt.level, 0.001) / Math.max(thresh, 0.001), slope)));
1256
+ }
1257
+ else {
1258
+ p = guessRate + (1 - guessRate - lapseRate) / (1 + Math.exp(-slope * (pt.level - thresh)));
1259
+ }
1260
+ p = Math.max(0.001, Math.min(0.999, p));
1261
+ ll += pt.n_correct * Math.log(p) + (pt.n_total - pt.n_correct) * Math.log(1 - p);
1262
+ }
1263
+ if (ll > bestLL) {
1264
+ bestLL = ll;
1265
+ bestThresh = thresh;
1266
+ bestSlope = slope;
1267
+ }
1268
+ }
1269
+ }
1270
+ parts.push(`## Psychometric Function Fitting (${funcType})`);
1271
+ parts.push('');
1272
+ parts.push('### Fitted Parameters');
1273
+ parts.push(`- **Threshold (\u03B1)**: ${fmt(bestThresh, 4)}`);
1274
+ parts.push(`- **Slope (\u03B2)**: ${fmt(bestSlope, 4)}`);
1275
+ parts.push(`- **Guess rate (\u03B3)**: ${guessRate}`);
1276
+ parts.push(`- **Lapse rate (\u03BB)**: ${lapseRate}`);
1277
+ parts.push(`- **Log-likelihood**: ${fmt(bestLL, 4)}`);
1278
+ parts.push('');
1279
+ parts.push('### Observed vs. Predicted');
1280
+ parts.push('| Level | Observed P(correct) | Predicted P(correct) | n |');
1281
+ parts.push('|---|---|---|---|');
1282
+ for (const pt of points) {
1283
+ const observed = pt.n_correct / pt.n_total;
1284
+ let predicted;
1285
+ if (funcType === 'weibull') {
1286
+ predicted = guessRate + (1 - guessRate - lapseRate) * (1 - Math.exp(-Math.pow(Math.max(pt.level, 0.001) / Math.max(bestThresh, 0.001), bestSlope)));
1287
+ }
1288
+ else {
1289
+ predicted = guessRate + (1 - guessRate - lapseRate) / (1 + Math.exp(-bestSlope * (pt.level - bestThresh)));
1290
+ }
1291
+ parts.push(`| ${fmt(pt.level, 4)} | ${fmt(observed, 4)} | ${fmt(predicted, 4)} | ${pt.n_total} |`);
1292
+ }
1293
+ // Key thresholds
1294
+ parts.push('');
1295
+ parts.push('### Key Thresholds');
1296
+ for (const targetP of [0.50, 0.75, 0.82, 0.90]) {
1297
+ // Inverse of psychometric function
1298
+ const p_adj = (targetP - guessRate) / (1 - guessRate - lapseRate);
1299
+ let level;
1300
+ if (funcType === 'weibull') {
1301
+ level = bestThresh * Math.pow(-Math.log(1 - Math.min(p_adj, 0.999)), 1 / bestSlope);
1302
+ }
1303
+ else {
1304
+ level = bestThresh - Math.log((1 / Math.min(p_adj, 0.999)) - 1) / bestSlope;
1305
+ }
1306
+ parts.push(`- **${fmt(targetP * 100, 3)}% correct**: level = ${fmt(level, 4)}`);
1307
+ }
1308
+ }
1309
+ else {
1310
+ return `Unknown method: "${method}". Supported: jnd, weber, staircase, psychometric_function`;
1311
+ }
1312
+ return parts.join('\n');
1313
+ },
1314
+ });
1315
+ // ════════════════════════════════════════════════════════════════════════════
1316
+ // 7. Connectome Query
1317
+ // ════════════════════════════════════════════════════════════════════════════
1318
+ registerTool({
1319
+ name: 'connectome_query',
1320
+ description: 'Query brain connectivity patterns using a simplified structural connectome of 20 major brain regions. Find paths between regions, identify network hubs, compute graph metrics.',
1321
+ parameters: {
1322
+ from_region: { type: 'string', description: 'Source region name or abbreviation' },
1323
+ to_region: { type: 'string', description: 'Target region name or abbreviation' },
1324
+ query_type: { type: 'string', description: 'Query: path (shortest path), hubs (identify hubs), connectivity (show connections), all (default: all)' },
1325
+ },
1326
+ tier: 'free',
1327
+ async execute(args) {
1328
+ const queryType = String(args.query_type || 'all').toLowerCase();
1329
+ const N = CONNECTOME_REGIONS.length;
1330
+ function findRegion(query) {
1331
+ if (!query)
1332
+ return -1;
1333
+ const q = String(query).toLowerCase();
1334
+ for (let i = 0; i < N; i++) {
1335
+ if (CONNECTOME_REGIONS[i].name.toLowerCase().includes(q) ||
1336
+ CONNECTOME_REGIONS[i].abbreviation.toLowerCase() === q) {
1337
+ return i;
1338
+ }
1339
+ }
1340
+ return -1;
1341
+ }
1342
+ const fromIdx = findRegion(args.from_region);
1343
+ const toIdx = findRegion(args.to_region);
1344
+ const parts = ['## Brain Connectome Analysis\n'];
1345
+ // Compute graph metrics
1346
+ // Node degree (weighted)
1347
+ const degrees = [];
1348
+ const strengths = [];
1349
+ for (let i = 0; i < N; i++) {
1350
+ let deg = 0, str = 0;
1351
+ for (let j = 0; j < N; j++) {
1352
+ if (CONNECTIVITY[i][j] > 0.1) {
1353
+ deg++;
1354
+ str += CONNECTIVITY[i][j];
1355
+ }
1356
+ }
1357
+ degrees.push(deg);
1358
+ strengths.push(str);
1359
+ }
1360
+ // Betweenness centrality (simplified: count shortest paths through each node)
1361
+ // Using Floyd-Warshall for all-pairs shortest paths
1362
+ const dist = Array.from({ length: N }, () => new Array(N).fill(Infinity));
1363
+ const next = Array.from({ length: N }, () => new Array(N).fill(-1));
1364
+ for (let i = 0; i < N; i++) {
1365
+ dist[i][i] = 0;
1366
+ for (let j = 0; j < N; j++) {
1367
+ if (CONNECTIVITY[i][j] > 0.1) {
1368
+ dist[i][j] = 1 / CONNECTIVITY[i][j]; // inverse weight = distance
1369
+ next[i][j] = j;
1370
+ }
1371
+ }
1372
+ }
1373
+ for (let k = 0; k < N; k++) {
1374
+ for (let i = 0; i < N; i++) {
1375
+ for (let j = 0; j < N; j++) {
1376
+ if (dist[i][k] + dist[k][j] < dist[i][j]) {
1377
+ dist[i][j] = dist[i][k] + dist[k][j];
1378
+ next[i][j] = next[i][k];
1379
+ }
1380
+ }
1381
+ }
1382
+ }
1383
+ // Reconstruct path
1384
+ function getPath(from, to) {
1385
+ if (next[from][to] === -1)
1386
+ return [];
1387
+ const path = [from];
1388
+ let cur = from;
1389
+ while (cur !== to) {
1390
+ cur = next[cur][to];
1391
+ if (cur === -1)
1392
+ return [];
1393
+ path.push(cur);
1394
+ }
1395
+ return path;
1396
+ }
1397
+ // Clustering coefficient
1398
+ function clusteringCoeff(node) {
1399
+ const neighbors = [];
1400
+ for (let j = 0; j < N; j++) {
1401
+ if (j !== node && CONNECTIVITY[node][j] > 0.1)
1402
+ neighbors.push(j);
1403
+ }
1404
+ if (neighbors.length < 2)
1405
+ return 0;
1406
+ let triangles = 0;
1407
+ for (let i = 0; i < neighbors.length; i++) {
1408
+ for (let j = i + 1; j < neighbors.length; j++) {
1409
+ if (CONNECTIVITY[neighbors[i]][neighbors[j]] > 0.1)
1410
+ triangles++;
1411
+ }
1412
+ }
1413
+ return (2 * triangles) / (neighbors.length * (neighbors.length - 1));
1414
+ }
1415
+ if (queryType === 'path' || queryType === 'all') {
1416
+ if (fromIdx >= 0 && toIdx >= 0) {
1417
+ const path = getPath(fromIdx, toIdx);
1418
+ parts.push('### Shortest Path');
1419
+ if (path.length > 0) {
1420
+ parts.push(`**From**: ${CONNECTOME_REGIONS[fromIdx].name} (${CONNECTOME_REGIONS[fromIdx].abbreviation})`);
1421
+ parts.push(`**To**: ${CONNECTOME_REGIONS[toIdx].name} (${CONNECTOME_REGIONS[toIdx].abbreviation})`);
1422
+ parts.push(`**Path length**: ${path.length - 1} hops | **Distance**: ${fmt(dist[fromIdx][toIdx], 4)}`);
1423
+ parts.push('');
1424
+ parts.push('**Route**:');
1425
+ for (let i = 0; i < path.length; i++) {
1426
+ const r = CONNECTOME_REGIONS[path[i]];
1427
+ const arrow = i < path.length - 1 ? ` \u2192 (w=${fmt(CONNECTIVITY[path[i]][path[i + 1]], 2)})` : '';
1428
+ parts.push(`${i + 1}. **${r.name}** (${r.abbreviation})${arrow}`);
1429
+ }
1430
+ }
1431
+ else {
1432
+ parts.push(`No path found between ${CONNECTOME_REGIONS[fromIdx].name} and ${CONNECTOME_REGIONS[toIdx].name}`);
1433
+ }
1434
+ parts.push('');
1435
+ }
1436
+ }
1437
+ if (queryType === 'connectivity' || queryType === 'all') {
1438
+ const targetIdx = fromIdx >= 0 ? fromIdx : toIdx >= 0 ? toIdx : -1;
1439
+ if (targetIdx >= 0) {
1440
+ const r = CONNECTOME_REGIONS[targetIdx];
1441
+ parts.push(`### Connectivity of ${r.name} (${r.abbreviation})`);
1442
+ const connections = [];
1443
+ for (let j = 0; j < N; j++) {
1444
+ if (j !== targetIdx && CONNECTIVITY[targetIdx][j] > 0.1) {
1445
+ connections.push({ region: CONNECTOME_REGIONS[j].name, abbr: CONNECTOME_REGIONS[j].abbreviation, weight: CONNECTIVITY[targetIdx][j] });
1446
+ }
1447
+ }
1448
+ connections.sort((a, b) => b.weight - a.weight);
1449
+ parts.push(`**Degree**: ${degrees[targetIdx]} | **Strength**: ${fmt(strengths[targetIdx], 4)} | **Clustering**: ${fmt(clusteringCoeff(targetIdx), 4)}`);
1450
+ parts.push('');
1451
+ parts.push('| Connected Region | Weight | Strength |');
1452
+ parts.push('|---|---|---|');
1453
+ for (const c of connections) {
1454
+ const bar = '\u2588'.repeat(Math.round(c.weight * 10));
1455
+ parts.push(`| ${c.region} (${c.abbr}) | ${fmt(c.weight, 2)} | ${bar} |`);
1456
+ }
1457
+ parts.push('');
1458
+ }
1459
+ }
1460
+ if (queryType === 'hubs' || queryType === 'all') {
1461
+ parts.push('### Network Hubs (ranked by strength)');
1462
+ const ranked = CONNECTOME_REGIONS.map((r, i) => ({
1463
+ name: r.name,
1464
+ abbr: r.abbreviation,
1465
+ lobe: r.lobe,
1466
+ degree: degrees[i],
1467
+ strength: strengths[i],
1468
+ clustering: clusteringCoeff(i),
1469
+ })).sort((a, b) => b.strength - a.strength);
1470
+ parts.push('| Rank | Region | Lobe | Degree | Strength | Clustering |');
1471
+ parts.push('|---|---|---|---|---|---|');
1472
+ for (let i = 0; i < ranked.length; i++) {
1473
+ const r = ranked[i];
1474
+ parts.push(`| ${i + 1} | ${r.name} (${r.abbr}) | ${r.lobe} | ${r.degree} | ${fmt(r.strength, 3)} | ${fmt(r.clustering, 3)} |`);
1475
+ }
1476
+ parts.push('');
1477
+ // Global metrics
1478
+ const avgDeg = degrees.reduce((a, b) => a + b, 0) / N;
1479
+ const avgStr = strengths.reduce((a, b) => a + b, 0) / N;
1480
+ const avgClust = CONNECTOME_REGIONS.reduce((sum, _, i) => sum + clusteringCoeff(i), 0) / N;
1481
+ // Characteristic path length
1482
+ let totalDist = 0, pathCount = 0;
1483
+ for (let i = 0; i < N; i++) {
1484
+ for (let j = i + 1; j < N; j++) {
1485
+ if (isFinite(dist[i][j])) {
1486
+ totalDist += dist[i][j];
1487
+ pathCount++;
1488
+ }
1489
+ }
1490
+ }
1491
+ parts.push('### Global Network Metrics');
1492
+ parts.push(`- **Avg degree**: ${fmt(avgDeg, 3)}`);
1493
+ parts.push(`- **Avg strength**: ${fmt(avgStr, 3)}`);
1494
+ parts.push(`- **Avg clustering coefficient**: ${fmt(avgClust, 3)}`);
1495
+ parts.push(`- **Characteristic path length**: ${fmt(pathCount > 0 ? totalDist / pathCount : 0, 3)}`);
1496
+ parts.push(`- **Network density**: ${fmt(degrees.reduce((a, b) => a + b, 0) / (N * (N - 1)), 3)}`);
1497
+ }
1498
+ return parts.join('\n');
1499
+ },
1500
+ });
1501
+ // ════════════════════════════════════════════════════════════════════════════
1502
+ // 8. Cognitive Task Design
1503
+ // ════════════════════════════════════════════════════════════════════════════
1504
+ registerTool({
1505
+ name: 'cognitive_task_design',
1506
+ description: 'Generate protocols for standard cognitive/neuroscience experimental tasks: Stroop, N-back, Go/No-Go, Wisconsin Card Sort, Iowa Gambling Task, Flanker, Visual Search, Change Detection.',
1507
+ parameters: {
1508
+ task: { type: 'string', description: 'Task: stroop, nback, gonogo, wcst, igt, flanker, visual_search, change_detection', required: true },
1509
+ n_trials: { type: 'number', description: 'Number of trials (default: 100)' },
1510
+ difficulty: { type: 'string', description: 'Difficulty: easy, medium, hard (default: medium)' },
1511
+ },
1512
+ tier: 'free',
1513
+ async execute(args) {
1514
+ const task = String(args.task).toLowerCase().replace(/[- ]/g, '_');
1515
+ const nTrials = Math.min(Number(args.n_trials) || 100, 500);
1516
+ const difficulty = String(args.difficulty || 'medium').toLowerCase();
1517
+ const parts = [];
1518
+ if (task === 'stroop') {
1519
+ const colors = ['RED', 'BLUE', 'GREEN', 'YELLOW'];
1520
+ const congruentPct = difficulty === 'easy' ? 0.75 : difficulty === 'hard' ? 0.25 : 0.50;
1521
+ const nCongruent = Math.round(nTrials * congruentPct);
1522
+ parts.push('## Stroop Task Protocol');
1523
+ parts.push('');
1524
+ parts.push('### Task Description');
1525
+ parts.push('Participants name the **ink color** of color words, ignoring the word meaning.');
1526
+ parts.push('Measures selective attention, inhibition, and cognitive control.');
1527
+ parts.push('');
1528
+ parts.push('### Parameters');
1529
+ parts.push(`- **Total trials**: ${nTrials}`);
1530
+ parts.push(`- **Congruent trials**: ${nCongruent} (${fmt(congruentPct * 100, 3)}%)`);
1531
+ parts.push(`- **Incongruent trials**: ${nTrials - nCongruent} (${fmt((1 - congruentPct) * 100, 3)}%)`);
1532
+ parts.push(`- **Difficulty**: ${difficulty}`);
1533
+ parts.push(`- **Colors**: ${colors.join(', ')}`);
1534
+ parts.push('');
1535
+ parts.push('### Timing');
1536
+ parts.push('- **Fixation**: 500 ms');
1537
+ parts.push('- **Stimulus duration**: Until response (max 2000 ms)');
1538
+ parts.push('- **Inter-trial interval**: 1000-1500 ms (jittered)');
1539
+ parts.push('- **Response window**: 200-2000 ms');
1540
+ parts.push('');
1541
+ parts.push('### Trial Structure');
1542
+ parts.push('1. Fixation cross (+) — 500 ms');
1543
+ parts.push('2. Color word in colored ink — until response');
1544
+ parts.push('3. Feedback (optional) — 300 ms');
1545
+ parts.push('4. ITI — 1000-1500 ms');
1546
+ parts.push('');
1547
+ parts.push('### Expected Effects');
1548
+ parts.push('- **Stroop effect**: ~100-200 ms slower for incongruent vs congruent');
1549
+ parts.push('- **Error rate**: ~5-10% incongruent, ~1-2% congruent');
1550
+ parts.push('- **Congruency sequence effect**: Reduced Stroop after incongruent trial');
1551
+ parts.push('');
1552
+ parts.push('### Counterbalancing');
1553
+ parts.push('- Equal frequency of each color as ink and word');
1554
+ parts.push('- No more than 3 consecutive same-type trials');
1555
+ parts.push('- Pseudorandomized order with constraints');
1556
+ parts.push('');
1557
+ parts.push('### Sample Trials (first 10)');
1558
+ parts.push('| Trial | Word | Ink Color | Condition | Correct Response |');
1559
+ parts.push('|---|---|---|---|---|');
1560
+ for (let i = 0; i < Math.min(10, nTrials); i++) {
1561
+ const isCongruent = i < nCongruent;
1562
+ const inkColor = colors[i % colors.length];
1563
+ const word = isCongruent ? inkColor : colors[(i + 1 + Math.floor(i / 2)) % colors.length];
1564
+ parts.push(`| ${i + 1} | ${word} | ${inkColor} | ${isCongruent ? 'Congruent' : 'Incongruent'} | ${inkColor} |`);
1565
+ }
1566
+ }
1567
+ else if (task === 'nback' || task === 'n_back') {
1568
+ const n = difficulty === 'easy' ? 1 : difficulty === 'hard' ? 3 : 2;
1569
+ const targetPct = 0.30;
1570
+ parts.push(`## ${n}-Back Task Protocol`);
1571
+ parts.push('');
1572
+ parts.push('### Task Description');
1573
+ parts.push(`Participants indicate when the current stimulus matches the one ${n} positions back.`);
1574
+ parts.push('Measures working memory updating, monitoring, and executive function.');
1575
+ parts.push('');
1576
+ parts.push('### Parameters');
1577
+ parts.push(`- **N**: ${n} (${n}-back)`);
1578
+ parts.push(`- **Total trials**: ${nTrials}`);
1579
+ parts.push(`- **Target trials**: ~${Math.round(nTrials * targetPct)} (${fmt(targetPct * 100, 3)}%)`);
1580
+ parts.push(`- **Lure trials**: ~${Math.round(nTrials * 0.15)} (15%, ${n > 1 ? `${n - 1}-back or ${n + 1}-back matches` : 'similar stimuli'})`);
1581
+ parts.push(`- **Stimulus set**: Letters (A-J, excluding similar pairs)`);
1582
+ parts.push('');
1583
+ parts.push('### Timing');
1584
+ parts.push('- **Stimulus duration**: 500 ms');
1585
+ parts.push('- **Inter-stimulus interval**: 2000 ms');
1586
+ parts.push('- **Response window**: 500-2500 ms from stimulus onset');
1587
+ parts.push('- **Total block time**: ~' + fmt(nTrials * 2.5 / 60, 2) + ' min');
1588
+ parts.push('');
1589
+ parts.push('### Trial Structure');
1590
+ parts.push('1. Letter appears on screen — 500 ms');
1591
+ parts.push('2. Blank (response continues) — 2000 ms');
1592
+ parts.push(`3. Target: press if matches ${n}-back letter`);
1593
+ parts.push('4. Non-target: withhold response');
1594
+ parts.push('');
1595
+ parts.push('### Expected Performance');
1596
+ parts.push(`| N-back Level | Hit Rate | FA Rate | d' | Typical RT |`);
1597
+ parts.push('|---|---|---|---|---|');
1598
+ parts.push('| 1-back | 90-95% | 3-8% | 3.0-4.0 | ~500 ms |');
1599
+ parts.push('| 2-back | 75-85% | 5-15% | 2.0-3.0 | ~550 ms |');
1600
+ parts.push('| 3-back | 55-70% | 10-25% | 1.0-2.0 | ~600 ms |');
1601
+ parts.push('');
1602
+ parts.push('### Counterbalancing');
1603
+ parts.push('- Equal frequency of all letters');
1604
+ parts.push('- Targets evenly distributed across blocks');
1605
+ parts.push(`- Lures specifically ${n - 1 > 0 ? `${n - 1}-back and ` : ''}${n + 1}-back matches to test interference`);
1606
+ parts.push('- No consecutive targets');
1607
+ }
1608
+ else if (task === 'gonogo' || task === 'go_no_go') {
1609
+ const goPct = difficulty === 'easy' ? 0.80 : difficulty === 'hard' ? 0.50 : 0.70;
1610
+ const nGo = Math.round(nTrials * goPct);
1611
+ parts.push('## Go/No-Go Task Protocol');
1612
+ parts.push('');
1613
+ parts.push('### Task Description');
1614
+ parts.push('Participants respond to frequent "Go" stimuli and withhold response to rare "No-Go" stimuli.');
1615
+ parts.push('Measures response inhibition and sustained attention.');
1616
+ parts.push('');
1617
+ parts.push('### Parameters');
1618
+ parts.push(`- **Total trials**: ${nTrials}`);
1619
+ parts.push(`- **Go trials**: ${nGo} (${fmt(goPct * 100, 3)}%)`);
1620
+ parts.push(`- **No-Go trials**: ${nTrials - nGo} (${fmt((1 - goPct) * 100, 3)}%)`);
1621
+ parts.push(`- **Difficulty**: ${difficulty}`);
1622
+ parts.push('');
1623
+ parts.push('### Timing');
1624
+ parts.push('- **Fixation**: 500 ms');
1625
+ parts.push('- **Stimulus**: 250 ms');
1626
+ parts.push('- **Response window**: 250-1000 ms post-stimulus');
1627
+ parts.push('- **ITI**: 1200-1800 ms (jittered)');
1628
+ parts.push('- **Total time**: ~' + fmt(nTrials * 2.25 / 60, 2) + ' min');
1629
+ parts.push('');
1630
+ parts.push('### Expected Effects');
1631
+ parts.push('- **Commission errors (false alarms)**: 15-30% on No-Go trials');
1632
+ parts.push('- **Omission errors**: 2-5% on Go trials');
1633
+ parts.push('- **Go RT**: ~350-450 ms');
1634
+ parts.push('- **Speed-accuracy tradeoff**: Faster Go RT = more commission errors');
1635
+ parts.push('- **Prepotent response**: High Go ratio builds response tendency, making inhibition harder');
1636
+ parts.push('');
1637
+ parts.push('### ERP Components');
1638
+ parts.push('- **N2**: ~200-350 ms, larger for No-Go (conflict monitoring)');
1639
+ parts.push('- **P3**: ~300-500 ms, No-Go P3 > Go P3 (inhibition index)');
1640
+ }
1641
+ else if (task === 'wcst' || task === 'wisconsin') {
1642
+ const categoriesBeforeShift = difficulty === 'easy' ? 10 : difficulty === 'hard' ? 6 : 8;
1643
+ parts.push('## Wisconsin Card Sorting Task (WCST) Protocol');
1644
+ parts.push('');
1645
+ parts.push('### Task Description');
1646
+ parts.push('Participants sort cards by an unknown rule (color, shape, number). After consecutive correct sorts, the rule changes without notice.');
1647
+ parts.push('Measures set-shifting, cognitive flexibility, and perseveration.');
1648
+ parts.push('');
1649
+ parts.push('### Parameters');
1650
+ parts.push(`- **Cards**: 128 (2 decks of 64) or until ${nTrials} trials`);
1651
+ parts.push(`- **Categories**: Color, Shape, Number`);
1652
+ parts.push(`- **Correct before shift**: ${categoriesBeforeShift} consecutive`);
1653
+ parts.push(`- **Max categories**: 6 (2 full cycles of Color-Shape-Number)`);
1654
+ parts.push('');
1655
+ parts.push('### Card Properties');
1656
+ parts.push('- **Colors**: Red, Blue, Green, Yellow');
1657
+ parts.push('- **Shapes**: Triangle, Star, Cross, Circle');
1658
+ parts.push('- **Numbers**: 1, 2, 3, 4');
1659
+ parts.push('');
1660
+ parts.push('### Scoring Metrics');
1661
+ parts.push('| Metric | Description | Normal Range |');
1662
+ parts.push('|---|---|---|');
1663
+ parts.push('| Categories completed | Number of rule shifts achieved | 5-6 |');
1664
+ parts.push('| Total errors | All incorrect sorts | 10-25 |');
1665
+ parts.push('| Perseverative errors | Errors following previous rule | 5-15 |');
1666
+ parts.push('| Non-perseverative errors | Random/other errors | 5-10 |');
1667
+ parts.push('| Trials to first category | Learning speed | 10-15 |');
1668
+ parts.push('| Failure to maintain set | Errors after 5+ correct | 0-2 |');
1669
+ parts.push('');
1670
+ parts.push('### Rule Sequence');
1671
+ parts.push('1. **Color** (sort by card color)');
1672
+ parts.push(`2. After ${categoriesBeforeShift} correct → **Shape** (shift, no notification)`);
1673
+ parts.push(`3. After ${categoriesBeforeShift} correct → **Number** (shift)`);
1674
+ parts.push('4. Cycle repeats: Color → Shape → Number');
1675
+ }
1676
+ else if (task === 'igt' || task === 'iowa') {
1677
+ parts.push('## Iowa Gambling Task (IGT) Protocol');
1678
+ parts.push('');
1679
+ parts.push('### Task Description');
1680
+ parts.push('Participants choose cards from 4 decks, learning which decks are advantageous vs. disadvantageous through trial and error.');
1681
+ parts.push('Measures decision making under ambiguity, somatic markers, and emotional learning.');
1682
+ parts.push('');
1683
+ parts.push('### Parameters');
1684
+ parts.push(`- **Total trials**: ${nTrials}`);
1685
+ parts.push('- **Starting money**: $2000');
1686
+ parts.push('- **Decks**: A, B, C, D');
1687
+ parts.push('');
1688
+ parts.push('### Deck Structure');
1689
+ parts.push('| Deck | Reward/Card | Penalty Freq | Avg Penalty | Net/10 cards | Type |');
1690
+ parts.push('|---|---|---|---|---|---|');
1691
+ parts.push('| A | $100 | 5/10 | $250 | -$250 | Disadvantageous (high freq loss) |');
1692
+ parts.push('| B | $100 | 1/10 | $1250 | -$250 | Disadvantageous (low freq, high loss) |');
1693
+ parts.push('| C | $50 | 5/10 | $50 | +$250 | Advantageous (high freq, small loss) |');
1694
+ parts.push('| D | $50 | 1/10 | $250 | +$250 | Advantageous (low freq, moderate loss) |');
1695
+ parts.push('');
1696
+ parts.push('### Block Structure (5 blocks of 20 trials)');
1697
+ parts.push('| Block | Trials | Expected Behavior |');
1698
+ parts.push('|---|---|---|');
1699
+ parts.push('| 1 | 1-20 | Exploration, sampling all decks |');
1700
+ parts.push('| 2 | 21-40 | Begin developing preferences |');
1701
+ parts.push('| 3 | 41-60 | "Hunch" period — gut feeling about bad decks |');
1702
+ parts.push('| 4 | 61-80 | Conceptual understanding emerging |');
1703
+ parts.push('| 5 | 81-100 | Stable preference for C/D |');
1704
+ parts.push('');
1705
+ parts.push('### Key Metrics');
1706
+ parts.push('- **Net score**: (C+D) - (A+B) selections per block');
1707
+ parts.push('- **Learning curve**: Net score should increase across blocks');
1708
+ parts.push('- **Anticipatory SCR**: Skin conductance before choosing bad decks (somatic marker)');
1709
+ parts.push('');
1710
+ parts.push('### Clinical Significance');
1711
+ parts.push('- **VMPFC lesions**: Choose disadvantageously throughout (Bechara et al., 1994)');
1712
+ parts.push('- **Addiction**: Prefer high-reward decks despite long-term loss');
1713
+ parts.push('- **Psychopathy**: Reduced anticipatory SCR to risky choices');
1714
+ }
1715
+ else if (task === 'flanker') {
1716
+ const setSizeIncompatible = difficulty === 'easy' ? 2 : difficulty === 'hard' ? 6 : 4;
1717
+ parts.push('## Eriksen Flanker Task Protocol');
1718
+ parts.push('');
1719
+ parts.push('### Task Description');
1720
+ parts.push('Participants respond to a central target while ignoring surrounding flanker stimuli.');
1721
+ parts.push('Measures selective attention and interference control.');
1722
+ parts.push('');
1723
+ parts.push('### Parameters');
1724
+ parts.push(`- **Total trials**: ${nTrials}`);
1725
+ parts.push(`- **Congruent trials**: ${Math.round(nTrials / 2)} (50%)`);
1726
+ parts.push(`- **Incongruent trials**: ${Math.round(nTrials / 2)} (50%)`);
1727
+ parts.push(`- **Flankers per side**: ${setSizeIncompatible / 2}`);
1728
+ parts.push('');
1729
+ parts.push('### Stimuli');
1730
+ parts.push('- **Congruent**: > > > > > (or < < < < <)');
1731
+ parts.push('- **Incongruent**: > > < > > (or < < > < <)');
1732
+ parts.push('- **Response**: Left hand for <, Right hand for >');
1733
+ parts.push('');
1734
+ parts.push('### Timing');
1735
+ parts.push('- **Fixation**: 400-600 ms (jittered)');
1736
+ parts.push('- **Stimulus**: Until response (max 1500 ms)');
1737
+ parts.push('- **Response window**: 200-1500 ms');
1738
+ parts.push('- **ITI**: 1000-1500 ms');
1739
+ parts.push('');
1740
+ parts.push('### Expected Effects');
1741
+ parts.push('- **Flanker effect**: ~50-100 ms slower for incongruent');
1742
+ parts.push('- **Error rate**: ~5-15% incongruent, ~1-3% congruent');
1743
+ parts.push('- **Gratton effect**: Reduced flanker effect after incongruent trials');
1744
+ parts.push('- **Speed-accuracy tradeoff**: Faster responses = larger flanker effect');
1745
+ parts.push('');
1746
+ parts.push('### ERP Components');
1747
+ parts.push('- **N2**: ~200-350 ms (conflict detection, ACC source)');
1748
+ parts.push('- **ERN**: Error-related negativity post-error (~50-100 ms)');
1749
+ parts.push('- **LRP**: Lateralized readiness potential (incorrect response activation on incongruent)');
1750
+ }
1751
+ else if (task === 'visual_search') {
1752
+ const setSize = difficulty === 'easy' ? [4, 8] : difficulty === 'hard' ? [8, 16, 32] : [4, 8, 16];
1753
+ const targetPresent = 0.50;
1754
+ parts.push('## Visual Search Task Protocol');
1755
+ parts.push('');
1756
+ parts.push('### Task Description');
1757
+ parts.push('Participants search for a target among distractors. Set size varies to measure search efficiency.');
1758
+ parts.push('Measures attentional selection, feature integration, and search strategies.');
1759
+ parts.push('');
1760
+ parts.push('### Parameters');
1761
+ parts.push(`- **Total trials**: ${nTrials}`);
1762
+ parts.push(`- **Set sizes**: ${setSize.join(', ')} items`);
1763
+ parts.push(`- **Target present**: ${fmt(targetPresent * 100, 3)}% of trials`);
1764
+ parts.push(`- **Target**: Red T among Green T/Red L distractors`);
1765
+ parts.push('');
1766
+ parts.push('### Timing');
1767
+ parts.push('- **Fixation**: 500-800 ms');
1768
+ parts.push('- **Search display**: Until response (max 5000 ms)');
1769
+ parts.push('- **ITI**: 1000 ms');
1770
+ parts.push('');
1771
+ parts.push('### Conditions');
1772
+ parts.push('| Condition | Target | Distractors | Search Type |');
1773
+ parts.push('|---|---|---|---|');
1774
+ parts.push('| Feature (pop-out) | Red circle | Green circles | Parallel (~0 ms/item) |');
1775
+ parts.push('| Conjunction | Red T | Red L + Green T | Serial (~20-40 ms/item) |');
1776
+ parts.push('| Spatial configuration | T | Rotated L | Serial (~40-60 ms/item) |');
1777
+ parts.push('');
1778
+ parts.push('### Expected Effects');
1779
+ parts.push('- **Search slope (target present)**: 0 ms/item (pop-out) to 20-60 ms/item (serial)');
1780
+ parts.push('- **Search slope (target absent)**: ~2x target-present slope');
1781
+ parts.push('- **2:1 slope ratio**: Consistent with self-terminating serial search');
1782
+ parts.push('- **Set size effect**: Linear RT increase for serial, flat for parallel');
1783
+ }
1784
+ else if (task === 'change_detection') {
1785
+ const setSizes = difficulty === 'easy' ? [2, 3, 4] : difficulty === 'hard' ? [4, 6, 8] : [3, 4, 6];
1786
+ parts.push('## Change Detection Task Protocol');
1787
+ parts.push('');
1788
+ parts.push('### Task Description');
1789
+ parts.push('Participants view a brief display of colored squares, then after a delay, indicate whether a change occurred.');
1790
+ parts.push('Measures visual working memory capacity (Luck & Vogel, 1997).');
1791
+ parts.push('');
1792
+ parts.push('### Parameters');
1793
+ parts.push(`- **Total trials**: ${nTrials}`);
1794
+ parts.push(`- **Set sizes**: ${setSizes.join(', ')} items`);
1795
+ parts.push(`- **Change trials**: 50%`);
1796
+ parts.push(`- **Colors**: Red, Blue, Green, Yellow, Purple, White, Black, Cyan`);
1797
+ parts.push('');
1798
+ parts.push('### Timing');
1799
+ parts.push('- **Fixation**: 500 ms');
1800
+ parts.push('- **Memory array**: 100 ms');
1801
+ parts.push('- **Retention interval**: 900 ms');
1802
+ parts.push('- **Test array**: Until response (max 3000 ms)');
1803
+ parts.push('- **ITI**: 1000 ms');
1804
+ parts.push('');
1805
+ parts.push('### Trial Structure');
1806
+ parts.push('1. Fixation (+) — 500 ms');
1807
+ parts.push('2. Memory array (N colored squares) — 100 ms');
1808
+ parts.push('3. Blank (retention interval) — 900 ms');
1809
+ parts.push('4. Test array (same or one changed) — until response');
1810
+ parts.push('');
1811
+ parts.push("### Capacity Estimation (Cowan's K)");
1812
+ parts.push('K = S * (H - F), where S = set size, H = hit rate, F = false alarm rate');
1813
+ parts.push('');
1814
+ parts.push('### Expected Results');
1815
+ parts.push('| Set Size | Expected Hit Rate | Expected FA Rate | Expected K |');
1816
+ parts.push('|---|---|---|---|');
1817
+ for (const s of setSizes) {
1818
+ const k = Math.min(s, 3.5);
1819
+ const hr = Math.min(0.95, k / s + 0.5 * (1 - k / s));
1820
+ const far = Math.max(0.05, 1 - hr);
1821
+ parts.push(`| ${s} | ${fmt(hr * 100, 3)}% | ${fmt(far * 100, 3)}% | ${fmt(s * (hr - far), 3)} |`);
1822
+ }
1823
+ parts.push('');
1824
+ parts.push('### Typical Capacity');
1825
+ parts.push('- **K = 3-4 items** for healthy adults (Luck & Vogel, 1997)');
1826
+ parts.push('- **K = 1-2 items** in schizophrenia');
1827
+ parts.push('- K predicts fluid intelligence (r ~ 0.5)');
1828
+ }
1829
+ else {
1830
+ return `Unknown task: "${task}". Supported: stroop, nback, gonogo, wcst, igt, flanker, visual_search, change_detection`;
1831
+ }
1832
+ return parts.join('\n');
1833
+ },
1834
+ });
1835
+ // ════════════════════════════════════════════════════════════════════════════
1836
+ // 9. Neuroimaging Coordinates
1837
+ // ════════════════════════════════════════════════════════════════════════════
1838
+ registerTool({
1839
+ name: 'neuroimaging_coords',
1840
+ description: 'Convert between brain coordinate systems (MNI, Talairach), look up brain regions from coordinates, or find coordinates for named regions. Includes ~50 standard MNI atlas entries.',
1841
+ parameters: {
1842
+ coordinates: { type: 'string', description: 'JSON: {x, y, z} in mm' },
1843
+ from_space: { type: 'string', description: 'Coordinate space: mni or talairach (default: mni)' },
1844
+ operation: { type: 'string', description: 'Operation: convert, lookup, find_region (default: lookup)' },
1845
+ },
1846
+ tier: 'free',
1847
+ async execute(args) {
1848
+ const operation = String(args.operation || 'lookup').toLowerCase();
1849
+ const fromSpace = String(args.from_space || 'mni').toLowerCase();
1850
+ const parts = ['## Neuroimaging Coordinate Analysis\n'];
1851
+ // MNI <-> Talairach conversion (Lancaster et al., 2007 - icbm2tal)
1852
+ function mniToTal(x, y, z) {
1853
+ // Simplified icbm2tal transform
1854
+ return {
1855
+ x: Math.round(0.9357 * x + 0.0029 * y - 0.0072 * z - 1.0423),
1856
+ y: Math.round(-0.0065 * x + 0.9396 * y - 0.0726 * z - 1.3940),
1857
+ z: Math.round(0.0103 * x + 0.0752 * y + 0.8967 * z + 3.6475),
1858
+ };
1859
+ }
1860
+ function talToMni(x, y, z) {
1861
+ // Approximate inverse
1862
+ return {
1863
+ x: Math.round(1.0667 * x - 0.0034 * y + 0.0087 * z + 1.1158),
1864
+ y: Math.round(0.0076 * x + 1.0637 * y + 0.0857 * z + 1.4862),
1865
+ z: Math.round(-0.0127 * x - 0.0895 * y + 1.1142 * z - 4.3377),
1866
+ };
1867
+ }
1868
+ if (args.coordinates) {
1869
+ const coords = safeJSON(String(args.coordinates));
1870
+ if (!coords || typeof coords.x !== 'number') {
1871
+ return 'Error: coordinates must be JSON {x, y, z} in mm.';
1872
+ }
1873
+ const { x, y, z } = coords;
1874
+ if (operation === 'convert') {
1875
+ parts.push('### Coordinate Conversion');
1876
+ if (fromSpace === 'mni') {
1877
+ const tal = mniToTal(x, y, z);
1878
+ parts.push(`**MNI**: (${x}, ${y}, ${z})`);
1879
+ parts.push(`**Talairach**: (${tal.x}, ${tal.y}, ${tal.z})`);
1880
+ parts.push('');
1881
+ parts.push('*Conversion via icbm2tal transform (Lancaster et al., 2007)*');
1882
+ }
1883
+ else {
1884
+ const mni = talToMni(x, y, z);
1885
+ parts.push(`**Talairach**: (${x}, ${y}, ${z})`);
1886
+ parts.push(`**MNI**: (${mni.x}, ${mni.y}, ${mni.z})`);
1887
+ parts.push('');
1888
+ parts.push('*Conversion via approximate inverse icbm2tal transform*');
1889
+ }
1890
+ parts.push('');
1891
+ }
1892
+ // Look up nearest atlas region
1893
+ if (operation === 'lookup' || operation === 'find_region' || operation === 'convert') {
1894
+ // Convert input to MNI if needed
1895
+ let mniX = x, mniY = y, mniZ = z;
1896
+ if (fromSpace === 'talairach') {
1897
+ const mni = talToMni(x, y, z);
1898
+ mniX = mni.x;
1899
+ mniY = mni.y;
1900
+ mniZ = mni.z;
1901
+ }
1902
+ parts.push('### Nearest Atlas Regions');
1903
+ const distances = MNI_ATLAS.map(entry => {
1904
+ const dx = mniX - entry.mni.x;
1905
+ const dy = mniY - entry.mni.y;
1906
+ const dz = mniZ - entry.mni.z;
1907
+ return { entry, dist: Math.sqrt(dx * dx + dy * dy + dz * dz) };
1908
+ }).sort((a, b) => a.dist - b.dist);
1909
+ parts.push(`**Query point** (MNI): (${mniX}, ${mniY}, ${mniZ})`);
1910
+ parts.push('');
1911
+ parts.push('| Rank | Region | MNI (x,y,z) | Distance (mm) | Brodmann |');
1912
+ parts.push('|---|---|---|---|---|');
1913
+ for (let i = 0; i < Math.min(10, distances.length); i++) {
1914
+ const d = distances[i];
1915
+ parts.push(`| ${i + 1} | ${d.entry.name} | (${d.entry.mni.x}, ${d.entry.mni.y}, ${d.entry.mni.z}) | ${fmt(d.dist, 3)} | ${d.entry.brodmann || '-'} |`);
1916
+ }
1917
+ if (distances[0].dist < 15) {
1918
+ parts.push('');
1919
+ parts.push(`**Best match**: ${distances[0].entry.name} (${fmt(distances[0].dist, 3)} mm away)`);
1920
+ }
1921
+ else {
1922
+ parts.push('');
1923
+ parts.push(`*Note: Nearest region is ${fmt(distances[0].dist, 3)} mm away. Coordinates may be in white matter or a region not in this atlas.*`);
1924
+ }
1925
+ }
1926
+ }
1927
+ else {
1928
+ // No coordinates provided — list atlas or find region by name
1929
+ const query = String(args.from_space || args.operation || '').toLowerCase();
1930
+ if (query && query !== 'mni' && query !== 'talairach' && query !== 'lookup') {
1931
+ // Search by name
1932
+ const matches = MNI_ATLAS.filter(e => e.name.toLowerCase().includes(query));
1933
+ if (matches.length > 0) {
1934
+ parts.push(`### Regions matching "${query}"`);
1935
+ parts.push('| Region | MNI (x,y,z) | Talairach (x,y,z) | Brodmann |');
1936
+ parts.push('|---|---|---|---|');
1937
+ for (const m of matches) {
1938
+ parts.push(`| ${m.name} | (${m.mni.x}, ${m.mni.y}, ${m.mni.z}) | (${m.talairach.x}, ${m.talairach.y}, ${m.talairach.z}) | ${m.brodmann || '-'} |`);
1939
+ }
1940
+ }
1941
+ else {
1942
+ parts.push(`No regions found matching "${query}".`);
1943
+ }
1944
+ }
1945
+ else {
1946
+ parts.push('### Complete MNI Atlas (' + MNI_ATLAS.length + ' regions)');
1947
+ parts.push('| Region | MNI (x,y,z) | Brodmann |');
1948
+ parts.push('|---|---|---|');
1949
+ for (const entry of MNI_ATLAS) {
1950
+ parts.push(`| ${entry.name} | (${entry.mni.x}, ${entry.mni.y}, ${entry.mni.z}) | ${entry.brodmann || '-'} |`);
1951
+ }
1952
+ }
1953
+ }
1954
+ return parts.join('\n');
1955
+ },
1956
+ });
1957
+ // ════════════════════════════════════════════════════════════════════════════
1958
+ // 10. Learning Model
1959
+ // ════════════════════════════════════════════════════════════════════════════
1960
+ registerTool({
1961
+ name: 'learning_model',
1962
+ description: 'Computational models of learning: Rescorla-Wagner (classical conditioning), temporal difference, Q-learning (with grid world), SARSA, Hebbian learning, Bayesian learning. Simulates learning curves.',
1963
+ parameters: {
1964
+ model: { type: 'string', description: 'Model: rescorla_wagner, td, q_learning, sarsa, hebbian, bayesian', required: true },
1965
+ params: { type: 'string', description: 'JSON with model-specific parameters', required: true },
1966
+ trials: { type: 'number', description: 'Number of learning trials (default: 100)' },
1967
+ },
1968
+ tier: 'free',
1969
+ async execute(args) {
1970
+ const model = String(args.model).toLowerCase().replace(/[- ]/g, '_');
1971
+ const p = safeJSON(String(args.params));
1972
+ if (!p)
1973
+ return 'Error: params must be valid JSON.';
1974
+ const nTrials = Math.min(Number(args.trials) || 100, 1000);
1975
+ const parts = [];
1976
+ if (model === 'rescorla_wagner' || model === 'rw') {
1977
+ // Rescorla-Wagner: ΔV = α * β * (λ - ΣV)
1978
+ const alpha = Number(p.alpha || p.learning_rate || 0.3); // CS salience
1979
+ const beta = Number(p.beta || 1.0); // US processing rate
1980
+ const lambda = Number(p.lambda || p.max_assoc || 1.0); // asymptote
1981
+ const nCS = Number(p.n_cs || p.stimuli || 1); // number of CSs
1982
+ const extinction_start = Number(p.extinction_start || 0); // trial to start extinction (0 = none)
1983
+ const reinstatement_trial = Number(p.reinstatement_trial || 0);
1984
+ const V = new Array(nCS).fill(0);
1985
+ const history = [];
1986
+ parts.push('## Rescorla-Wagner Model (Classical Conditioning)');
1987
+ parts.push('');
1988
+ parts.push(`**Formula**: \u0394V = \u03B1 \u00D7 \u03B2 \u00D7 (\u03BB - \u03A3V)`);
1989
+ parts.push(`**Parameters**: \u03B1=${alpha}, \u03B2=${beta}, \u03BB=${lambda}, CSs=${nCS}`);
1990
+ if (extinction_start > 0)
1991
+ parts.push(`**Extinction begins**: trial ${extinction_start}`);
1992
+ parts.push('');
1993
+ for (let t = 1; t <= nTrials; t++) {
1994
+ const usPresent = extinction_start > 0 && t >= extinction_start && (reinstatement_trial === 0 || t < reinstatement_trial) ? 0 : lambda;
1995
+ const sumV = V.reduce((a, b) => a + b, 0);
1996
+ const error = usPresent - sumV;
1997
+ for (let cs = 0; cs < nCS; cs++) {
1998
+ const deltaV = alpha * beta * (usPresent - sumV);
1999
+ V[cs] += deltaV;
2000
+ }
2001
+ history.push({ trial: t, V: [...V], error });
2002
+ }
2003
+ parts.push('### Learning Curve');
2004
+ parts.push('| Trial | ' + Array.from({ length: nCS }, (_, i) => `V(CS${i + 1})`).join(' | ') + ' | \u03A3V | Prediction Error |');
2005
+ parts.push('|---|' + Array.from({ length: nCS + 2 }, () => '---').join('|') + '|');
2006
+ const step = Math.max(1, Math.floor(nTrials / 25));
2007
+ for (let i = 0; i < history.length; i += step) {
2008
+ const h = history[i];
2009
+ const sumV = h.V.reduce((a, b) => a + b, 0);
2010
+ parts.push(`| ${h.trial} | ${h.V.map(v => fmt(v, 4)).join(' | ')} | ${fmt(sumV, 4)} | ${fmt(h.error, 4)} |`);
2011
+ }
2012
+ // Always show last trial
2013
+ const last = history[history.length - 1];
2014
+ const lastSum = last.V.reduce((a, b) => a + b, 0);
2015
+ parts.push(`| ${last.trial} | ${last.V.map(v => fmt(v, 4)).join(' | ')} | ${fmt(lastSum, 4)} | ${fmt(last.error, 4)} |`);
2016
+ parts.push('');
2017
+ parts.push(`**Final associative strength**: \u03A3V = ${fmt(lastSum, 4)}`);
2018
+ if (nCS > 1) {
2019
+ parts.push(`*With ${nCS} CSs, each acquires V = ${fmt(lastSum / nCS, 4)} (blocking/overshadowing)*`);
2020
+ }
2021
+ }
2022
+ else if (model === 'td' || model === 'temporal_difference') {
2023
+ // TD(0) learning on a Markov chain
2024
+ const alpha = Number(p.alpha || p.learning_rate || 0.1);
2025
+ const gamma = Number(p.gamma || p.discount || 0.9);
2026
+ const nStates = Math.min(Number(p.n_states || p.states || 5), 20);
2027
+ const rewardState = Number(p.reward_state ?? nStates - 1);
2028
+ const rewardValue = Number(p.reward || 1.0);
2029
+ const V = new Array(nStates).fill(0);
2030
+ const history = [];
2031
+ parts.push('## Temporal Difference Learning — TD(0)');
2032
+ parts.push('');
2033
+ parts.push(`**Update rule**: V(s) \u2190 V(s) + \u03B1[\u03B4] where \u03B4 = r + \u03B3V(s') - V(s)`);
2034
+ parts.push(`**Parameters**: \u03B1=${alpha}, \u03B3=${gamma}, states=${nStates}`);
2035
+ parts.push(`**Reward**: ${rewardValue} at state ${rewardState}`);
2036
+ parts.push('');
2037
+ for (let t = 0; t < nTrials; t++) {
2038
+ // Walk through states sequentially (Markov chain: S0 -> S1 -> ... -> S_terminal)
2039
+ for (let s = 0; s < nStates - 1; s++) {
2040
+ const r = s + 1 === rewardState ? rewardValue : 0;
2041
+ const sNext = s + 1;
2042
+ const td_error = r + gamma * V[sNext] - V[s];
2043
+ V[s] += alpha * td_error;
2044
+ }
2045
+ // Terminal state resets
2046
+ V[nStates - 1] = 0;
2047
+ if (t % Math.max(1, Math.floor(nTrials / 20)) === 0 || t === nTrials - 1) {
2048
+ history.push({ trial: t + 1, V: [...V] });
2049
+ }
2050
+ }
2051
+ parts.push('### Value Function Over Trials');
2052
+ parts.push('| Trial | ' + Array.from({ length: nStates }, (_, i) => `S${i}`).join(' | ') + ' |');
2053
+ parts.push('|---|' + Array.from({ length: nStates }, () => '---').join('|') + '|');
2054
+ for (const h of history) {
2055
+ parts.push(`| ${h.trial} | ${h.V.map(v => fmt(v, 4)).join(' | ')} |`);
2056
+ }
2057
+ parts.push('');
2058
+ // Analytical solution: V(s) = gamma^(rewardState - s - 1) * rewardValue for s < rewardState
2059
+ parts.push('### Analytical vs. Learned Values');
2060
+ parts.push('| State | Learned V | Analytical V | Error |');
2061
+ parts.push('|---|---|---|---|');
2062
+ for (let s = 0; s < nStates; s++) {
2063
+ const analytical = s < rewardState ? rewardValue * Math.pow(gamma, rewardState - s - 1) : 0;
2064
+ parts.push(`| S${s} | ${fmt(V[s], 4)} | ${fmt(analytical, 4)} | ${fmt(Math.abs(V[s] - analytical), 4)} |`);
2065
+ }
2066
+ }
2067
+ else if (model === 'q_learning') {
2068
+ // Q-learning on a grid world
2069
+ // Q(s,a) ← Q(s,a) + α[r + γ·max Q(s',a') - Q(s,a)]
2070
+ const alpha = Number(p.alpha || p.learning_rate || 0.1);
2071
+ const gamma = Number(p.gamma || p.discount || 0.95);
2072
+ const epsilon = Number(p.epsilon || 0.1);
2073
+ const gridW = Math.min(Number(p.width || p.grid_width || 5), 10);
2074
+ const gridH = Math.min(Number(p.height || p.grid_height || 5), 10);
2075
+ const goalX = Number(p.goal_x ?? gridW - 1);
2076
+ const goalY = Number(p.goal_y ?? gridH - 1);
2077
+ const reward = Number(p.reward || 10);
2078
+ // Obstacles (optional)
2079
+ const obstacles = new Set();
2080
+ if (p.obstacles && Array.isArray(p.obstacles)) {
2081
+ for (const o of p.obstacles) {
2082
+ obstacles.add(`${o.x},${o.y}`);
2083
+ }
2084
+ }
2085
+ const nStates = gridW * gridH;
2086
+ const actions = ['up', 'down', 'left', 'right'];
2087
+ const Q = Array.from({ length: nStates }, () => new Array(4).fill(0));
2088
+ const stateVisits = new Array(nStates).fill(0);
2089
+ function stateIdx(x, y) { return y * gridW + x; }
2090
+ function isValid(x, y) {
2091
+ return x >= 0 && x < gridW && y >= 0 && y < gridH && !obstacles.has(`${x},${y}`);
2092
+ }
2093
+ function step(x, y, action) {
2094
+ let nx = x, ny = y;
2095
+ if (action === 0)
2096
+ ny = y - 1; // up
2097
+ else if (action === 1)
2098
+ ny = y + 1; // down
2099
+ else if (action === 2)
2100
+ nx = x - 1; // left
2101
+ else if (action === 3)
2102
+ nx = x + 1; // right
2103
+ if (!isValid(nx, ny)) {
2104
+ nx = x;
2105
+ ny = y;
2106
+ } // wall bounce
2107
+ const r = (nx === goalX && ny === goalY) ? reward : -0.1; // small step penalty
2108
+ return { nx, ny, r };
2109
+ }
2110
+ const episodeRewards = [];
2111
+ for (let trial = 0; trial < nTrials; trial++) {
2112
+ let x = 0, y = 0;
2113
+ let totalReward = 0;
2114
+ let steps = 0;
2115
+ const maxSteps = gridW * gridH * 4;
2116
+ while (!(x === goalX && y === goalY) && steps < maxSteps) {
2117
+ const s = stateIdx(x, y);
2118
+ stateVisits[s]++;
2119
+ // Epsilon-greedy action selection
2120
+ let action;
2121
+ if (Math.random() < epsilon) {
2122
+ action = Math.floor(Math.random() * 4);
2123
+ }
2124
+ else {
2125
+ action = Q[s].indexOf(Math.max(...Q[s]));
2126
+ }
2127
+ const { nx, ny, r } = step(x, y, action);
2128
+ const sNext = stateIdx(nx, ny);
2129
+ // Q-learning update: Q(s,a) ← Q(s,a) + α[r + γ·max Q(s',a') - Q(s,a)]
2130
+ const maxQ_next = Math.max(...Q[sNext]);
2131
+ Q[s][action] += alpha * (r + gamma * maxQ_next - Q[s][action]);
2132
+ totalReward += r;
2133
+ x = nx;
2134
+ y = ny;
2135
+ steps++;
2136
+ }
2137
+ episodeRewards.push(totalReward);
2138
+ }
2139
+ parts.push('## Q-Learning (Grid World)');
2140
+ parts.push('');
2141
+ parts.push(`**Update**: Q(s,a) \u2190 Q(s,a) + \u03B1[r + \u03B3\u00B7max Q(s\',a\') - Q(s,a)]`);
2142
+ parts.push(`**Parameters**: \u03B1=${alpha}, \u03B3=${gamma}, \u03B5=${epsilon}`);
2143
+ parts.push(`**Grid**: ${gridW}x${gridH} | **Goal**: (${goalX}, ${goalY}) | **Reward**: ${reward}`);
2144
+ if (obstacles.size > 0)
2145
+ parts.push(`**Obstacles**: ${[...obstacles].join('; ')}`);
2146
+ parts.push('');
2147
+ // Show learned policy
2148
+ parts.push('### Learned Policy (best action per state)');
2149
+ const arrowMap = ['\u2191', '\u2193', '\u2190', '\u2192']; // up, down, left, right
2150
+ let grid = '```\n';
2151
+ for (let y = 0; y < gridH; y++) {
2152
+ let row = '';
2153
+ for (let x = 0; x < gridW; x++) {
2154
+ if (x === goalX && y === goalY) {
2155
+ row += ' G ';
2156
+ }
2157
+ else if (obstacles.has(`${x},${y}`)) {
2158
+ row += ' # ';
2159
+ }
2160
+ else {
2161
+ const s = stateIdx(x, y);
2162
+ const bestAction = Q[s].indexOf(Math.max(...Q[s]));
2163
+ row += ` ${arrowMap[bestAction]} `;
2164
+ }
2165
+ }
2166
+ grid += row + '\n';
2167
+ }
2168
+ grid += '```';
2169
+ parts.push(grid);
2170
+ parts.push('');
2171
+ // Q-value table for key states
2172
+ parts.push('### Q-Values (selected states)');
2173
+ parts.push('| State (x,y) | Q(up) | Q(down) | Q(left) | Q(right) | Best |');
2174
+ parts.push('|---|---|---|---|---|---|');
2175
+ for (let y = 0; y < gridH; y++) {
2176
+ for (let x = 0; x < gridW; x++) {
2177
+ if (obstacles.has(`${x},${y}`))
2178
+ continue;
2179
+ const s = stateIdx(x, y);
2180
+ if (stateVisits[s] > 0) {
2181
+ const best = Q[s].indexOf(Math.max(...Q[s]));
2182
+ parts.push(`| (${x},${y}) | ${fmt(Q[s][0], 3)} | ${fmt(Q[s][1], 3)} | ${fmt(Q[s][2], 3)} | ${fmt(Q[s][3], 3)} | ${actions[best]} |`);
2183
+ }
2184
+ }
2185
+ }
2186
+ parts.push('');
2187
+ // Learning curve
2188
+ parts.push('### Learning Curve (episode rewards)');
2189
+ const bucketSize = Math.max(1, Math.floor(nTrials / 20));
2190
+ parts.push('| Episodes | Mean Reward | Min | Max |');
2191
+ parts.push('|---|---|---|---|');
2192
+ for (let i = 0; i < nTrials; i += bucketSize) {
2193
+ const bucket = episodeRewards.slice(i, i + bucketSize);
2194
+ const mean = bucket.reduce((a, b) => a + b, 0) / bucket.length;
2195
+ parts.push(`| ${i + 1}-${Math.min(i + bucketSize, nTrials)} | ${fmt(mean, 4)} | ${fmt(Math.min(...bucket), 4)} | ${fmt(Math.max(...bucket), 4)} |`);
2196
+ }
2197
+ }
2198
+ else if (model === 'sarsa') {
2199
+ // SARSA: Q(s,a) ← Q(s,a) + α[r + γ·Q(s',a') - Q(s,a)]
2200
+ const alpha = Number(p.alpha || p.learning_rate || 0.1);
2201
+ const gamma = Number(p.gamma || p.discount || 0.95);
2202
+ const epsilon = Number(p.epsilon || 0.1);
2203
+ const gridW = Math.min(Number(p.width || 5), 10);
2204
+ const gridH = Math.min(Number(p.height || 5), 10);
2205
+ const goalX = Number(p.goal_x ?? gridW - 1);
2206
+ const goalY = Number(p.goal_y ?? gridH - 1);
2207
+ const reward = Number(p.reward || 10);
2208
+ // Cliff world: penalty zone
2209
+ const cliffPenalty = Number(p.cliff_penalty || -100);
2210
+ const hasCliff = Boolean(p.cliff);
2211
+ const nStates = gridW * gridH;
2212
+ const Q = Array.from({ length: nStates }, () => new Array(4).fill(0));
2213
+ function stateIdx(x, y) { return y * gridW + x; }
2214
+ function isValid(x, y) { return x >= 0 && x < gridW && y >= 0 && y < gridH; }
2215
+ function isCliff(x, y) {
2216
+ if (!hasCliff)
2217
+ return false;
2218
+ return y === gridH - 1 && x > 0 && x < gridW - 1;
2219
+ }
2220
+ function step(x, y, action) {
2221
+ let nx = x, ny = y;
2222
+ if (action === 0)
2223
+ ny--;
2224
+ else if (action === 1)
2225
+ ny++;
2226
+ else if (action === 2)
2227
+ nx--;
2228
+ else if (action === 3)
2229
+ nx++;
2230
+ if (!isValid(nx, ny)) {
2231
+ nx = x;
2232
+ ny = y;
2233
+ }
2234
+ let r = -1;
2235
+ if (isCliff(nx, ny)) {
2236
+ r = cliffPenalty;
2237
+ nx = 0;
2238
+ ny = gridH - 1;
2239
+ }
2240
+ if (nx === goalX && ny === goalY)
2241
+ r = reward;
2242
+ return { nx, ny, r };
2243
+ }
2244
+ function chooseAction(s) {
2245
+ if (Math.random() < epsilon)
2246
+ return Math.floor(Math.random() * 4);
2247
+ return Q[s].indexOf(Math.max(...Q[s]));
2248
+ }
2249
+ const episodeRewards = [];
2250
+ for (let trial = 0; trial < nTrials; trial++) {
2251
+ let x = 0, y = hasCliff ? gridH - 1 : 0;
2252
+ let s = stateIdx(x, y);
2253
+ let a = chooseAction(s);
2254
+ let totalReward = 0;
2255
+ let steps = 0;
2256
+ while (!(x === goalX && y === goalY) && steps < gridW * gridH * 4) {
2257
+ const { nx, ny, r } = step(x, y, a);
2258
+ const sNext = stateIdx(nx, ny);
2259
+ const aNext = chooseAction(sNext);
2260
+ // SARSA update: Q(s,a) ← Q(s,a) + α[r + γ·Q(s',a') - Q(s,a)]
2261
+ Q[s][a] += alpha * (r + gamma * Q[sNext][aNext] - Q[s][a]);
2262
+ totalReward += r;
2263
+ x = nx;
2264
+ y = ny;
2265
+ s = sNext;
2266
+ a = aNext;
2267
+ steps++;
2268
+ }
2269
+ episodeRewards.push(totalReward);
2270
+ }
2271
+ parts.push('## SARSA Learning (On-Policy TD Control)');
2272
+ parts.push('');
2273
+ parts.push(`**Update**: Q(s,a) \u2190 Q(s,a) + \u03B1[r + \u03B3\u00B7Q(s\',a\') - Q(s,a)]`);
2274
+ parts.push(`**Parameters**: \u03B1=${alpha}, \u03B3=${gamma}, \u03B5=${epsilon}`);
2275
+ parts.push(`**Grid**: ${gridW}x${gridH}${hasCliff ? ' (cliff world)' : ''}`);
2276
+ parts.push('');
2277
+ // Policy visualization
2278
+ const arrowMap = ['\u2191', '\u2193', '\u2190', '\u2192'];
2279
+ parts.push('### Learned Policy');
2280
+ let grid = '```\n';
2281
+ for (let y = 0; y < gridH; y++) {
2282
+ let row = '';
2283
+ for (let x = 0; x < gridW; x++) {
2284
+ if (x === goalX && y === goalY)
2285
+ row += ' G ';
2286
+ else if (isCliff(x, y))
2287
+ row += ' C ';
2288
+ else {
2289
+ const s = stateIdx(x, y);
2290
+ row += ` ${arrowMap[Q[s].indexOf(Math.max(...Q[s]))]} `;
2291
+ }
2292
+ }
2293
+ grid += row + '\n';
2294
+ }
2295
+ grid += '```';
2296
+ parts.push(grid);
2297
+ parts.push('');
2298
+ // Note difference from Q-learning
2299
+ parts.push('### SARSA vs Q-Learning Comparison');
2300
+ parts.push('- **SARSA** (on-policy): Learns policy it actually follows, accounts for exploration');
2301
+ parts.push('- **Q-learning** (off-policy): Learns optimal policy regardless of behavior');
2302
+ parts.push('- In cliff worlds, SARSA takes safer path; Q-learning walks the cliff edge');
2303
+ parts.push('');
2304
+ const lastBucket = episodeRewards.slice(-10);
2305
+ parts.push(`**Final 10 episodes avg reward**: ${fmt(lastBucket.reduce((a, b) => a + b, 0) / lastBucket.length, 4)}`);
2306
+ }
2307
+ else if (model === 'hebbian') {
2308
+ // Hebbian learning: Δw = η * x * y
2309
+ const eta = Number(p.eta || p.learning_rate || 0.01);
2310
+ const nPre = Math.min(Number(p.n_pre || p.inputs || 5), 20);
2311
+ const nPost = Math.min(Number(p.n_post || p.outputs || 3), 10);
2312
+ const normalize = Boolean(p.normalize ?? true);
2313
+ const rule = String(p.rule || 'basic').toLowerCase();
2314
+ // Weight matrix
2315
+ const W = Array.from({ length: nPost }, () => Array.from({ length: nPre }, () => (Math.random() - 0.5) * 0.1));
2316
+ const weightHistory = [];
2317
+ parts.push('## Hebbian Learning');
2318
+ parts.push('');
2319
+ if (rule === 'oja') {
2320
+ parts.push(`**Oja's Rule**: \u0394w = \u03B7 \u00D7 y \u00D7 (x - y \u00D7 w) (normalized Hebb)`);
2321
+ }
2322
+ else if (rule === 'bcm') {
2323
+ parts.push(`**BCM Rule**: \u0394w = \u03B7 \u00D7 y \u00D7 (y - \u03B8) \u00D7 x (sliding threshold)`);
2324
+ }
2325
+ else {
2326
+ parts.push(`**Basic Hebb**: \u0394w = \u03B7 \u00D7 x \u00D7 y`);
2327
+ }
2328
+ parts.push(`**Parameters**: \u03B7=${eta}, pre=${nPre}, post=${nPost}, normalize=${normalize}`);
2329
+ parts.push('');
2330
+ let theta = 0.5; // BCM threshold
2331
+ for (let t = 0; t < nTrials; t++) {
2332
+ // Generate random input pattern
2333
+ const x = Array.from({ length: nPre }, () => Math.random());
2334
+ // Compute output
2335
+ const y = new Array(nPost).fill(0);
2336
+ for (let j = 0; j < nPost; j++) {
2337
+ for (let i = 0; i < nPre; i++) {
2338
+ y[j] += W[j][i] * x[i];
2339
+ }
2340
+ y[j] = Math.max(0, y[j]); // ReLU activation
2341
+ }
2342
+ // Update weights
2343
+ for (let j = 0; j < nPost; j++) {
2344
+ for (let i = 0; i < nPre; i++) {
2345
+ if (rule === 'oja') {
2346
+ W[j][i] += eta * y[j] * (x[i] - y[j] * W[j][i]);
2347
+ }
2348
+ else if (rule === 'bcm') {
2349
+ W[j][i] += eta * y[j] * (y[j] - theta) * x[i];
2350
+ }
2351
+ else {
2352
+ W[j][i] += eta * x[i] * y[j];
2353
+ }
2354
+ }
2355
+ }
2356
+ // BCM: update threshold
2357
+ if (rule === 'bcm') {
2358
+ const avgY2 = y.reduce((s, yi) => s + yi * yi, 0) / nPost;
2359
+ theta = theta * 0.99 + avgY2 * 0.01;
2360
+ }
2361
+ // Normalize weights if requested (Oja's rule is self-normalizing)
2362
+ if (normalize && rule !== 'oja') {
2363
+ for (let j = 0; j < nPost; j++) {
2364
+ const norm = Math.sqrt(W[j].reduce((s, w) => s + w * w, 0));
2365
+ if (norm > 1) {
2366
+ for (let i = 0; i < nPre; i++)
2367
+ W[j][i] /= norm;
2368
+ }
2369
+ }
2370
+ }
2371
+ // Record stats
2372
+ if (t % Math.max(1, Math.floor(nTrials / 20)) === 0 || t === nTrials - 1) {
2373
+ const allW = W.flat();
2374
+ const avgW = allW.reduce((a, b) => a + Math.abs(b), 0) / allW.length;
2375
+ const maxW = Math.max(...allW.map(Math.abs));
2376
+ const normW = Math.sqrt(allW.reduce((s, w) => s + w * w, 0));
2377
+ weightHistory.push({ trial: t + 1, avgW, maxW, normW });
2378
+ }
2379
+ }
2380
+ parts.push('### Weight Evolution');
2381
+ parts.push('| Trial | Avg |W| | Max |W| | ||W|| |');
2382
+ parts.push('|---|---|---|---|');
2383
+ for (const h of weightHistory) {
2384
+ parts.push(`| ${h.trial} | ${fmt(h.avgW, 4)} | ${fmt(h.maxW, 4)} | ${fmt(h.normW, 4)} |`);
2385
+ }
2386
+ parts.push('');
2387
+ parts.push('### Final Weight Matrix');
2388
+ parts.push('| Post \\ Pre | ' + Array.from({ length: nPre }, (_, i) => `x${i}`).join(' | ') + ' |');
2389
+ parts.push('|---|' + Array.from({ length: nPre }, () => '---').join('|') + '|');
2390
+ for (let j = 0; j < nPost; j++) {
2391
+ parts.push(`| y${j} | ${W[j].map(w => fmt(w, 3)).join(' | ')} |`);
2392
+ }
2393
+ parts.push('');
2394
+ parts.push('### Properties');
2395
+ if (rule === 'oja') {
2396
+ parts.push("- Oja's rule extracts the first principal component");
2397
+ parts.push('- Weights are self-normalizing (no weight explosion)');
2398
+ }
2399
+ else if (rule === 'bcm') {
2400
+ parts.push('- BCM develops orientation selectivity');
2401
+ parts.push('- Sliding threshold prevents runaway excitation');
2402
+ }
2403
+ else {
2404
+ parts.push('- Basic Hebb is unstable without normalization (weights explode)');
2405
+ parts.push('- "Neurons that fire together wire together"');
2406
+ }
2407
+ }
2408
+ else if (model === 'bayesian') {
2409
+ // Bayesian learning: update prior with evidence
2410
+ const priorMean = Number(p.prior_mean || p.mu_0 || 0);
2411
+ const priorVar = Number(p.prior_var || p.sigma_0_sq || 1);
2412
+ const likelihoodVar = Number(p.likelihood_var || p.sigma_sq || 1);
2413
+ const trueValue = Number(p.true_value || p.theta || 1.0);
2414
+ parts.push('## Bayesian Learning (Gaussian)');
2415
+ parts.push('');
2416
+ parts.push(`**Prior**: N(\u03BC\u2080=${priorMean}, \u03C3\u2080\u00B2=${priorVar})`);
2417
+ parts.push(`**Likelihood**: N(\u03B8, \u03C3\u00B2=${likelihoodVar})`);
2418
+ parts.push(`**True value (\u03B8)**: ${trueValue}`);
2419
+ parts.push('');
2420
+ parts.push(`**Posterior update**: \u03BC_n = (\u03C3\u00B2\u00B7\u03BC_{n-1} + \u03C3\u00B2_{n-1}\u00B7x_n) / (\u03C3\u00B2 + \u03C3\u00B2_{n-1})`);
2421
+ parts.push('');
2422
+ let mu = priorMean;
2423
+ let sigma2 = priorVar;
2424
+ const history = [];
2425
+ for (let t = 1; t <= nTrials; t++) {
2426
+ // Generate noisy observation
2427
+ const observation = trueValue + Math.sqrt(likelihoodVar) * gaussianRandom();
2428
+ // Bayesian update (conjugate prior for Gaussian)
2429
+ const newSigma2 = 1 / (1 / sigma2 + 1 / likelihoodVar);
2430
+ const newMu = newSigma2 * (mu / sigma2 + observation / likelihoodVar);
2431
+ mu = newMu;
2432
+ sigma2 = newSigma2;
2433
+ if (t <= 10 || t % Math.max(1, Math.floor(nTrials / 20)) === 0 || t === nTrials) {
2434
+ const ci = 1.96 * Math.sqrt(sigma2);
2435
+ history.push({ trial: t, observation, mu, sigma2, ci_low: mu - ci, ci_high: mu + ci });
2436
+ }
2437
+ }
2438
+ parts.push('### Posterior Evolution');
2439
+ parts.push('| Trial | Observation | Posterior \u03BC | Posterior \u03C3\u00B2 | 95% CI |');
2440
+ parts.push('|---|---|---|---|---|');
2441
+ parts.push(`| 0 (prior) | - | ${fmt(priorMean, 4)} | ${fmt(priorVar, 4)} | [${fmt(priorMean - 1.96 * Math.sqrt(priorVar), 4)}, ${fmt(priorMean + 1.96 * Math.sqrt(priorVar), 4)}] |`);
2442
+ for (const h of history) {
2443
+ parts.push(`| ${h.trial} | ${fmt(h.observation, 4)} | ${fmt(h.mu, 4)} | ${fmt(h.sigma2, 6)} | [${fmt(h.ci_low, 4)}, ${fmt(h.ci_high, 4)}] |`);
2444
+ }
2445
+ parts.push('');
2446
+ parts.push(`**Final estimate**: \u03B8 \u2248 ${fmt(mu, 6)} \u00B1 ${fmt(1.96 * Math.sqrt(sigma2), 6)} (95% CI)`);
2447
+ parts.push(`**True value**: ${trueValue}`);
2448
+ parts.push(`**Error**: ${fmt(Math.abs(mu - trueValue), 6)}`);
2449
+ parts.push('');
2450
+ parts.push('### Key Properties');
2451
+ parts.push('- Posterior variance shrinks as ~1/n (precision grows linearly)');
2452
+ parts.push('- Posterior mean is a precision-weighted average of prior and data');
2453
+ parts.push('- As n\u2192\u221E, posterior concentrates on true value (consistency)');
2454
+ parts.push(`- After ${nTrials} observations, uncertainty reduced by factor of ${fmt(priorVar / sigma2, 3)}x`);
2455
+ }
2456
+ else {
2457
+ return `Unknown model: "${model}". Supported: rescorla_wagner, td, q_learning, sarsa, hebbian, bayesian`;
2458
+ }
2459
+ return parts.join('\n');
2460
+ },
2461
+ });
2462
+ }
2463
+ // ─── Gaussian random helper (Box-Muller) ─────────────────────────────────────
2464
+ function gaussianRandom() {
2465
+ let u = 0, v = 0;
2466
+ while (u === 0)
2467
+ u = Math.random();
2468
+ while (v === 0)
2469
+ v = Math.random();
2470
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
2471
+ }
2472
+ //# sourceMappingURL=lab-neuro.js.map