plasticity_lab/config.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration knobs for [`crate::PlasticityTrainer`].
6///
7/// Every field here drives an explicit runtime code path in
8/// [`crate::trainer::PlasticityTrainer::train_step`] — see that method's rustdoc
9/// for exactly how each field is consumed. `TrainingConfig` intentionally does
10/// not expose knobs without a consumer (learning rate, homeostasis setpoints,
11/// and batch size were removed for this reason; see `CHANGELOG.md`). Low-level
12/// STDP / homeostasis tuning lives in `neuromod::SpikingNetwork`, which derives
13/// its own learning rate and thresholds from the neuromodulator state passed
14/// into `step`.
15///
16/// Values are serializable (serde) so callers can persist them as part of their
17/// own experiment configs or checkpointing setup — this crate does not implement
18/// checkpointing itself. Missing fields deserialize via [`Default`]
19/// (`#[serde(default)]` on the struct), and unknown fields (for example from an
20/// older config that still carries a since-removed knob) are ignored rather
21/// than rejected, since the struct does not use `deny_unknown_fields`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(default)]
24pub struct TrainingConfig {
25 /// When `true` (default), `train_step` adjusts neuromodulators from the reward.
26 /// When `false`, the network steps with its current modulators unchanged.
27 pub use_reward_modulation: bool,
28}
29
30impl Default for TrainingConfig {
31 fn default() -> Self {
32 Self {
33 use_reward_modulation: true,
34 }
35 }
36}
37
38#[cfg(test)]
39mod tests {
40 use super::TrainingConfig;
41
42 #[test]
43 fn default_enables_reward_modulation() {
44 assert!(TrainingConfig::default().use_reward_modulation);
45 }
46
47 #[test]
48 fn missing_fields_deserialize_to_default() {
49 let cfg: TrainingConfig =
50 serde_json::from_str("{}").expect("deserialize empty config object");
51 assert_eq!(cfg, TrainingConfig::default());
52 }
53
54 #[test]
55 fn serialization_round_trip_preserves_false() {
56 let cfg = TrainingConfig {
57 use_reward_modulation: false,
58 };
59 let json = serde_json::to_string(&cfg).expect("serialize config");
60 let round_tripped: TrainingConfig =
61 serde_json::from_str(&json).expect("deserialize config");
62 assert_eq!(cfg, round_tripped);
63 }
64
65 #[test]
66 fn serialization_round_trip_preserves_default() {
67 let cfg = TrainingConfig::default();
68 let json = serde_json::to_string(&cfg).expect("serialize default config");
69 let round_tripped: TrainingConfig =
70 serde_json::from_str(&json).expect("deserialize default config");
71 assert_eq!(cfg, round_tripped);
72 }
73
74 #[test]
75 fn stale_fields_from_pre_0_2_configs_are_ignored_not_rejected() {
76 // Configs serialized before the fields were removed (see CHANGELOG.md)
77 // may still carry `learning_rate`, `target_spikes_per_step`,
78 // `homeostasis_strength`, and `batch_size` in stored checkpoints or
79 // experiment configs. None of those fields were ever read by the
80 // trainer; deserialization must keep ignoring them rather than error.
81 let cfg: TrainingConfig = serde_json::from_str(
82 r#"{
83 "learning_rate": 0.02,
84 "target_spikes_per_step": 0.1,
85 "homeostasis_strength": 0.001,
86 "batch_size": 4,
87 "use_reward_modulation": false
88 }"#,
89 )
90 .expect("deserialize config carrying removed fields");
91 assert!(!cfg.use_reward_modulation);
92 }
93}