SaltedLemon commited on
Commit
c67f1b7
·
verified ·
1 Parent(s): f85ad7e

Upload code/scripts/test_training.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. code/scripts/test_training.py +142 -0
code/scripts/test_training.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the training script's staging, curricula and checkpoint logic.
2
+
3
+ Separate from ``test_planner.py``, which covers the recursion itself. These
4
+ cover the wiring around it — the places where a run can be configured into
5
+ something other than what the stage table says, silently.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+ import torch
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
15
+
16
+ from lejepa_control.losses import arrival_hold_loss # noqa: E402
17
+ from lejepa_control_2.scripts.train_planner import ( # noqa: E402
18
+ STAGES,
19
+ current_value,
20
+ parse_args,
21
+ parse_curriculum,
22
+ )
23
+
24
+
25
+ def test_stage_presets_match_the_bring_up_table():
26
+ """Each stage turns on exactly one more mechanism than the last."""
27
+ a = parse_args(['--stage', 'A'])
28
+ assert a.cycles == 1 and a.use_feedback is False
29
+ assert (a.lambda_cycle, a.lambda_anchor, a.lambda_sat, a.lambda_support) \
30
+ == (0.0, 0.0, 0.0, 0.0)
31
+
32
+ b = parse_args(['--stage', 'B'])
33
+ assert b.cycles == 3 and b.use_feedback is True and b.lambda_cycle == 0.3
34
+ assert (b.lambda_anchor, b.lambda_sat, b.lambda_support) == (0.0, 0.0, 0.0)
35
+
36
+ c = parse_args(['--stage', 'C'])
37
+ assert (c.lambda_anchor, c.lambda_sat) == (0.05, 1e-3)
38
+ assert c.lambda_support == 0.0
39
+
40
+ d = parse_args(['--stage', 'D'])
41
+ assert d.lambda_support == 0.01
42
+ assert max(v for _, v in parse_curriculum(d.horizon_curriculum, 100)) == 5
43
+
44
+ # E is D with warm start off — the cold-start control, not a new mechanism
45
+ e = parse_args(['--stage', 'E'])
46
+ assert e.warm_start is False
47
+ for key in ('cycles', 'lambda_cycle', 'lambda_support', 'lambda_anchor',
48
+ 'lambda_sat', 'horizon_curriculum'):
49
+ assert getattr(e, key) == getattr(d, key), f'E differs from D in {key}'
50
+
51
+
52
+ def test_explicit_flags_override_the_preset():
53
+ """A preset must not silently win over something typed on the CLI."""
54
+ args = parse_args(['--stage', 'A', '--cycles', '5', '--lambda-cycle', '0.7'])
55
+ assert args.cycles == 5 and args.lambda_cycle == 0.7
56
+ # and untouched keys still come from the preset
57
+ assert args.use_feedback is False
58
+
59
+
60
+ def test_default_run_uses_the_section_10_hyperparameters():
61
+ args = parse_args([])
62
+ assert (args.inner, args.cycles) == (6, 3)
63
+ assert (args.hold_weight, args.alpha) == (0.5, 0.05)
64
+ assert (args.lambda_cycle, args.lambda_support) == (0.3, 0.01)
65
+ assert (args.lambda_anchor, args.lambda_sat) == (0.05, 1e-3)
66
+ assert (args.lr, args.weight_decay, args.grad_clip) == (1e-4, 1e-4, 1.0)
67
+ assert (args.batch_size, args.steps) == (32, 20000)
68
+ assert args.curriculum == '0:2,0.25:3,0.5:5'
69
+ assert parse_curriculum(args.horizon_curriculum, 20000) == [
70
+ (0, 3), (10000, 5)
71
+ ]
72
+
73
+
74
+ def test_curriculum_advances_at_the_right_steps():
75
+ stages = parse_curriculum('0:2,0.25:3,0.5:5', 20000)
76
+ assert stages == [(0, 2), (5000, 3), (10000, 5)]
77
+ assert current_value(stages, 0) == 2
78
+ assert current_value(stages, 4999) == 2
79
+ assert current_value(stages, 5000) == 3
80
+ assert current_value(stages, 19999) == 5
81
+
82
+
83
+ @pytest.mark.parametrize(
84
+ 'horizon_spec,step,want_h,want_q',
85
+ [
86
+ # stage D: the two curricula advance together, so the clamp is inert
87
+ ('0:3,0.5:5', 0, 3, 2),
88
+ ('0:3,0.5:5', 6000, 3, 3),
89
+ ('0:3,0.5:5', 12000, 5, 5),
90
+ # stages B and C: H is pinned at 3 while the offset curriculum still
91
+ # climbs to 5. This is where the clamp actually bites.
92
+ ('0:3', 12000, 3, 3),
93
+ ],
94
+ )
95
+ def test_goal_offset_is_clamped_to_the_live_horizon(
96
+ horizon_spec, step, want_h, want_q
97
+ ):
98
+ """The offset curriculum must not outrun the horizon being rolled out.
99
+
100
+ Stages B and C hold ``H = 3`` while the shared offset curriculum advances
101
+ to 5 at the halfway point. Sampling ``q = 5`` against a 3-step rollout
102
+ would be silently clamped down to 3 inside ``arrival_hold_loss``, so the
103
+ deadline would stop meaning what the curriculum says it means — and the
104
+ dataset would be relabeling goals from 5 transitions ahead that the
105
+ planner has no steps left to reach. The clamp belongs at the sampler.
106
+ """
107
+ horizon = current_value(parse_curriculum(horizon_spec, 20000), step)
108
+ offset_stages = parse_curriculum('0:2,0.25:3,0.5:5', 20000)
109
+ offset = min(current_value(offset_stages, step), horizon)
110
+ assert (horizon, offset) == (want_h, want_q)
111
+ assert offset <= horizon
112
+
113
+
114
+ def test_arrival_term_is_undistorted_once_the_offset_is_clamped():
115
+ """With the clamp in place ``q`` always indexes a step that exists."""
116
+ d = torch.tensor([[0.9, 0.6, 0.3]])
117
+ for q in (1, 2, 3):
118
+ offset = torch.tensor([q])
119
+ got = arrival_hold_loss(d, offset, 0.5).mean()
120
+ hold = d[:, q:].mean(dim=1) if q < 3 else torch.zeros(1)
121
+ assert torch.allclose(got, (d[:, q - 1] + 0.5 * hold).mean())
122
+
123
+
124
+ def test_ablation_switches_are_reachable_but_off_by_default():
125
+ """Section 12's ablations are wired behind flags, not on by default."""
126
+ args = parse_args([])
127
+ assert args.detach_schedule == 'last-cycle' # ablation 4
128
+ assert args.terminal_only is False # ablation 5
129
+ assert args.path_weighting == 'late' # ablation 6
130
+ assert args.use_feedback is True # ablation 2
131
+
132
+ assert parse_args(['--terminal-only']).terminal_only is True
133
+ assert parse_args(['--no-feedback']).use_feedback is False
134
+ assert parse_args(['--path-weighting', 'discount']).path_weighting \
135
+ == 'discount'
136
+ for schedule in ('last-cycle', 'one-step', 'full'):
137
+ assert parse_args(['--detach-schedule', schedule]).detach_schedule \
138
+ == schedule
139
+
140
+
141
+ if __name__ == '__main__':
142
+ sys.exit(pytest.main([__file__, '-v', '--no-header']))