code3939 commited on
Commit
b7e9b58
·
verified ·
1 Parent(s): b685739

Upload 765 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +15 -0
  2. Upload/01_Source_Code/Notebooks/DicisionTransformer.ipynb +0 -0
  3. Upload/01_Source_Code/Notebooks/Performance_Analysis_RTG.ipynb +0 -0
  4. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/Run-Unity.ps1 +32 -0
  5. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_diagnostics.py +63 -0
  6. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_reward_components.py +44 -0
  7. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_statistics.py +116 -0
  8. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/audit_additional.py +124 -0
  9. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/fetch_reference_weights.py +37 -0
  10. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/inspect_public_data.py +45 -0
  11. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/prepare_additional.py +72 -0
  12. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/progress.py +18 -0
  13. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/report_additional.py +100 -0
  14. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/run_additional.py +93 -0
  15. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/validate_additional.py +185 -0
  16. Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/verify_pytorch_weights.py +36 -0
  17. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/Run-Unity.ps1 +28 -0
  18. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/capture_changes.py +52 -0
  19. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/finalize_report.py +57 -0
  20. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/prepare_jobs.py +76 -0
  21. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/progress.py +28 -0
  22. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/run_phase.py +93 -0
  23. Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/summarize_results.py +58 -0
  24. Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tests/test_validate_evaluation.py +78 -0
  25. Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tools/validate_evaluation.py +180 -0
  26. Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tools/verify_model_pair.py +71 -0
  27. Upload/01_Source_Code/Python_Training/dataset_dt.py +193 -0
  28. Upload/01_Source_Code/Python_Training/finetuning_ext_RLStep.py +215 -0
  29. Upload/01_Source_Code/Python_Training/model_dt.py +85 -0
  30. Upload/01_Source_Code/Python_Training/train_sequential_ext_RLStep.py +181 -0
  31. Upload/01_Source_Code/Python_Training/train_sequential_ext_RLStep_For_BC.py +183 -0
  32. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations.meta +8 -0
  33. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem.demo +0 -0
  34. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem.demo.meta +10 -0
  35. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem_0.demo +0 -0
  36. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem_0.demo.meta +10 -0
  37. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/bulletdodgedemo.demo +0 -0
  38. Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/bulletdodgedemo.demo.meta +10 -0
  39. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor.meta +8 -0
  40. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation.meta +8 -0
  41. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs +164 -0
  42. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs.meta +2 -0
  43. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationModelParityTests.cs +120 -0
  44. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationModelParityTests.cs.meta +2 -0
  45. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationSmokeTests.cs +115 -0
  46. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationSmokeTests.cs.meta +2 -0
  47. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/RevisionEvaluationBatch.cs +318 -0
  48. Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/RevisionEvaluationBatch.cs.meta +2 -0
  49. Upload/01_Source_Code/Unity_Evaluation/Assets/ML-Agents.meta +8 -0
  50. Upload/01_Source_Code/Unity_Evaluation/Assets/ML-Agents/Configs.meta +8 -0
.gitattributes CHANGED
@@ -34,3 +34,18 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  ML_AGENT[[:space:]]Unity[[:space:]]Usage.docx filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  ML_AGENT[[:space:]]Unity[[:space:]]Usage.docx filter=lfs diff=lfs merge=lfs -text
37
+ Upload/01_Source_Code/Unity_Evaluation/Assets/Model/dt_model_12.onnx.data filter=lfs diff=lfs merge=lfs -text
38
+ Upload/01_Source_Code/Unity_Evaluation/Assets/Model/dt_model_final.onnx.data filter=lfs diff=lfs merge=lfs -text
39
+ Upload/01_Source_Code/Unity_Evaluation/Assets/Model/dt_model.onnx.data filter=lfs diff=lfs merge=lfs -text
40
+ Upload/01_Source_Code/Unity_Evaluation/Assets/Model/Old/dt_model_11.onnx[[:space:]]1.data filter=lfs diff=lfs merge=lfs -text
41
+ Upload/01_Source_Code/Unity_Evaluation/Assets/Model/Old/dt_model_11.onnx.data filter=lfs diff=lfs merge=lfs -text
42
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/net20/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
43
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/net35/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
44
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/net40/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
45
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/net45/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
46
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/net6.0/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
47
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/netstandard1.0/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
48
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/netstandard1.3/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
49
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/lib/netstandard2.0/Newtonsoft.Json.dll filter=lfs diff=lfs merge=lfs -text
50
+ Upload/01_Source_Code/Unity_Evaluation/Packages/Newtonsoft.Json.13.0.4/Newtonsoft.Json.13.0.4.nupkg filter=lfs diff=lfs merge=lfs -text
51
+ Upload/06_Evaluation_Results/Manuscript_Figures/figure-17.png filter=lfs diff=lfs merge=lfs -text
Upload/01_Source_Code/Notebooks/DicisionTransformer.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
Upload/01_Source_Code/Notebooks/Performance_Analysis_RTG.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/Run-Unity.ps1 ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [ValidateSet('Verify','Run','Latency')][string]$Mode = 'Verify',
3
+ [string]$JobPath = '',
4
+ [string]$RunName = 'additional-verification'
5
+ )
6
+ $ErrorActionPreference = 'Stop'
7
+ $taskRoot = Split-Path -Parent $PSScriptRoot
8
+ $root = Split-Path -Parent (Split-Path -Parent $taskRoot)
9
+ $projectRoot = Join-Path $root 'RevisionEval'
10
+ $editor = 'C:/Program Files/Unity/Hub/Editor/6000.0.62f1/Editor/Unity.exe'
11
+ $method = if ($Mode -eq 'Run') { 'RevisionEvaluationBatch.Run' } elseif ($Mode -eq 'Verify') { 'AdditionalEvaluationTools.Verify' } else { 'AdditionalEvaluationTools.MeasureLatency' }
12
+ if (Test-Path -LiteralPath "$taskRoot/Logs/$RunName.log") { throw 'Use a fresh run name; prior logs are preserved.' }
13
+ $editorArguments = @('-batchmode','-force-d3d11','-projectPath',('"'+$projectRoot+'"'),
14
+ '-executeMethod',$method,'-logFile',('"'+$taskRoot+'/Logs/'+$RunName+'.log"'))
15
+ if ($Mode -eq 'Run') {
16
+ $job = Get-Content -LiteralPath $JobPath -Raw -Encoding UTF8 | ConvertFrom-Json
17
+ $editorArguments += @('-evaluationJob',('"'+(Resolve-Path -LiteralPath $JobPath).Path+'"'),
18
+ '-evaluationOutput',('"'+$job.output+'"'))
19
+ } else {
20
+ $editorArguments += @('-evaluationFixtures',('"'+$taskRoot+'/Verification/actual-input-parity.json"'),
21
+ '-originalFixtures',('"'+$root+'/Document/RevisionEvaluation_Local_20260910/Verification/E_1_DT_C_5_sequence_fixtures.json"'),
22
+ '-evaluationReport',('"'+$taskRoot+'/Reports/'+$RunName+'.json"'))
23
+ }
24
+ $editorArguments | ConvertTo-Json | Set-Content -LiteralPath "$taskRoot/Reports/$RunName-command.json" -Encoding UTF8
25
+ $taskProcess = Start-Process -FilePath $editor -ArgumentList $editorArguments -WindowStyle Hidden -PassThru
26
+ $taskProcess.Id | Set-Content -LiteralPath "$taskRoot/Reports/$RunName.pid"
27
+ Write-Output "$RunName Unity PID: $($taskProcess.Id)"
28
+ $taskProcess.WaitForExit()
29
+ $taskProcess.Refresh()
30
+ $taskProcess.ExitCode | Set-Content -LiteralPath "$taskRoot/Reports/$RunName-exit-code.txt"
31
+ Write-Output "$RunName exit code: $($taskProcess.ExitCode)"
32
+ exit $taskProcess.ExitCode
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_diagnostics.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reproducible follow-up contrasts; analysis is exploratory after observing results."""
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+ from analyze_statistics import METRICS, holm, paired
6
+
7
+ TASK = Path(__file__).resolve().parents[1]
8
+
9
+
10
+ def load(p):
11
+ return json.loads(p.read_text(encoding='utf-8-sig'))
12
+
13
+
14
+ def one(folder):
15
+ files = sorted(folder.rglob('*.json'))
16
+ assert len(files) == 1
17
+ return files[0], load(files[0])
18
+
19
+
20
+ def provenance(files):
21
+ return [dict(path=str(p.relative_to(TASK)), sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files]
22
+
23
+
24
+ def save(name, report):
25
+ (TASK / 'Reports' / name).write_text(json.dumps(report, indent=2), encoding='utf-8')
26
+
27
+
28
+ def main():
29
+ pa, a = one(TASK / 'Results/bc-diagnostic/bc-normal')
30
+ pb, b = one(TASK / 'Results/bc-diagnostic/bc-legacy-rtg')
31
+ assert not a['evaluation']['diagnostic_only'] and b['evaluation']['diagnostic_only']
32
+ bc = [paired(a, b, metric, 'BC diagnostic/' + metric) for metric in METRICS]
33
+ for r, p in zip(bc, holm([r['p_raw'] for r in bc])):
34
+ r['p_holm_6'] = p
35
+ save('bc-diagnostic-effect.json', dict(status='passed',
36
+ scope='Diagnostic only; same fixed BC weights and paired initial states. Normal minus legacy-decrement.',
37
+ selection='Exploratory follow-up analysis after observing diagnostic results.',
38
+ holm_family_size=6, files=provenance([pa, pb]), comparisons=bc))
39
+ records = [(p, load(p)) for p in sorted((TASK / 'Results/rtg').rglob('*.json'))]
40
+ assert len(records) == 13
41
+ refs = [d for p, d in records if d['evaluation']['policy_type'] == 'DT' and d['evaluation']['initial_rtg'] == 70]
42
+ assert len(refs) == 1
43
+ reference = refs[0]
44
+ rows = []
45
+ for p, d in records:
46
+ m = d['evaluation']
47
+ if d is reference:
48
+ continue
49
+ for metric in METRICS:
50
+ r = paired(d, reference, metric, f"RTG contrast/{m['policy_type']}/{m['initial_rtg']}/{metric}")
51
+ r.update(policy=m['policy_type'], initial_rtg=m['initial_rtg'], reference='DT initial RTG 70')
52
+ rows.append(r)
53
+ assert len(rows) == 72
54
+ for r, p in zip(rows, holm([r['p_raw'] for r in rows])):
55
+ r['p_holm_72'] = p
56
+ save('rtg-statistics.json', dict(status='passed',
57
+ selection='Exploratory analysis after observing the RTG sweep; reference 70 is the unchanged main-evaluation setting.',
58
+ holm_family_size=72, files=provenance([p for p, d in records]), comparisons=rows))
59
+ print(json.dumps(dict(status='passed', bc_comparisons=len(bc), rtg_comparisons=len(rows))))
60
+
61
+
62
+ if __name__ == '__main__':
63
+ main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_reward_components.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algebraic reward decomposition from recorded hits, wins and total rewards."""
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+ import numpy as np
6
+
7
+ TASK = Path(__file__).resolve().parents[1]
8
+ ROOT = TASK.parents[1]
9
+
10
+
11
+ def main():
12
+ files = sorted((TASK / 'Results/heldout').rglob('*.json'))
13
+ assert len(files) == 9
14
+ rows = []
15
+ for p in files:
16
+ d = json.loads(p.read_text(encoding='utf-8-sig'))
17
+ m = d['evaluation']
18
+ assert m['reward_protocol'] == 'ShootingAgent-shaped-angle-v1'
19
+ hits = np.asarray(m['episode_shots_hit'], dtype=float)
20
+ wins = np.asarray(d['all_wins'], dtype=bool)
21
+ rewards = np.asarray(d['all_rewards'], dtype=float)
22
+ remaining = m['target_count'] - hits
23
+ assert len(hits) == len(wins) == len(rewards) == 200
24
+ assert np.all(remaining >= 0) and np.array_equal(wins, remaining == 0)
25
+ hit_reward = 3 * hits
26
+ terminal = np.where(wins, 2., -.01 * remaining)
27
+ residual = rewards - hit_reward - terminal
28
+ rows.append(dict(model=m['model_name'], targets=m['target_count'], episodes=200,
29
+ mean_total_reward=float(rewards.mean()), mean_hit_reward=float(hit_reward.mean()),
30
+ mean_terminal_reward=float(terminal.mean()), mean_step_shaping_residual=float(residual.mean()),
31
+ mean_action_steps=float(np.mean(d['all_steps'])),
32
+ file=str(p.relative_to(TASK)), sha256=hashlib.sha256(p.read_bytes()).hexdigest()))
33
+ source = ROOT / 'RevisionEval/Assets/Script/Trajectory/EvaluationReward.cs'
34
+ report = dict(status='passed',
35
+ method='Residual = recorded total - 3*hits - terminal (2 for success, -0.01*remaining at timeout).',
36
+ limitation='Residual combines alignment, fire/no-fire shaping and step penalties, plus float accumulation error. These terms were not separately logged.',
37
+ reward_source=str(source.relative_to(ROOT)), source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),
38
+ rows=rows)
39
+ (TASK / 'Reports/heldout-reward-components.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
40
+ print(json.dumps(dict(status='passed', rows=len(rows))))
41
+
42
+
43
+ if __name__ == '__main__':
44
+ main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_statistics.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Paired, fixed-checkpoint evaluation analysis. Never modifies raw results."""
2
+ import argparse
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ import numpy as np
7
+ import scipy
8
+ from scipy import stats
9
+
10
+ TASK = Path(__file__).resolve().parents[1]
11
+ METRICS = ['all_wins', 'all_steps', 'all_rewards', 'all_smoothness', 'all_accuracies', 'all_actual_firing_accuracies']
12
+
13
+ def seed_for(label):
14
+ return int(hashlib.sha256(label.encode()).hexdigest()[:8], 16)
15
+
16
+ def mean_ci(x, label):
17
+ x = np.asarray(x, dtype=float)
18
+ if np.ptp(x) == 0:
19
+ return [float(x[0]), float(x[0])]
20
+ res = stats.bootstrap((x,), np.mean, confidence_level=.95, n_resamples=19999,
21
+ batch=512, method='percentile', random_state=seed_for(label))
22
+ return [float(res.confidence_interval.low), float(res.confidence_interval.high)]
23
+
24
+ def holm(ps):
25
+ order = np.argsort(ps)
26
+ out = np.empty(len(ps))
27
+ out[order] = np.minimum(1., np.maximum.accumulate(np.asarray(ps)[order] * np.arange(len(ps), 0, -1)))
28
+ return out.tolist()
29
+
30
+ def paired(a, b, metric, label):
31
+ for key in ['episode_seeds', 'initial_state_sha256', 'initial_states']:
32
+ assert a['evaluation'][key] == b['evaluation'][key], f'Pair mismatch: {key}'
33
+ x, y = np.asarray(a[metric], dtype=float), np.asarray(b[metric], dtype=float)
34
+ d = x-y
35
+ row = dict(metric=metric, n=len(d), mean_a=float(x.mean()), mean_b=float(y.mean()), difference_a_minus_b=float(d.mean()))
36
+ if metric == 'all_wins':
37
+ ab = int(np.sum((x == 1) & (y == 0)))
38
+ ba = int(np.sum((x == 0) & (y == 1)))
39
+ row.update(test='exact McNemar via two-sided binomial discordance test', a_only_success=ab,
40
+ b_only_success=ba, p_raw=float(stats.binomtest(ab, ab+ba, .5).pvalue) if ab+ba else 1.,
41
+ difference_ci95=None, ci_note='Report exact marginal success CIs; no degenerate binary bootstrap CI.')
42
+ else:
43
+ p = 1. if np.all(d == 0) else stats.permutation_test((d,), np.mean,
44
+ permutation_type='samples', alternative='two-sided', n_resamples=49999,
45
+ batch=512, random_state=seed_for(label)).pvalue
46
+ row.update(test='paired sign permutation of mean difference', p_raw=float(p),
47
+ difference_ci95=mean_ci(d, label+' CI'))
48
+ return row
49
+
50
+ def main():
51
+ p = argparse.ArgumentParser()
52
+ p.add_argument('root', type=Path)
53
+ p.add_argument('name')
54
+ args = p.parse_args()
55
+ assert holm([.01, .04, .03]) == [.03, .06, .06]
56
+ assert stats.binomtest(3, 3, .5).pvalue == .25
57
+ records, descriptive, files = {}, [], []
58
+ for file in sorted(args.root.rglob('*.json')):
59
+ d = json.loads(file.read_text(encoding='utf-8-sig'))
60
+ if 'all_wins' not in d: continue
61
+ m = d['evaluation']
62
+ assert not m.get('diagnostic_only', False)
63
+ identity = (m['target_count'], m['model_name'])
64
+ assert identity not in records
65
+ records[identity] = d
66
+ files.append(dict(path=str(file), sha256=hashlib.sha256(file.read_bytes()).hexdigest()))
67
+ n = len(d['all_wins']); k = sum(d['all_wins'])
68
+ ci = stats.binomtest(k,n).proportion_ci(method='exact')
69
+ row = dict(targets=m['target_count'], model=m['model_name'], episodes=n, successes=k,
70
+ success_percent=100*k/n, success_ci95_percent=[100*ci.low,100*ci.high], metrics={})
71
+ for metric in METRICS[1:]:
72
+ x=np.asarray(d[metric],dtype=float)
73
+ row['metrics'][metric]=dict(mean=float(x.mean()), sample_sd=float(x.std(ddof=1)),
74
+ mean_ci95=mean_ci(x,str(identity)+metric))
75
+ descriptive.append(row)
76
+ comparisons=[]
77
+ for target in sorted({k[0] for k in records}):
78
+ models={name:d for (t,name),d in records.items() if t==target}
79
+ ppos=[name for name,d in models.items() if d['evaluation']['policy_type']=='PPO']
80
+ assert len(ppos)==1
81
+ contrasts=[(name,ppos[0]) for name in models if name.startswith('E_3_')]
82
+ contrasts.append(('E_3_DT_S_100','E_3_DT_BC_100'))
83
+ for a,b in contrasts:
84
+ for metric in METRICS:
85
+ row=paired(models[a],models[b],metric,f'{args.name}/{target}/{a}/{b}/{metric}')
86
+ row.update(targets=target, model_a=a, model_b=b)
87
+ comparisons.append(row)
88
+ for row,adjusted in zip(comparisons,holm([r['p_raw'] for r in comparisons])):
89
+ row['p_holm']=adjusted
90
+ row['significant_holm_005']=adjusted<.05
91
+ report=dict(status='passed', analysis_name=args.name, numpy=np.__version__, scipy=scipy.__version__,
92
+ inference_scope='Fixed checkpoints over paired environmental episodes; no independent training-run inference.',
93
+ selection='Follow-up analysis selected after inspecting the previous full results.',
94
+ assumptions='Independent environment episodes; paired label exchangeability for permutation null. PPO action RNG remains stochastic.',
95
+ confidence_intervals='Marginal 95%, not multiplicity-adjusted. Degenerate continuous bootstrap CIs report observed lack of variability only.',
96
+ holm_family_size=len(comparisons), files=files, descriptive=descriptive, comparisons=comparisons)
97
+ (TASK/'Reports'/f'{args.name}-statistics.json').write_text(json.dumps(report,indent=2),encoding='utf-8')
98
+ lines=['# 짝지은 평가 통계 분석', '', '고정된 모델 가중치의 환경 에피소드 비교다. E1/E2/E3를 독립 학습 반복으로 합산하지 않았다.',
99
+ f'전체 {len(comparisons)}개 비교에 Holm 보정. CI는 개별 95% 구간이며 동시 신뢰구간이 아니다.', '',
100
+ '| 표적 | 모델 | 성공/평가 | 성공률 정확 95% CI | 평균 종료 step | 평균 보상 |', '|---:|---|---:|---|---:|---:|']
101
+ for r in descriptive:
102
+ if not r['model'].startswith('E_3') and r['model']!='V12 PPO':continue
103
+ lo,hi=r['success_ci95_percent']
104
+ lines.append(f"| {r['targets']} | {r['model']} | {r['successes']}/{r['episodes']} | {lo:.2f}–{hi:.2f}% | {r['metrics']['all_steps']['mean']:.2f} | {r['metrics']['all_rewards']['mean']:.4f} |")
105
+ lines+=['','## DT_S100과 BC100 비교','','차이는 DT−BC. 성공률은 비율 단위, 정확도는 percentage points.','',
106
+ '| 표적 | 지표 | 평균 차이 | 차이 95% CI | 보정 전 p | Holm p |','|---:|---|---:|---|---:|---:|']
107
+ for r in comparisons:
108
+ if r['model_b']!='E_3_DT_BC_100':continue
109
+ ci=r.get('difference_ci95')
110
+ ci_text='별도 추정하지 않음' if ci is None else f'{ci[0]:.5f}–{ci[1]:.5f}'
111
+ lines.append(f"| {r['targets']} | {r['metric']} | {r['difference_a_minus_b']:.5f} | {ci_text} | {r['p_raw']:.6g} | {r['p_holm']:.6g} |")
112
+ lines+=['','비유의 결과는 동등성 증명이 아니다. 평균 종료 step은 timeout도 포함한다. 모든 비교와 입력 파일 해시는 JSON에 기록했다.']
113
+ (TASK/f'{args.name}_STATISTICS_KO.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
114
+ print(json.dumps(dict(status='passed',files=len(files),episodes=sum(r['episodes'] for r in descriptive),comparisons=len(comparisons)),ensure_ascii=False))
115
+
116
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/audit_additional.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify preservation and completed experiment provenance without changing raw data."""
2
+ import argparse
3
+ import difflib
4
+ import hashlib
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ TASK = Path(__file__).resolve().parents[1]
10
+ ROOT = TASK.parents[1]
11
+ OLD = ROOT / 'Document/RevisionEvaluation_Local_20260910'
12
+
13
+
14
+ def load(p):
15
+ return json.loads(p.read_text(encoding='utf-8-sig'))
16
+
17
+
18
+ def sha(p):
19
+ h = hashlib.sha256()
20
+ with p.open('rb') as f:
21
+ for block in iter(lambda: f.read(1024 * 1024), b''):
22
+ h.update(block)
23
+ return h.hexdigest()
24
+
25
+
26
+ def main():
27
+ parser = argparse.ArgumentParser()
28
+ parser.add_argument('--final', action='store_true')
29
+ args = parser.parse_args()
30
+ checks = []
31
+ original = load(OLD / 'Reports/original-project-sha256.json')
32
+ for r in original:
33
+ p = ROOT / r['path']
34
+ checks.append(dict(kind='original_project', path=str(p), passed=p.exists() and sha(p) == r['sha256']))
35
+ models = load(TASK / 'Reports/models-before-additional.json')
36
+ for r in models:
37
+ for base in [ROOT, ROOT / 'RevisionEval']:
38
+ p = base / r['path']
39
+ checks.append(dict(kind='model', path=str(p), passed=p.exists() and sha(p) == r['sha256']))
40
+ original_meta = ROOT / (r['path'] + '.meta')
41
+ copied_meta = ROOT / 'RevisionEval' / (r['path'] + '.meta')
42
+ checks.append(dict(kind='model_meta', path=str(copied_meta),
43
+ passed=original_meta.exists() and copied_meta.exists() and sha(original_meta) == sha(copied_meta)))
44
+ old_summary = load(OLD / 'Reports/results-summary.json')
45
+ for phase, records in old_summary.items():
46
+ for r in records:
47
+ p = OLD / r['result']
48
+ checks.append(dict(kind='previous_result_' + phase, path=str(p), passed=p.exists() and sha(p) == r['sha256']))
49
+ source = []
50
+ patch = []
51
+ for r in load(TASK / 'Reports/pre-change-source-hashes.json'):
52
+ current = ROOT / r['path']
53
+ backup = TASK / 'Backups' / current.name
54
+ checks.append(dict(kind='source_backup', path=str(backup), passed=sha(backup) == r['sha256']))
55
+ source.append(dict(path=r['path'], before_sha256=r['sha256'], after_sha256=sha(current)))
56
+ patch.extend(difflib.unified_diff(backup.read_text(encoding='utf-8-sig').splitlines(True),
57
+ current.read_text(encoding='utf-8-sig').splitlines(True),
58
+ fromfile='before/' + current.name, tofile=r['path']))
59
+ extra = ROOT / 'RevisionEval/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs'
60
+ source.append(dict(path=str(extra.relative_to(ROOT)), before_sha256=None, after_sha256=sha(extra)))
61
+ (TASK / 'Reports/source-changes.patch').write_text(''.join(patch), encoding='utf-8')
62
+ completed = []
63
+ configurations = []
64
+ jobs = load(TASK / 'Reports/job-manifest.json')
65
+ for j in jobs:
66
+ config = TASK / 'Jobs' / (j['id'] + '.json')
67
+ checks.append(dict(kind='configuration_matches_manifest', path=str(config), passed=load(config) == j))
68
+ configurations.append(dict(path=str(config.relative_to(TASK)), sha256=sha(config)))
69
+ report = Path(j['report'])
70
+ if not report.exists():
71
+ if args.final:
72
+ checks.append(dict(kind='job', path=str(report), passed=False))
73
+ continue
74
+ validation = TASK / 'Reports' / (j['id'] + '-validation.json')
75
+ exit_code = TASK / 'Reports' / (j['id'] + '-exit-code.txt')
76
+ if not args.final and (load(report)['status'] == 'running' or not exit_code.exists() or not validation.exists()):
77
+ continue
78
+ ok = (load(report)['status'] == 'passed' and validation.exists() and
79
+ load(validation)['status'] == 'passed' and exit_code.exists() and
80
+ exit_code.read_text(encoding='utf-8-sig').strip() == '0')
81
+ checks.append(dict(kind='job', path=str(report), passed=ok))
82
+ if ok:
83
+ completed.append(j['id'])
84
+ actual = load(report)['job']
85
+ checks.append(dict(kind='executed_configuration', path=str(report),
86
+ passed=all(actual[k] == v for k, v in j.items() if k in actual)))
87
+ raw = []
88
+ for p in sorted((TASK / 'Results').rglob('*.json')):
89
+ d = load(p)
90
+ if 'all_wins' in d:
91
+ raw.append(dict(path=str(p.relative_to(TASK)), sha256=sha(p), episodes=d['episodes'],
92
+ phase=d['evaluation']['experiment_kind']))
93
+ if args.final:
94
+ for name in ['additional-verification-retry1.json', 'pytorch-additional-parity.json',
95
+ 'onnxruntime-vs-recorded-unity.json', 'existing-statistics.json', 'heldout-statistics.json',
96
+ 'bc-diagnostic-effect.json', 'rtg-statistics.json', 'heldout-reward-components.json']:
97
+ p = TASK / 'Reports' / name
98
+ checks.append(dict(kind='analysis_or_verification', path=str(p), passed=p.exists() and load(p)['status'] == 'passed'))
99
+ checks.append(dict(kind='total_episodes', passed=sum(r['episodes'] for r in raw) == 2610))
100
+ checks.append(dict(kind='total_files', passed=len(raw) == 27))
101
+ for phase in ['bc-diagnostic', 'rtg', 'heldout', 'timing']:
102
+ p = TASK / 'Reports' / (phase + '-phase.json')
103
+ checks.append(dict(kind='phase', path=str(p), passed=p.exists() and load(p)['status'] == 'passed'))
104
+ p = TASK / 'Reports/latency-benchmark.json'
105
+ bench = load(p) if p.exists() else {}
106
+ checks.append(dict(kind='latency_benchmark', path=str(p), passed=bench.get('status') == 'passed' and
107
+ len(bench.get('models', [])) == 3 and
108
+ all(r['measurements'] == len(r['raw_ms']) == 2000 and r['warmup'] == 200 for r in bench.get('models', []))))
109
+ result = dict(status='passed' if all(c['passed'] for c in checks) else 'failed',
110
+ mode='final' if args.final else 'interim', utc=datetime.now(timezone.utc).isoformat(),
111
+ original_files=len(original), unchanged_models_per_project=len(models),
112
+ previous_result_files=sum(map(len, old_summary.values())), completed_jobs=completed,
113
+ result_files=len(raw), result_episodes=sum(r['episodes'] for r in raw),
114
+ failed_checks=[c for c in checks if not c['passed']], checks=checks, sources=source,
115
+ configuration_hash_timing='Recorded at audit; configurations were written before evaluations and checked against their manifest and Unity run reports.',
116
+ configurations=configurations, raw_results=raw)
117
+ out = TASK / 'Reports' / ('final-audit.json' if args.final else 'interim-audit.json')
118
+ out.write_text(json.dumps(result, indent=2), encoding='utf-8')
119
+ print(json.dumps({k:v for k,v in result.items() if k not in ['checks', 'sources', 'raw_results', 'completed_jobs', 'configurations']}))
120
+ assert result['status'] == 'passed'
121
+
122
+
123
+ if __name__ == '__main__':
124
+ main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/fetch_reference_weights.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Download pinned public weight files only. Remote Python is never executed."""
2
+ import hashlib,json,urllib.request,urllib.parse,zipfile
3
+ from pathlib import Path
4
+ from inspect_public_data import RemoteZip
5
+ TASK=Path(__file__).resolve().parents[1]
6
+ ROOT=TASK.parents[1]
7
+
8
+ def main():
9
+ inventory=json.loads((TASK/'Reports/public-data-inventory.json').read_text(encoding='utf-8-sig'))
10
+ archives=json.loads((TASK/'Reports/public-archive-inventory.json').read_text())
11
+ prefix='https://huggingface.co/code3939/DecisionTransformer-Unity-Sim/resolve/'+archives['revision']+'/'
12
+ out=TASK/'Verification/Weights';out.mkdir(exist_ok=True)
13
+ manifest=[]
14
+ for epoch in [1,2,3]:
15
+ name=f'E_{epoch}_DT_BC_100'
16
+ onnx=next(i for i in inventory if i['path']==f'DT Update/OnnxModels/{name}.onnx')
17
+ local=ROOT/f'RevisionEval/Assets/Model/FinalModel/BCModels/{name}.onnx'
18
+ assert hashlib.sha256(local.read_bytes()).hexdigest()==onnx['lfs']['oid']
19
+ pth=next(i for i in inventory if i['path']==f'DT Update/OnnxModels/{name}.pth')
20
+ with urllib.request.urlopen(prefix+urllib.parse.quote(pth['path']),timeout=30) as r:b=r.read(pth['size']+1)
21
+ digest=hashlib.sha256(b).hexdigest();assert len(b)==pth['size'] and digest==pth['lfs']['oid']
22
+ target=out/f'{name}.pth';assert not target.exists();target.write_bytes(b)
23
+ manifest.append(dict(model=name,path=str(target.relative_to(TASK)),sha256=digest,source=pth['path'],revision=archives['revision']))
24
+ print('Fetched and verified',name,flush=True)
25
+ archive=next(i for i in archives['archives'] if i['name']=='DT Models.zip')
26
+ remote=RemoteZip(prefix+urllib.parse.quote(archive['name']),archive['size'])
27
+ with zipfile.ZipFile(remote) as z:
28
+ onnx=z.read('E_3_DT_S_100.onnx')
29
+ assert hashlib.sha256(onnx).hexdigest()==hashlib.sha256((ROOT/'RevisionEval/Assets/Model/FinalModel/E_3_DT_S_100.onnx').read_bytes()).hexdigest()
30
+ b=z.read('E_3_DT_S_100.pth')
31
+ target=out/'E_3_DT_S_100.pth';assert not target.exists();target.write_bytes(b)
32
+ manifest.append(dict(model='E_3_DT_S_100',path=str(target.relative_to(TASK)),sha256=hashlib.sha256(b).hexdigest(),
33
+ source='DT Models.zip/E_3_DT_S_100.pth',revision=archives['revision']))
34
+ (TASK/'Reports/reference-weight-manifest.json').write_text(json.dumps(manifest,indent=2),encoding='utf-8')
35
+ print('Fetched and verified E_3_DT_S_100; paired public ONNX matches local SHA256',flush=True)
36
+
37
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/inspect_public_data.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Read public archive directory via HTTP Range; no pickle deserialization or remote code."""
2
+ import io,json,urllib.request,urllib.parse,zipfile
3
+ from pathlib import Path
4
+ TASK=Path(__file__).resolve().parents[1]
5
+ REPO='https://huggingface.co/code3939/DecisionTransformer-Unity-Sim'
6
+
7
+ class RemoteZip(io.RawIOBase):
8
+ def __init__(self,url,size):self.url=url;self.size=size;self.pos=0;self.bytes_read=0
9
+ def seekable(self):return True
10
+ def readable(self):return True
11
+ def tell(self):return self.pos
12
+ def seek(self,offset,whence=0):
13
+ self.pos=offset if whence==0 else self.pos+offset if whence==1 else self.size+offset
14
+ return self.pos
15
+ def read(self,n=-1):
16
+ if n<0:n=self.size-self.pos
17
+ n=min(n,self.size-self.pos)
18
+ if not n:return b''
19
+ if n>32*1024*1024:raise ValueError('Unexpected large metadata read')
20
+ end=self.pos+n-1
21
+ req=urllib.request.Request(self.url,headers={'Range':f'bytes={self.pos}-{end}'})
22
+ with urllib.request.urlopen(req,timeout=30) as r:
23
+ if r.status!=206 or not r.headers.get('Content-Range','').startswith(f'bytes {self.pos}-{end}/'):
24
+ raise ValueError('Server did not honor exact byte range')
25
+ b=r.read(n+1)
26
+ if len(b)!=n:raise ValueError('Wrong byte count')
27
+ self.pos+=n;self.bytes_read+=n;return b
28
+
29
+ def main():
30
+ with urllib.request.urlopen('https://huggingface.co/api/models/code3939/DecisionTransformer-Unity-Sim',timeout=30) as r:meta=json.load(r)
31
+ revision=meta['sha']
32
+ inventory=json.loads((TASK/'Reports/public-data-inventory.json').read_text(encoding='utf-8-sig'))
33
+ out=dict(revision=revision,archives=[])
34
+ for name in ['DT TrajectoryData.zip','DT Models.zip']:
35
+ item=next(v for v in inventory if v['path']==name)
36
+ url=REPO+'/resolve/'+revision+'/'+urllib.parse.quote(name)
37
+ remote=RemoteZip(url,item['size'])
38
+ with zipfile.ZipFile(remote) as z:
39
+ entries=[dict(name=i.filename,size=i.file_size,compressed=i.compress_size,offset=i.header_offset,crc=i.CRC) for i in z.infolist()]
40
+ out['archives'].append(dict(name=name,sha256=item['lfs']['oid'],size=item['size'],metadata_bytes_read=remote.bytes_read,entries=entries))
41
+ print(name, len(entries),'entries',sum(i['size'] for i in entries),'uncompressed bytes',flush=True)
42
+ for i in entries[:50]:print(i,flush=True)
43
+ (TASK/'Reports/public-archive-inventory.json').write_text(json.dumps(out,indent=2),encoding='utf-8')
44
+
45
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/prepare_additional.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import json
3
+ from pathlib import Path
4
+ import numpy as np
5
+ import onnxruntime as ort
6
+
7
+ TASK=Path(__file__).resolve().parents[1]
8
+ ROOT=TASK.parents[1]
9
+ OLD=ROOT/'Document/RevisionEvaluation_Local_20260910'
10
+ PROJECT=ROOT/'RevisionEval'
11
+ PPO='Assets/Model/V12/V12 PPO.onnx'
12
+ DT='Assets/Model/FinalModel/E_3_DT_S_100.onnx'
13
+ BC='Assets/Model/FinalModel/BCModels/E_3_DT_BC_100.onnx'
14
+
15
+ def save(path,value):
16
+ assert not path.exists(), str(path)
17
+ path.write_text(json.dumps(value,indent=2),encoding='utf-8')
18
+
19
+ def main():
20
+ inventory=json.loads((OLD/'Reports/model-inventory.json').read_text(encoding='utf-8'))
21
+ paths={Path(r['path']).stem:r['path'] for r in inventory}
22
+ sessions={}; cases=[]; maximum=0.
23
+ for file in sorted((OLD/'Results/full/transformers').rglob('*.json')):
24
+ data=json.loads(file.read_text(encoding='utf-8')); meta=data['evaluation']; name=meta['model_name']
25
+ if meta['policy_type']!='BC' and name!='E_3_DT_S_100': continue
26
+ model=paths[name]
27
+ if model not in sessions:
28
+ opt=ort.SessionOptions(); opt.intra_op_num_threads=2;opt.inter_op_num_threads=1
29
+ sessions[model]=ort.InferenceSession(str(PROJECT/model),sess_options=opt,providers=['CPUExecutionProvider'])
30
+ for t in meta['input_traces']:
31
+ inp={k:np.asarray(t[k],dtype=np.int32 if k=='timesteps' else np.float32).reshape(shape)
32
+ for k,shape in [('observations',(1,32,9)),('actions',(1,32,3)),('returns_to_go',(1,32,1)),('timesteps',(1,32))]}
33
+ out=sessions[model].run(['predicted_actions'],inp)[0]
34
+ selected=out[0,t['valid_length']-1]
35
+ error=float(np.max(np.abs(selected-np.asarray(t['selected_action'])))); maximum=max(maximum,error)
36
+ assert np.allclose(selected,t['selected_action'],atol=1e-4,rtol=1e-4), (file,t['timestep'],error)
37
+ case=dict(model=model, sha256=hashlib.sha256((PROJECT/model).read_bytes()).hexdigest(),
38
+ label=f"{name}/targets={meta['target_count']}/episode={t['episode']}/t={t['timestep']}",
39
+ valid_length=t['valid_length'],expected_output=out.reshape(-1).tolist(),
40
+ **{k:t[k] for k in ('observations','actions','returns_to_go','timesteps')})
41
+ cases.append(case)
42
+ assert len(cases)==96 and len(sessions)==4
43
+ save(TASK/'Verification/actual-input-parity.json',dict(cases=cases))
44
+ save(TASK/'Reports/onnxruntime-vs-recorded-unity.json',dict(status='passed',cases=96,models=4,
45
+ ort=ort.__version__,maximum_absolute_selected_error=maximum,scope='Actual recorded traces, no BC PyTorch weights.'))
46
+ jobs=[]
47
+ def job(id,phase,models,targets,episodes,seed,rtg,**extra):
48
+ d=dict(id=id,phase=phase,policy='PPO' if models==[PPO] else 'DT',models=models,
49
+ scene=f'Assets/Scenes/RevisionEvaluation/{id}.unity',output=str(TASK/'Results'/phase/id),
50
+ report=str(TASK/'Reports'/f'{id}.json'),episodes=episodes,targets=targets,seed=seed,
51
+ max_steps=1000,initial_rtg=rtg,timeout_seconds=43200,experiment_kind=phase,**extra)
52
+ save(TASK/'Jobs'/f'{id}.json',d);jobs.append(d)
53
+ job('bc-normal','bc-diagnostic',[BC],20,50,42,0)
54
+ job('bc-legacy-rtg','bc-diagnostic',[BC],20,50,42,0,bc_legacy_rtg=True)
55
+ for rtg in range(0,111,10):
56
+ job(f'rtg-{rtg:03}','rtg',[DT],20,50,2000,rtg,rtg_sensitivity=True)
57
+ job('rtg-bc-reference','rtg',[BC],20,50,2000,0)
58
+ for targets,rtg in [(10,35),(15,55),(20,70)]:
59
+ job(f'heldout-{targets}-ppo','heldout',[PPO],targets,200,1000,rtg)
60
+ job(f'heldout-{targets}-dt-bc','heldout',[DT,BC],targets,200,1000,rtg)
61
+ job('timing-dt-bc','timing',[DT,BC],20,20,3000,70,record_timing=True,time_scale_override=1.)
62
+ job('timing-ppo','timing',[PPO],20,20,3000,70,record_timing=True,time_scale_override=1.)
63
+ save(TASK/'Reports/job-manifest.json',jobs)
64
+ hashes=[]
65
+ for row in inventory:
66
+ hash=hashlib.sha256((PROJECT/row['path']).read_bytes()).hexdigest()
67
+ assert hash==row['sha256']
68
+ hashes.append(dict(path=row['path'],sha256=hash))
69
+ save(TASK/'Reports/models-before-additional.json',hashes)
70
+ print(dict(parity_cases=96,models=4,max_error=maximum,jobs=len(jobs),new_episodes=sum(j['episodes']*len(j['models']) for j in jobs)))
71
+
72
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/progress.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json,re
2
+ from pathlib import Path
3
+ TASK=Path(__file__).resolve().parents[1]
4
+ jobs=json.loads((TASK/'Reports/job-manifest.json').read_text(encoding='utf-8'))
5
+ done=[];active=[]
6
+ for job in jobs:
7
+ report=Path(job['report'])
8
+ if report.exists():
9
+ r=json.loads(report.read_text(encoding='utf-8-sig'))
10
+ if r['status']=='passed':done.append(job);continue
11
+ path=TASK/'Logs'/f'{job["id"]}.log'
12
+ with path.open('rb') as f:
13
+ f.seek(max(0,path.stat().st_size-150000));s=f.read().decode('utf-8',errors='replace')
14
+ matches=re.findall(r'Episode (\d+)/(\d+) Finished\. Reward: ([^\r\n]+)',s)
15
+ active.append(dict(job=job['id'],status=r['status'],recent_episode=matches[-1] if matches else None,
16
+ last_error=r.get('error','')))
17
+ print(json.dumps(dict(completed_jobs=len(done),completed_episodes=sum(j['episodes']*len(j['models']) for j in done),
18
+ total_jobs=len(jobs),planned_episodes=sum(j['episodes']*len(j['models']) for j in jobs),active=active),ensure_ascii=False))
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/report_additional.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json
2
+ from pathlib import Path
3
+ import numpy as np
4
+ from scipy import stats
5
+ import matplotlib
6
+ matplotlib.use('Agg')
7
+ import matplotlib.pyplot as plt
8
+ from analyze_statistics import mean_ci
9
+
10
+ TASK=Path(__file__).resolve().parents[1]
11
+ ROOT=TASK.parents[1]
12
+
13
+ def load(p):return json.loads(p.read_text(encoding='utf-8-sig'))
14
+ def save(p,x):p.write_text(json.dumps(x,indent=2),encoding='utf-8')
15
+ def ci(k,n):
16
+ c=stats.binomtest(k,n).proportion_ci(method='exact');return [100*c.low,100*c.high]
17
+ def main():
18
+ rows=[]
19
+ for p in sorted((TASK/'Results').rglob('*.json')):
20
+ d=load(p)
21
+ if 'all_wins' not in d:continue
22
+ m=d['evaluation'];n=d['episodes'];k=sum(d['all_wins'])
23
+ rows.append(dict(file=str(p.relative_to(TASK)),sha256=hashlib.sha256(p.read_bytes()).hexdigest(),
24
+ phase=m['experiment_kind'],model=m['model_name'],policy=m['policy_type'],targets=m['target_count'],
25
+ initial_rtg=m['initial_rtg'],diagnostic=m.get('diagnostic_only',False),episodes=n,successes=k,
26
+ success_percent=100*k/n,success_ci95_percent=ci(k,n),mean_steps=float(np.mean(d['all_steps'])),
27
+ mean_reward=float(np.mean(d['all_rewards'])),reward_ci95=mean_ci(d['all_rewards'],str(p)),
28
+ mean_smoothness=float(np.mean(d['all_smoothness'])),
29
+ mean_actual_accuracy=float(np.mean(d['all_actual_firing_accuracies'])),
30
+ mean_request_accuracy=float(np.mean(d['all_accuracies'])),
31
+ failures=[dict(seed=m['episode_seeds'][i],steps=d['all_steps'][i],
32
+ remaining_targets=m['target_count']-m['episode_shots_hit'][i],reward=d['all_rewards'][i])
33
+ for i,w in enumerate(d['all_wins']) if not w] ))
34
+ save(TASK/'Reports/additional-results-summary.json',rows)
35
+ rtg=sorted([r for r in rows if r['phase']=='rtg' and r['policy']=='DT'],key=lambda r:r['initial_rtg'])
36
+ bc=[r for r in rows if r['phase']=='rtg' and r['policy']=='BC']
37
+ if len(rtg)==12 and len(bc)==1:
38
+ b=bc[0];fig,axes=plt.subplots(1,2,figsize=(11,4.2),layout='constrained')
39
+ for ax,metric,interval,label in [(axes[0],'success_percent','success_ci95_percent','Success rate (%)'),
40
+ (axes[1],'mean_reward','reward_ci95','Mean cumulative reward')]:
41
+ x=np.array([r['initial_rtg'] for r in rtg]);y=np.array([r[metric] for r in rtg])
42
+ bounds=np.array([r[interval] for r in rtg])
43
+ ax.errorbar(x,y,yerr=np.vstack([y-bounds[:,0],bounds[:,1]-y]),fmt='o-',color='#1767ab',capsize=3,label='DT S100 E3')
44
+ ax.axhline(b[metric],color='#d87818',linestyle='--',label='BC100 E3 (always zero RTG)')
45
+ ax.axhspan(*b[interval],color='#d87818',alpha=.12)
46
+ ax.set(xlabel='Initial DT return-to-go',ylabel=label);ax.grid(alpha=.2);ax.legend(fontsize=8)
47
+ axes[0].set_ylim(-3,105)
48
+ fig.suptitle('20 targets | 50 paired episodes per setting | seeds 2000–2049\n95% marginal confidence intervals; fixed checkpoints')
49
+ fig.savefig(TASK/'Reports/rtg-sensitivity.png',dpi=180);plt.close(fig)
50
+ timing=[]
51
+ for p in (TASK/'Results/timing').rglob('*.json'):
52
+ d=load(p);m=d['evaluation']
53
+ assert m['timing_enabled']
54
+ if m['policy_type'] in ['DT','BC']:
55
+ assert len(m['control_loop_ms'])==len(m['inference_call_ms'])
56
+ assert np.all(np.asarray(m['control_loop_ms'])>=np.asarray(m['inference_call_ms']))
57
+ for key in ['inference_call_ms','control_loop_ms']:
58
+ a=np.asarray(m.get(key,[]),dtype=float)
59
+ if not len(a):continue
60
+ assert len(a)==sum(d['all_steps'])
61
+ assert len(a)>=2200,(p,len(a))
62
+ assert np.all(np.isfinite(a)) and np.all(a>0)
63
+ x=a[200:2200]
64
+ timing.append(dict(model=m['model_name'],metric=key,warmup_excluded=200,samples=2000,
65
+ available_actions=len(a),mean_ms=float(x.mean()),median_ms=float(np.median(x)),
66
+ p95_ms=float(np.quantile(x,.95,method='inverted_cdf')),scope=m['control_timing_scope']))
67
+ save(TASK/'Reports/control-timing-summary.json',timing)
68
+ lines=['# 추가 실험 결과','','이 문서는 완료된 추가 실행만 집계한다. 기존 2,850회와 합산하지 않는다. 온라인 Transformer 학습은 범위에서 제외했다.',
69
+ f"완료 결과: {len(rows)}개 파일, {sum(r['episodes'] for r in rows):,} 에피소드. 진단·RTG 민감도·새 환경 seed·timing은 각각 별도 조건이다.", '',
70
+ '[핵심 해석과 논문 수정 영향](INTERPRETATION_KO.md), [최종 보존·실행 검증](Reports/final-audit.json).', '',
71
+ '정상 DT는 표의 초기 RTG에서 시작해 매 step 보상을 차감한다. 정상 BC는 모든 step의 RTG 입력을 0으로 고정한다. PPO는 RTG 입력을 사용하지 않는다. BC legacy 조건만 초기 0 이후 보상을 차감하는 진단이다.', '',
72
+ '| 실험 | 표적 | 모델 | 초기 RTG | 성공/평가 | 성공률 정확 95% CI | 평균 종료 step | 평균 보상 |',
73
+ '|---|---:|---|---:|---:|---|---:|---:|']
74
+ for r in rows:
75
+ lo,hi=r['success_ci95_percent'];phase=r['phase']+(' (진단 전용)' if r['diagnostic'] else '')
76
+ rtg_text='해당 없음' if r['policy']=='PPO' else str(r['initial_rtg'])
77
+ lines.append(f"| {phase} | {r['targets']} | {r['model']} | {rtg_text} | {r['successes']}/{r['episodes']} | {lo:.2f}–{hi:.2f}% | {r['mean_steps']:.2f} | {r['mean_reward']:.4f} |")
78
+ lines+=['','## 새 환경 seed의 실패 사례','','개별 실패 seed·종료 step·남은 표적·보상은 Reports/additional-results-summary.json의 failures에 기록한다.','',
79
+ '| 표적 | 모델 | 실패 seed |','|---:|---|---|']
80
+ for r in rows:
81
+ if r['phase']=='heldout':
82
+ seeds=', '.join(str(f['seed']) for f in r['failures']) or '관측 없음'
83
+ lines.append(f"| {r['targets']} | {r['model']} | {seeds} |")
84
+ lines+=['','## 실제 제어 loop 측정','','첫 200 action을 제외한 다음 2,000 action. 에피소드 종료·파일 저장은 측정 구간 밖이다. PPO는 ML-Agents scheduling을 포함하며 DT와 호출 경계가 완전히 같지는 않다.','',
85
+ '| 모델 | 범위 | 평균 ms | 중앙값 ms | p95 ms |','|---|---|---:|---:|---:|']
86
+ for r in timing:lines.append(f"| {r['model']} | {r['metric']} | {r['mean_ms']:.3f} | {r['median_ms']:.3f} | {r['p95_ms']:.3f} |")
87
+ bench=TASK/'Reports/latency-benchmark.json'
88
+ if bench.exists() and load(bench)['status']=='passed':
89
+ lines+=['','## 모델 호출 단독 측정','','모델별 200회 warmup 후 2,000회. GPU 출력 readback 완료까지 포함한다. DT/BC GPU와 PPO CPU를 사용하며 모델 구조·장치 차이를 분리한 인과 비교는 아니다. 기존 사용자 Unity 프로세스는 유지했으므로 장치 독점 측정으로 해석하지 않는다.','',
90
+ '| 모델 | Backend | 평균 ms | 중앙값 ms | p95 ms |','|---|---|---:|---:|---:|']
91
+ for r in load(bench)['models']:lines.append(f"| {Path(r['model']).stem} | {r['backend']} | {r['mean_ms']:.3f} | {r['median_ms']:.3f} | {r['p95_ms']:.3f} |")
92
+ lines+=['','## 해석 범위','','고정된 가중치에서의 평가다. E1/E2/E3는 epoch별 체크포인트이며 독립 학습 seed 반복으로 간주하지 않는다. BC legacy RTG는 입력 불일치의 진단용이며 정상 BC 성능으로 보고하지 않는다. RTG curve는 행동 조건화 검사이며 BC보다 RTG가 필수적이라는 증명이 아니다.','',
93
+ '새 환경 seed는 이전 평가 42–91과 분리된 1000–1199이다. 환경 생성 분포는 같고, 원본 S 데이터와의 상태 비중복을 입증한 분포 외 평가로 해석하지 않는다.', '',
94
+ 'CI는 개별 구간이다. 연속 지표 비교의 paired permutation 가정과 Holm 보정 범위는 통계 보고서를 참조한다. 비유의 차이는 동등성 증명이 아니다. Editor timing에서 실제 배포 빌드의 렌더링 FPS를 추론하지 않는다.', '',
95
+ '독립 학습 seed 반복은 원본 S 데이터 trajectory_data_part_*.pkl의 위치 확인이 필요하여 미실행이다. 환경 seed 평가 1,800회로 이를 대체했다고 주장하지 않는다.', '',
96
+ '관련 기록: [실행 계획](PROTOCOL_KO.md), [작업 기록](WORK_LOG_KO.md), [기존 평가 통계](existing_STATISTICS_KO.md), [새 환경 seed 통계](heldout_STATISTICS_KO.md), [RTG 그림](Reports/rtg-sensitivity.png), [RTG 통계](Reports/rtg-statistics.json), [BC 원인 진단 통계](Reports/bc-diagnostic-effect.json).']
97
+ (TASK/'RESULTS_KO.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
98
+ print(dict(files=len(rows),episodes=sum(r['episodes'] for r in rows),rtg_conditions=len(rtg),control_timing_rows=len(timing)))
99
+
100
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/run_additional.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sequential, restartable runner. Each successful job is validated before continuing."""
2
+ import argparse
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ import subprocess
7
+ import sys
8
+ from datetime import datetime,timezone
9
+ import validate_additional
10
+
11
+ TASK=Path(__file__).resolve().parents[1]
12
+ ROOT=TASK.parents[1]
13
+ PHASES=['bc-diagnostic','rtg','heldout','timing']
14
+
15
+ def load(p):return json.loads(p.read_text(encoding='utf-8-sig'))
16
+ def save(p,d):p.write_text(json.dumps(d,indent=2),encoding='utf-8')
17
+ def utc():return datetime.now(timezone.utc).isoformat()
18
+
19
+ def invoke(mode,name,job=None):
20
+ command=['powershell.exe','-NoProfile','-File',str(TASK/'Tools/Run-Unity.ps1'),'-Mode',mode,'-RunName',name]
21
+ if job:command+=['-JobPath',str(TASK/'Jobs'/f'{job["id"]}.json')]
22
+ print(f'{utc()} Starting {name}',flush=True)
23
+ result=subprocess.run(command)
24
+ assert result.returncode==0, f'{name}: process exit {result.returncode}'
25
+ report=load(Path(job['report']) if job else TASK/'Reports'/f'{name}.json')
26
+ assert report['status']=='passed',report
27
+
28
+ def validate_job(job):
29
+ errors,warnings,rows=validate_additional.validate(job['output'],job['episodes'],len(job['models']),[job['targets']],
30
+ job.get('rtg_sensitivity',False),job.get('bc_legacy_rtg',False))
31
+ save(TASK/'Reports'/f'{job["id"]}-validation.json',dict(status='passed' if not errors else 'failed',errors=errors,warnings=warnings,files=len(rows)))
32
+ assert not errors,errors
33
+ return rows
34
+
35
+ def bc_diagnostic_summary():
36
+ def one(root):
37
+ files=list(root.rglob('*.json'));assert len(files)==1
38
+ return load(files[0])
39
+ a=one(TASK/'Results/bc-diagnostic/bc-normal');b=one(TASK/'Results/bc-diagnostic/bc-legacy-rtg')
40
+ oldfiles=list((ROOT/'Document/RevisionEvaluation_Local_20260910/Results/full/transformers/20').rglob('*E_3_DT_BC_100.json'))
41
+ assert len(oldfiles)==1
42
+ old=load(oldfiles[0])
43
+ checks={k:a[k]==old[k] for k in ['all_wins','all_steps','all_rewards','all_smoothness','all_accuracies','all_actual_firing_accuracies']}
44
+ assert all(checks.values()),f'Normal BC behavior changed: {checks}'
45
+ for key in ['episode_seeds','initial_states','initial_state_sha256']:
46
+ assert a['evaluation'][key]==b['evaluation'][key]==old['evaluation'][key]
47
+ summary=dict(status='passed',normal_reproduces_previous=checks,scope='Only RTG update differs; diagnostic is not a valid BC benchmark',
48
+ normal=dict(successes=sum(a['all_wins']),mean_steps=a['avg_steps'],reward=a['avg_reward']),
49
+ legacy=dict(successes=sum(b['all_wins']),mean_steps=b['avg_steps'],reward=b['avg_reward'],
50
+ max_abs_rtg=max(b['evaluation']['max_abs_rtg_input'])))
51
+ save(TASK/'Reports/bc-diagnostic-summary.json',summary)
52
+ print(summary,flush=True)
53
+
54
+ def run(phase):
55
+ jobs=[j for j in load(TASK/'Reports/job-manifest.json') if j['phase']==phase]
56
+ assert load(TASK/'Reports/additional-verification-retry1.json')['status']=='passed'
57
+ if PHASES.index(phase)>0:
58
+ assert load(TASK/'Reports'/f'{PHASES[PHASES.index(phase)-1]}-phase.json')['status']=='passed'
59
+ report_path=TASK/'Reports'/f'{phase}-phase.json'
60
+ report=dict(phase=phase,status='running',started_utc=utc(),jobs=[])
61
+ save(report_path,report)
62
+ try:
63
+ if phase=='timing' and not (TASK/'Reports/latency-benchmark.json').exists():
64
+ invoke('Latency','latency-benchmark')
65
+ for job in jobs:
66
+ path=Path(job['report'])
67
+ if not path.exists() or load(path)['status']!='passed':invoke('Run',job['id'],job)
68
+ validate_job(job)
69
+ report['jobs'].append(dict(id=job['id'],status='passed'))
70
+ save(report_path,report)
71
+ print(f'{utc()} Validated {job["id"]}',flush=True)
72
+ if phase=='bc-diagnostic':bc_diagnostic_summary()
73
+ else:
74
+ errors,warnings,rows=validate_additional.validate(TASK/'Results'/phase,
75
+ {'rtg':50,'heldout':200,'timing':20}[phase],{'rtg':2,'heldout':3,'timing':3}[phase],
76
+ [10,15,20] if phase=='heldout' else [20],phase=='rtg')
77
+ assert not errors,errors
78
+ report.update(result_files=len(rows),episodes=sum(r[2]['episodes'] for r in rows))
79
+ if phase=='heldout':
80
+ result=subprocess.run([sys.executable,str(TASK/'Tools/analyze_statistics.py'),str(TASK/'Results/heldout'),'heldout'])
81
+ assert result.returncode==0
82
+ report.update(status='passed',finished_utc=utc())
83
+ except Exception as e:
84
+ report.update(status='failed',error=str(e),finished_utc=utc())
85
+ save(report_path,report)
86
+ raise
87
+ save(report_path,report)
88
+
89
+ def main():
90
+ p=argparse.ArgumentParser();p.add_argument('phase',choices=PHASES+['all']);a=p.parse_args()
91
+ for phase in PHASES if a.phase=='all' else [a.phase]:run(phase)
92
+
93
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/validate_additional.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validate aligned-evaluation-v2.1 JSON logs. Python 3 standard library only; never edits logs."""
2
+ import argparse
3
+ from collections import defaultdict
4
+ import json
5
+ import math
6
+ from pathlib import Path
7
+
8
+ PROTOCOL = 'aligned-evaluation-v2.1'
9
+ REWARD = 'ShootingAgent-shaped-angle-v1'
10
+ SEQUENCE = 'dataset-shifted-action-right-pad32-v2'
11
+ ACCURACY = 'hits-per-fire-request-percent-v1'
12
+
13
+ def number(v):
14
+ return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v)
15
+
16
+ def close(a, b):
17
+ return number(a) and math.isclose(a, b, rel_tol=2e-5, abs_tol=2e-4)
18
+
19
+ def validate(root, expected_episodes=50, expected_models=None, expected_targets=None, allow_rtg_sensitivity=False, allow_bc_diagnostic=False):
20
+ errors, warnings, results = [], [], []
21
+ for path in sorted(Path(root).rglob('*.json')):
22
+ try:
23
+ data = json.loads(path.read_text(encoding='utf-8-sig'))
24
+ except (ValueError, OSError) as exc:
25
+ errors.append(f'{path}: unreadable JSON: {exc}'); continue
26
+ if not isinstance(data, dict) or 'all_wins' not in data:
27
+ continue
28
+ meta = data.get('evaluation')
29
+ if not isinstance(meta, dict) or meta.get('protocol_version') != PROTOCOL:
30
+ errors.append(f'{path}: missing/current protocol metadata; do not mix old logs'); continue
31
+ label = f'{meta.get("target_count")}/{meta.get("model_name")}'
32
+ def fail(message): errors.append(f'{label}: {message}')
33
+ n = data.get('episodes')
34
+ if type(n) is not int or n < 1:
35
+ fail('invalid episode count'); continue
36
+ if n != expected_episodes or meta.get('episodes_requested') != n:
37
+ fail(f'expected {expected_episodes} episodes, got {n}')
38
+ policy = meta.get('policy_type')
39
+ diagnostic = meta.get('diagnostic_only', False)
40
+ if diagnostic and not allow_bc_diagnostic: fail('Diagnostic BC results are not normal evaluation results')
41
+ if diagnostic and (policy != 'BC' or meta.get('bc_legacy_rtg') is not True or meta.get('initial_rtg') != 0):
42
+ fail('Invalid legacy-BC diagnostic metadata')
43
+ if policy not in ('PPO', 'DT', 'BC'): fail('unknown policy type')
44
+ if type(meta.get('target_count')) is not int or meta['target_count'] <= 0:
45
+ fail('invalid target count'); continue
46
+ if not isinstance(meta.get('model_name'), str) or not meta['model_name']:
47
+ fail('missing model name')
48
+ if meta.get('reward_protocol') != REWARD: fail('reward protocol differs from common training-based reward')
49
+ if meta.get('accuracy_protocol') != ACCURACY: fail('accuracy protocol mismatch')
50
+ expected_sequence = 'PPO' if policy == 'PPO' else SEQUENCE
51
+ if meta.get('sequence_protocol') != expected_sequence: fail('sequence protocol mismatch')
52
+ expected_history = 'PPO' if policy == 'PPO' else 'raw-continuous-and-actual-fire-v1'
53
+ if meta.get('action_history_protocol') != expected_history: fail('action history protocol mismatch')
54
+ if meta.get('rtg_sensitivity') and not allow_rtg_sensitivity:
55
+ fail('RTG sensitivity run mixed with main evaluation; use a separate folder or --allow-rtg-sensitivity')
56
+ for key in ['fixed_delta_time', 'rotation_speed', 'shoot_distance']:
57
+ if not number(meta.get(key)) or meta[key] <= 0: fail(f'invalid setting {key}')
58
+ if not number(meta.get('step_penalty')): fail('invalid step penalty')
59
+ if not isinstance(meta.get('unity_version'), str) or not meta['unity_version']: fail('missing Unity version')
60
+ if type(meta.get('queries_hit_triggers')) is not bool: fail('missing physics trigger setting')
61
+ if type(meta.get('enemy_layer_mask')) is not int: fail('missing enemy layer mask')
62
+ limit = meta.get('max_action_steps')
63
+ if type(limit) is not int or limit < 1: fail('invalid step limit'); limit = 0
64
+ elif policy in ('DT', 'BC') and limit > 4096: fail('timestep capacity exceeded')
65
+
66
+ numeric_arrays = ['all_wins', 'all_rewards', 'all_steps', 'all_smoothness',
67
+ 'all_accuracies', 'all_actual_firing_accuracies']
68
+ valid_arrays = True
69
+ for key in numeric_arrays:
70
+ values = data.get(key)
71
+ if not isinstance(values, list) or len(values) != n or not all(number(v) for v in values):
72
+ fail(f'{key} invalid/length mismatch'); valid_arrays = False
73
+ counts_valid = True
74
+ for key in ['episode_action_steps', 'episode_shots_fired', 'episode_shots_hit', 'episode_raycasts']:
75
+ values = meta.get(key)
76
+ if not isinstance(values, list) or len(values) != n or not all(type(v) is int and v >= 0 for v in values):
77
+ fail(f'{key} invalid/length mismatch'); counts_valid = False
78
+ for key in ['episode_seeds', 'initial_states', 'initial_state_sha256']:
79
+ if not isinstance(meta.get(key), list) or len(meta[key]) != n: fail(f'{key} length mismatch')
80
+ master = meta.get('master_seed')
81
+ if type(master) is not int or meta.get('episode_seeds') != list(range(master, master + n)):
82
+ fail('seed schedule mismatch')
83
+ states = meta.get('initial_states')
84
+ if isinstance(states, list):
85
+ for i, state in enumerate(states):
86
+ if not isinstance(state, dict) or state.get('episode') != i + 1 or type(master) is not int or state.get('seed') != master + i:
87
+ fail('initial_states episode/seed mismatch'); break
88
+ if not isinstance(state.get('targets'), list) or len(state['targets']) != meta['target_count']:
89
+ fail('initial_states target count mismatch'); break
90
+ hashes = meta.get('initial_state_sha256')
91
+ if isinstance(hashes, list) and any(not isinstance(v, str) or len(v) != 64 or any(c not in '0123456789abcdef' for c in v) for v in hashes):
92
+ fail('invalid initial state hash')
93
+ if valid_arrays:
94
+ wins, steps, rewards = (data[k] for k in ('all_wins', 'all_steps', 'all_rewards'))
95
+ if not all(type(v) is int and v in (0, 1) for v in wins): fail('non-binary wins')
96
+ if not close(data.get('win_rate'), 100 * sum(wins) / n): fail('win_rate inconsistent')
97
+ if meta.get('episode_action_steps') != steps: fail('action counts differ from all_steps')
98
+ if any(type(v) is not int or not 1 <= v <= limit for v in steps): fail('step limit violated')
99
+ if any(w == 0 and step != limit for w, step in zip(wins, steps)): fail('failed episode ended before timeout')
100
+ if any(not 0 <= v <= 100 for k in ['all_accuracies', 'all_actual_firing_accuracies'] for v in data[k]): fail('accuracy outside 0-100')
101
+ if any(v < 0 for v in data['all_smoothness']): fail('negative smoothness')
102
+ mean = sum(rewards) / n
103
+ if not close(data.get('avg_reward'), mean): fail('avg_reward inconsistent')
104
+ if not close(data.get('avg_steps'), sum(steps) / n): fail('avg_steps inconsistent')
105
+ if not close(data.get('reward_std'), math.sqrt(sum((v-mean)**2 for v in rewards) / n)): fail('reward_std inconsistent with population SD')
106
+ if not close(data.get('smoothness'), sum(a*b for a,b in zip(data['all_smoothness'],steps))/max(1,sum(steps))):
107
+ fail('smoothness summary inconsistent with step-weighted aggregation')
108
+ if counts_valid:
109
+ requests, hits, casts = (meta[k] for k in ['episode_shots_fired','episode_shots_hit','episode_raycasts'])
110
+ for i, (req, hit, cast, step) in enumerate(zip(requests,hits,casts,steps)):
111
+ if not 0 <= hit <= cast <= req <= step: fail(f'episode {i+1}: hit/raycast/request counts invalid')
112
+ if hit > meta['target_count'] or (wins[i] == 1) != (hit == meta['target_count']): fail(f'episode {i+1}: hits inconsistent with success')
113
+ if not close(data['all_accuracies'][i], 100*hit/req if req else 0): fail('per-episode request accuracy inconsistent')
114
+ if not close(data['all_actual_firing_accuracies'][i], 100*hit/cast if cast else 0): fail('per-episode actual firing accuracy inconsistent')
115
+ if not close(data.get('accuracy'), 100*sum(hits)/sum(requests) if sum(requests) else 0): fail('accuracy summary inconsistent')
116
+ if not close(data.get('actual_firing_accuracy'), 100*sum(hits)/sum(casts) if sum(casts) else 0): fail('actual firing accuracy summary inconsistent')
117
+ if policy in ('BC', 'DT'):
118
+ values = meta.get('max_abs_rtg_input')
119
+ terminal = meta.get('episode_terminal_rtg')
120
+ initial = meta.get('initial_rtg')
121
+ rtg_valid = (number(initial) and isinstance(values,list) and len(values)==n and all(number(v) and v>=0 for v in values)
122
+ and isinstance(terminal,list) and len(terminal)==n and all(number(v) for v in terminal))
123
+ if not rtg_valid: fail('RTG diagnostics invalid')
124
+ elif policy == 'BC' and not diagnostic:
125
+ if meta.get('bc_zero_rtg') is not True or initial != 0 or any(v != 0 for v in values + terminal): fail('BC RTG was not always zero')
126
+ else:
127
+ if meta.get('bc_zero_rtg') is not False: fail('DT incorrectly marked zero-RTG')
128
+ if valid_arrays and any(not math.isclose(t,initial-r,rel_tol=2e-4,abs_tol=0.02) for t,r in zip(terminal,data['all_rewards'])):
129
+ fail('terminal RTG does not include the full episode reward')
130
+ traces = meta.get('input_traces', [])
131
+ if not isinstance(traces, list):
132
+ fail('invalid input_traces'); traces = []
133
+ for trace in traces:
134
+ if not isinstance(trace,dict): fail('invalid input trace'); continue
135
+ t, valid = trace.get('timestep'), trace.get('valid_length')
136
+ if type(t) is not int or t < 0 or valid != min(t+1,32): fail('trace valid length mismatch'); continue
137
+ expected = list(range(max(0,t-31), t+1)) + [0]*(32-valid)
138
+ if trace.get('timesteps') != expected: fail('trace absolute timesteps mismatch')
139
+ for key,dim in [('observations',9),('actions',3),('returns_to_go',1)]:
140
+ a = trace.get(key)
141
+ if not isinstance(a,list) or len(a)!=32*dim or not all(number(v) for v in a): fail(f'trace {key} invalid'); continue
142
+ if any(v!=0 for v in a[valid*dim:]): fail('trace right padding is not zero')
143
+ if key=='actions' and any(v not in (0,1) for v in a[2:valid*3:3]): fail('trace fire history is not binary')
144
+ if key=='returns_to_go' and policy=='BC' and not diagnostic and any(v!=0 for v in a): fail('BC RTG trace drift')
145
+ results.append((label,path,data,meta))
146
+
147
+ if not results: errors.append('No evaluation results found')
148
+ by_target = defaultdict(list)
149
+ for row in results: by_target[row[3]['target_count']].append(row)
150
+ if expected_targets is not None and set(by_target) != set(expected_targets):
151
+ errors.append(f'Expected target conditions {sorted(expected_targets)}, found {sorted(by_target)}')
152
+ for target, rows in by_target.items():
153
+ identities = [(r[3].get('model_name'),r[3].get('initial_rtg'),r[3].get('policy_type')) for r in rows]
154
+ if len(set(identities)) != len(identities): errors.append(f'{target}: duplicate model/RTG runs; select one run per comparison')
155
+ models = {r[3].get('model_name') for r in rows}
156
+ if not allow_rtg_sensitivity and len(models) != len(rows): errors.append(f'{target}: more than one run for a model')
157
+ if expected_models is not None and len(models) != expected_models: errors.append(f'{target}: expected {expected_models} distinct models, got {len(models)}')
158
+ baseline = rows[0][3]
159
+ for label,path,data,meta in rows[1:]:
160
+ for key in ['episode_seeds','initial_states','initial_state_sha256','fixed_delta_time','max_action_steps',
161
+ 'rotation_speed','shoot_distance','step_penalty','enemy_layer_mask','unity_version','queries_hit_triggers',
162
+ 'reward_protocol','accuracy_protocol']:
163
+ if meta.get(key) != baseline.get(key): errors.append(f'{label}: paired condition mismatch: {key}')
164
+ if not allow_rtg_sensitivity:
165
+ rtgs = {r[3].get('initial_rtg') for r in rows if r[3].get('policy_type') == 'DT'}
166
+ if len(rtgs)>1: errors.append(f'{target}: DT models have different initial RTGs')
167
+ return errors,warnings,results
168
+
169
+ def main():
170
+ p=argparse.ArgumentParser(description=__doc__)
171
+ p.add_argument('root',type=Path)
172
+ p.add_argument('--expected-episodes',type=int,default=50)
173
+ p.add_argument('--expected-models',type=int)
174
+ p.add_argument('--expected-targets',type=int,nargs='+')
175
+ p.add_argument('--allow-rtg-sensitivity',action='store_true')
176
+ p.add_argument('--allow-bc-diagnostic',action='store_true')
177
+ args=p.parse_args()
178
+ errors,warnings,rows=validate(args.root,args.expected_episodes,args.expected_models,args.expected_targets,args.allow_rtg_sensitivity,args.allow_bc_diagnostic)
179
+ for m in errors: print('ERROR:',m)
180
+ for m in warnings: print('WARNING:',m)
181
+ print(f'Checked {len(rows)} result files; {len(errors)} errors.')
182
+ if not errors: print('Protocol/seed/initial-state/metric checks passed. Model validity and statistical assumptions require separate assessment.')
183
+ raise SystemExit(1 if errors else 0)
184
+
185
+ if __name__=='__main__': main()
Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/verify_pytorch_weights.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,importlib.util,json
2
+ from pathlib import Path
3
+ import numpy as np
4
+ import torch
5
+ TASK=Path(__file__).resolve().parents[1]
6
+ ROOT=TASK.parents[1]
7
+
8
+ def main():
9
+ source=ROOT/'Document/ShootingGame_IDE_Handoff/reference_originals/Python/model_dt.py'
10
+ spec=importlib.util.spec_from_file_location('verified_local_model_dt',source)
11
+ module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
12
+ torch.set_num_threads(2)
13
+ fixtures=json.loads((TASK/'Verification/actual-input-parity.json').read_text())['cases']
14
+ weights=json.loads((TASK/'Reports/reference-weight-manifest.json').read_text())
15
+ rows=[]
16
+ for w in weights:
17
+ path=TASK/w['path'];assert hashlib.sha256(path.read_bytes()).hexdigest()==w['sha256']
18
+ state=torch.load(path,map_location='cpu',weights_only=True)
19
+ model=module.DecisionTransformer(9,3,hidden=256,n_layers=4,n_heads=4,max_len=4096)
20
+ model.load_state_dict(state,strict=True);model.eval();count=0;maximum=0.
21
+ for c in fixtures:
22
+ if Path(c['model']).stem!=w['model']:continue
23
+ inputs=[torch.tensor(c[k],dtype=torch.long if k=='timesteps' else torch.float32).reshape(shape)
24
+ for k,shape in [('observations',(1,32,9)),('actions',(1,32,3)),('returns_to_go',(1,32,1)),('timesteps',(1,32))]]
25
+ with torch.no_grad():out=model(*inputs).numpy().reshape(-1)
26
+ expected=np.asarray(c['expected_output']);error=float(np.max(np.abs(out-expected)));maximum=max(maximum,error)
27
+ assert np.allclose(out,expected,atol=1e-4,rtol=1e-4),(c['label'],error)
28
+ count+=1
29
+ assert count==24
30
+ rows.append(dict(model=w['model'],cases=count,maximum_absolute_error=maximum,state_tensors=len(state),strict_load=True))
31
+ report=dict(status='passed',torch=torch.__version__,backend='CPU',models=rows,
32
+ source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),scope='96 actual inputs, PyTorch vs ONNX Runtime; independently checked against Unity GPU outputs.')
33
+ (TASK/'Reports/pytorch-additional-parity.json').write_text(json.dumps(report,indent=2))
34
+ print(json.dumps(report,indent=2))
35
+
36
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/Run-Unity.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [ValidateSet('Verify','Run')][string]$Mode = 'Verify',
3
+ [string]$JobPath = '',
4
+ [string]$RunName = 'unity-verification'
5
+ )
6
+ $ErrorActionPreference = 'Stop'
7
+ $taskRoot = Split-Path -Parent $PSScriptRoot
8
+ $projectRoot = Join-Path (Split-Path -Parent (Split-Path -Parent $taskRoot)) 'RevisionEval'
9
+ $editor = 'C:/Program Files/Unity/Hub/Editor/6000.0.62f1/Editor/Unity.exe'
10
+ $editorArguments = @('-batchmode','-force-d3d11','-projectPath',('"'+$projectRoot+'"'),
11
+ '-executeMethod',('RevisionEvaluationBatch.'+$Mode),'-logFile',('"'+$taskRoot+'/Logs/'+$RunName+'.log"'))
12
+ if ($Mode -eq 'Verify') {
13
+ $editorArguments += @('-evaluationFixtures',('"'+$taskRoot+'/Verification/E_1_DT_C_5_sequence_fixtures.json"'),
14
+ '-evaluationReport',('"'+$taskRoot+'/Reports/'+$RunName+'.json"'))
15
+ } else {
16
+ $job = Get-Content -LiteralPath $JobPath -Raw -Encoding UTF8 | ConvertFrom-Json
17
+ $editorArguments += @('-evaluationJob',('"'+(Resolve-Path -LiteralPath $JobPath).Path+'"'),
18
+ '-evaluationOutput',('"'+$job.output+'"'))
19
+ }
20
+ $editorArguments | ConvertTo-Json | Set-Content -LiteralPath "$taskRoot/Reports/$RunName-command.json" -Encoding UTF8
21
+ $taskProcess = Start-Process -FilePath $editor -ArgumentList $editorArguments -WindowStyle Hidden -PassThru
22
+ $taskProcess.Id | Set-Content -LiteralPath "$taskRoot/Reports/$RunName.pid"
23
+ Write-Output "$RunName Unity PID: $($taskProcess.Id)"
24
+ $taskProcess.WaitForExit()
25
+ $taskProcess.Refresh()
26
+ $taskProcess.ExitCode | Set-Content -LiteralPath "$taskRoot/Reports/$RunName-exit-code.txt"
27
+ Write-Output "$RunName exit code: $($taskProcess.ExitCode)"
28
+ exit $taskProcess.ExitCode
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/capture_changes.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Capture the final working-copy changes and model-preservation evidence."""
2
+ import difflib
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+
7
+ task = Path(__file__).resolve().parents[1]
8
+ source = task.parents[1]
9
+ project = source / "RevisionEval"
10
+ original = json.loads((task / "Reports/original-project-sha256.json").read_text(encoding="utf-8-sig"))
11
+
12
+
13
+ def sha(path):
14
+ with path.open("rb") as stream:
15
+ return hashlib.file_digest(stream, "sha256").hexdigest()
16
+
17
+
18
+ def read_source(path):
19
+ try:
20
+ return path.read_text(encoding="utf-8-sig")
21
+ except UnicodeDecodeError:
22
+ return path.read_text(encoding="cp949")
23
+
24
+
25
+ baseline = {item["path"].replace("\\", "/"): item for item in original}
26
+ changed, added, missing = [], [], []
27
+ diff = []
28
+ for name, item in baseline.items():
29
+ path = project / name
30
+ if not path.exists():
31
+ missing.append(name)
32
+ elif sha(path) != item["sha256"]:
33
+ changed.append(name)
34
+ if path.suffix in (".cs", ".json", ".asset", ".meta"):
35
+ diff.extend(difflib.unified_diff(read_source(source / name).splitlines(True),
36
+ read_source(path).splitlines(True),
37
+ fromfile="a/" + name, tofile="b/" + name))
38
+ for folder in ("Assets", "Packages", "ProjectSettings"):
39
+ for path in (project / folder).rglob("*"):
40
+ if path.is_file() and path.relative_to(project).as_posix() not in baseline:
41
+ added.append(path.relative_to(project).as_posix())
42
+ if path.suffix == ".cs":
43
+ diff.extend(difflib.unified_diff([], path.read_text(encoding="utf-8-sig").splitlines(True),
44
+ fromfile="/dev/null", tofile="b/" + path.relative_to(project).as_posix()))
45
+ data = {"working_project": str(project), "changed_existing": changed, "added": sorted(added),
46
+ "missing_existing": missing,
47
+ "existing_meta_changed": [name for name in changed if name.endswith(".meta")],
48
+ "model_files_changed": [name for name in changed if name.endswith((".onnx", ".pth", ".nn"))],
49
+ "training_script_unchanged": "Assets/Script/Shoodting/DroneAgent.cs" not in changed}
50
+ (task / "Reports/working-copy-changes.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
51
+ (task / "INTEGRATION_DIFF.patch").write_text("".join(diff), encoding="utf-8")
52
+ print(json.dumps({k: v for k, v in data.items() if k != "added"}, ensure_ascii=False))
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/finalize_report.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Check the complete model roster and consolidate existing validation evidence."""
2
+ from collections import Counter
3
+ from datetime import datetime
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+
8
+ task = Path(__file__).resolve().parents[1]
9
+ project = task.parents[1] / "RevisionEval"
10
+
11
+
12
+ def load(relative):
13
+ return json.loads((task / relative).read_text(encoding="utf-8-sig"))
14
+
15
+
16
+ phases = {name: load(f"Reports/{name}-phase.json") for name in ("smoke", "repeat", "full")}
17
+ assert all(value["status"] == "passed" for value in phases.values())
18
+ inventory = load("Reports/model-inventory.json")
19
+ roster = {Path(model["path"]).stem for model in inventory}
20
+ assert len(roster) == 19
21
+ results = [json.loads(path.read_text()) for path in (task / "Results/full").rglob("*.json")]
22
+ assert len(results) == 57 and sum(result["episodes"] for result in results) == 2850
23
+ conditions = []
24
+ for targets in (10, 15, 20):
25
+ subset = [r for r in results if r["evaluation"]["target_count"] == targets]
26
+ assert {r["evaluation"]["model_name"] for r in subset} == roster
27
+ assert Counter(r["evaluation"]["policy_type"] for r in subset) == {"PPO": 1, "DT": 15, "BC": 3}
28
+ conditions.append({"targets": targets, "models": len(subset), "episodes": sum(r["episodes"] for r in subset)})
29
+ preservation = load("Reports/original-preservation.json")
30
+ changes = load("Reports/working-copy-changes.json")
31
+ assert not preservation["changed_or_missing"]
32
+ assert not changes["model_files_changed"] and not changes["existing_meta_changed"] and changes["training_script_unchanged"]
33
+ source_hashes = {name: hashlib.sha256((project / name).read_bytes()).hexdigest()
34
+ for name in changes["changed_existing"] + changes["added"] if name.endswith(".cs")}
35
+ parity = load("Verification_Local/model_verification.json")
36
+ assert len(parity["tests"]) == 18 and all(case["passed"] for case in parity["tests"])
37
+ unit_log = (task / "Logs/python-validator-tests.log").read_text()
38
+ assert "Ran 19 tests" in unit_log and "\nOK\n" in unit_log
39
+ full = phases["full"]
40
+ summary = {"status": "passed", "protocol": "aligned-evaluation-v2.1",
41
+ "working_project": str(project), "result_files": 57, "episodes": 2850,
42
+ "conditions": conditions, "seed_first": 42, "seed_last": 91,
43
+ "started_utc": full["started_utc"], "finished_utc": full["finished_utc"],
44
+ "full_wall_seconds": (datetime.fromisoformat(full["finished_utc"]) - datetime.fromisoformat(full["started_utc"])).total_seconds(),
45
+ "python_validator_tests_passed": 19,
46
+ "python_model_parity_cases_passed": 18,
47
+ "python_model_parity_max_error": max(case["max_abs_error"] for case in parity["tests"]),
48
+ "unity_verification": load("Reports/unity-verification.json"),
49
+ "all_transformer_contracts": load("Reports/all-transformer-contracts.json"),
50
+ "reproducibility": load("Reports/reproducibility.json"),
51
+ "original_preservation": preservation, "source_files_sha256": source_hashes,
52
+ "limits": ["PyTorch/Unity parity covers E_1_DT_C_5 only.",
53
+ "PPO retains stochastic inference: initial states repeat, steps/rewards need not.",
54
+ "PPO Default uses CPU; DT/BC use GPUCompute. Wall time is not a latency comparison.",
55
+ "No manuscript edits or inferential statistics were performed."]}
56
+ (task / "Reports/local-validation-summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
57
+ print("Final roster verified: 57 files, 2850 episodes, 19 exact models per target condition; original files preserved.")
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/prepare_jobs.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Record existing model identities and prepare explicit, reproducible Unity batch jobs."""
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import onnx
7
+
8
+ TASK = Path(__file__).resolve().parents[1]
9
+ SOURCE = TASK.parents[1]
10
+ PROJECT = SOURCE / "RevisionEval"
11
+ HANDOFF = SOURCE / "Document/ShootingGame_IDE_Handoff"
12
+
13
+
14
+ def write(path, value):
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
17
+
18
+
19
+ def sha(path):
20
+ with path.open("rb") as stream:
21
+ return hashlib.file_digest(stream, "sha256").hexdigest()
22
+
23
+
24
+ def main():
25
+ expected = json.loads((HANDOFF / "FILES_SHA256.json").read_text(encoding="utf-8"))
26
+ failures = [name for name, digest in expected.items() if sha(HANDOFF / name) != digest]
27
+ write(TASK / "Reports/handoff-integrity.json", {"checked": len(expected), "mismatches": failures})
28
+ if failures:
29
+ raise RuntimeError(f"Handoff integrity failed: {failures}")
30
+
31
+ ppo = "Assets/Model/V12/V12 PPO.onnx"
32
+ dt = [f"Assets/Model/FinalModel/E_{exp}_DT_{kind}.onnx"
33
+ for exp in (1, 2, 3) for kind in ("C_5", "C_10", "S_100", "SC_5", "SC_10")]
34
+ bc = [f"Assets/Model/FinalModel/BCModels/E_{exp}_DT_BC_100.onnx" for exp in (1, 2, 3)]
35
+ inventory = []
36
+ for name in [ppo, *dt, *bc]:
37
+ path = PROJECT / name
38
+ model = onnx.load(path, load_external_data=False)
39
+ inputs = [{"name": v.name, "dtype": v.type.tensor_type.elem_type,
40
+ "shape": [d.dim_value if d.HasField("dim_value") else d.dim_param
41
+ for d in v.type.tensor_type.shape.dim]} for v in model.graph.input]
42
+ outputs = [{"name": v.name, "dtype": v.type.tensor_type.elem_type,
43
+ "shape": [d.dim_value if d.HasField("dim_value") else d.dim_param
44
+ for d in v.type.tensor_type.shape.dim]} for v in model.graph.output]
45
+ if name != ppo:
46
+ contract = {v["name"]: (v["dtype"], len(v["shape"])) for v in inputs}
47
+ assert contract == {"observations": (1, 3), "actions": (1, 3),
48
+ "returns_to_go": (1, 3), "timesteps": (6, 2)}, (name, inputs)
49
+ assert [v["name"] for v in outputs] == ["predicted_actions"], (name, outputs)
50
+ inventory.append({"path": name, "sha256": sha(path), "bytes": path.stat().st_size,
51
+ "inputs": inputs, "outputs": outputs})
52
+ write(TASK / "Reports/model-inventory.json", inventory)
53
+
54
+ def job(phase, name, models, targets=10, episodes=2):
55
+ identity = f"{phase}-{name}-{targets}"
56
+ value = {"id": identity, "policy": "PPO" if name == "ppo" else "DT",
57
+ "scene": f"Assets/Scenes/RevisionEvaluation/{identity}.unity",
58
+ "output": str(TASK / "Results" / phase / name / str(targets)),
59
+ "report": str(TASK / "Reports" / f"{identity}.json"),
60
+ "models": models, "episodes": episodes, "targets": targets, "seed": 42,
61
+ "max_steps": 1000, "initial_rtg": {10: 35, 15: 55, 20: 70}[targets],
62
+ "timeout_seconds": 43200 if episodes == 50 else 1800}
63
+ write(TASK / "Jobs" / f"{identity}.json", value)
64
+
65
+ for phase in ("smoke", "repeat"):
66
+ job(phase, "ppo", [ppo])
67
+ job(phase, "dt", [dt[0]])
68
+ job(phase, "bc", [bc[0]])
69
+ for targets in (10, 15, 20):
70
+ job("full", "ppo", [ppo], targets, 50)
71
+ job("full", "transformers", [*dt, *bc], targets, 50)
72
+ print(f"Verified {len(expected)} handoff files; recorded 19 model hashes/contracts; prepared 12 jobs.")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/progress.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Read-only progress snapshot for the long evaluation phase."""
2
+ import json
3
+ from pathlib import Path
4
+ import re
5
+ from datetime import datetime, timezone
6
+
7
+ task = Path(__file__).resolve().parents[1]
8
+ phase_path = task / "Reports/full-phase.json"
9
+ if phase_path.exists():
10
+ phase = json.loads(phase_path.read_text())
11
+ files = list((task / "Results/full").rglob("*.json"))
12
+ completed = {item["id"] for item in phase["jobs"] if item["passed"]}
13
+ jobs = [f"full-{kind}-{targets}" for targets in (10, 15, 20) for kind in ("ppo", "transformers")]
14
+ current = next((job for job in jobs if job not in completed), None)
15
+ log_path = task / "Logs" / f"{current}.log"
16
+ details = {}
17
+ if log_path.exists():
18
+ text = log_path.read_text(encoding="utf-8", errors="replace")
19
+ models = re.findall(r"\[Evaluation\] ([^\r\n]+): (?:DT, return-conditioned|BC, RTG always zero)", text)
20
+ episodes = re.findall(r"Episode (\d+)/50 Finished", text)
21
+ details = {"model": models[-1] if models else "PPO/startup",
22
+ "finished_episodes_in_job": len(episodes),
23
+ "last_episode": int(episodes[-1]) if episodes else 0,
24
+ "errors": re.findall(r"(?:\w+Exception:|error CS\d+|\[Evaluation\] Aborted)[^\r\n]*", text)[-3:]}
25
+ elapsed = (datetime.now(timezone.utc) - datetime.fromisoformat(phase["started_utc"])).total_seconds()
26
+ print(json.dumps({"status": phase["status"], "result_files": len(files), "expected_files": 57,
27
+ "completed_episodes_in_saved_files": 50 * len(files), "current_job": current,
28
+ "elapsed_minutes": round(elapsed / 60, 1), **details}, ensure_ascii=False))
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/run_phase.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run explicit Unity jobs sequentially; retain all logs and fail closed on errors."""
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime, timezone
8
+
9
+ TASK = Path(__file__).resolve().parents[1]
10
+ SOURCE = TASK.parents[1]
11
+ VALIDATOR = SOURCE / "Document/ShootingGame_IDE_Handoff/evaluation_code/Tools/validate_evaluation.py"
12
+
13
+
14
+ def load(path):
15
+ return json.loads(path.read_text(encoding="utf-8-sig"))
16
+
17
+
18
+ def save(path, value):
19
+ path.write_text(json.dumps(value, indent=2, ensure_ascii=False), encoding="utf-8")
20
+
21
+
22
+ def validate(phase, episodes, models, targets):
23
+ command = [sys.executable, str(VALIDATOR), str(TASK / "Results" / phase),
24
+ "--expected-episodes", str(episodes), "--expected-models", str(models),
25
+ "--expected-targets", *map(str, targets)]
26
+ result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace")
27
+ (TASK / "Logs" / f"{phase}-validation.log").write_text(result.stdout + result.stderr, encoding="utf-8")
28
+ print(result.stdout + result.stderr, flush=True)
29
+ return result.returncode == 0
30
+
31
+
32
+ def repeat_check():
33
+ def collect(phase):
34
+ return {d["evaluation"]["model_name"]: d for p in (TASK / "Results" / phase).rglob("*.json")
35
+ if "evaluation" in (d := load(p))}
36
+ first, second = collect("smoke"), collect("repeat")
37
+ assert len(first) == len(second) == 3 and first.keys() == second.keys()
38
+ rows = []
39
+ for model, a in first.items():
40
+ b = second[model]
41
+ state_match = all(a["evaluation"][k] == b["evaluation"][k]
42
+ for k in ("initial_states", "initial_state_sha256", "episode_seeds"))
43
+ row = {"model": model, "policy": a["evaluation"]["policy_type"], "initial_state_match": state_match,
44
+ "wins_match": a["all_wins"] == b["all_wins"], "steps_match": a["all_steps"] == b["all_steps"],
45
+ "rewards_match": a["all_rewards"] == b["all_rewards"],
46
+ "first_steps": a["all_steps"], "repeat_steps": b["all_steps"]}
47
+ rows.append(row)
48
+ passed = all(r["initial_state_match"] for r in rows)
49
+ save(TASK / "Reports/reproducibility.json", {"initial_states_passed": passed, "models": rows})
50
+ return passed
51
+
52
+
53
+ def main():
54
+ parser = argparse.ArgumentParser()
55
+ parser.add_argument("phase", choices=["smoke", "repeat", "full"])
56
+ args = parser.parse_args()
57
+ phase = args.phase
58
+ assert load(TASK / "Reports/unity-verification.json")["status"] == "passed", "Unity verification is required."
59
+ if phase == "repeat":
60
+ assert load(TASK / "Reports/smoke-phase.json")["status"] == "passed"
61
+ if phase == "full":
62
+ assert load(TASK / "Reports/repeat-phase.json")["status"] == "passed"
63
+ assert load(TASK / "Reports/reproducibility.json")["initial_states_passed"]
64
+ names = ([f"{phase}-{kind}-10" for kind in ("ppo", "dt", "bc")] if phase != "full" else
65
+ [f"full-{kind}-{count}" for count in (10, 15, 20) for kind in ("ppo", "transformers")])
66
+ summary = {"phase": phase, "status": "running", "started_utc": datetime.now(timezone.utc).isoformat(), "jobs": []}
67
+ report_path = TASK / "Reports" / f"{phase}-phase.json"
68
+ save(report_path, summary)
69
+ for name in names:
70
+ command = ["powershell.exe", "-NoProfile", "-File", str(TASK / "Tools/Run-Unity.ps1"),
71
+ "-Mode", "Run", "-JobPath", str(TASK / "Jobs" / f"{name}.json"), "-RunName", name]
72
+ print(f"Starting {name}", flush=True)
73
+ result = subprocess.run(command)
74
+ job_report = TASK / "Reports" / f"{name}.json"
75
+ passed = result.returncode == 0 and job_report.exists() and load(job_report)["status"] == "passed"
76
+ summary["jobs"].append({"id": name, "process_exit": result.returncode, "passed": passed})
77
+ save(report_path, summary)
78
+ if not passed:
79
+ summary["status"] = "failed"
80
+ save(report_path, summary)
81
+ raise SystemExit(1)
82
+ passed = validate(phase, 50 if phase == "full" else 2, 19 if phase == "full" else 3,
83
+ [10, 15, 20] if phase == "full" else [10])
84
+ if phase == "repeat" and passed:
85
+ passed = repeat_check()
86
+ summary["status"] = "passed" if passed else "failed"
87
+ summary["finished_utc"] = datetime.now(timezone.utc).isoformat()
88
+ save(report_path, summary)
89
+ raise SystemExit(0 if passed else 1)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/summarize_results.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Report evaluation artifacts without editing the manuscript or combining old logs."""
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+ import statistics
6
+
7
+ TASK = Path(__file__).resolve().parents[1]
8
+ SOURCE = TASK.parents[1]
9
+
10
+
11
+ def digest(path):
12
+ with path.open("rb") as stream:
13
+ return hashlib.file_digest(stream, "sha256").hexdigest()
14
+
15
+
16
+ def main():
17
+ result = {}
18
+ for phase in ("smoke", "repeat", "full"):
19
+ rows = []
20
+ for path in (TASK / "Results" / phase).rglob("*.json"):
21
+ data = json.loads(path.read_text(encoding="utf-8"))
22
+ if "evaluation" not in data:
23
+ continue
24
+ meta = data["evaluation"]
25
+ rows.append({"targets": meta["target_count"], "model": meta["model_name"], "policy": meta["policy_type"],
26
+ "episodes": data["episodes"], "successes": sum(data["all_wins"]),
27
+ "success_percent": data["win_rate"], "mean_steps": statistics.mean(data["all_steps"]),
28
+ "mean_reward": statistics.mean(data["all_rewards"]),
29
+ "sample_sd_reward": statistics.stdev(data["all_rewards"]) if data["episodes"] > 1 else None,
30
+ "mean_episode_request_accuracy": statistics.mean(data["all_accuracies"]),
31
+ "mean_episode_actual_accuracy": statistics.mean(data["all_actual_firing_accuracies"]),
32
+ "result": str(path.relative_to(TASK)), "sha256": digest(path)})
33
+ result[phase] = sorted(rows, key=lambda row: (row["targets"], row["model"]))
34
+ (TASK / "Reports/results-summary.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
35
+ lines = ["# 로컬 Unity 평가 결과", "", "과거 결과 및 원고 수치는 포함하지 않는다. 각 단계의 통과 여부는 Reports/*-phase.json을 확인한다.", ""]
36
+ for phase, rows in result.items():
37
+ lines += [f"## {phase}", "", f"완료 결과 파일 {len(rows)}개, 에피소드 {sum(row['episodes'] for row in rows)}개.", "",
38
+ "| 표적 | 모델 | 성공/평가 | 평균 action 수 | 평균 보상 | 보상 표본 SD |",
39
+ "|---:|---|---:|---:|---:|---:|"]
40
+ for row in rows:
41
+ lines.append(f"| {row['targets']} | {row['model']} | {row['successes']}/{row['episodes']} | "
42
+ f"{row['mean_steps']:.2f} | {row['mean_reward']:.4f} | {row['sample_sd_reward']:.4f} |")
43
+ lines.append("")
44
+ lines += ["성공률의 통계적 비교, 다중비교 보정, 신뢰구간은 이 요약에서 수행하지 않았다.",
45
+ "2회 확인 실행은 성능 결론용 표본이 아니다. 초기 확인·반복 확인·본 평가를 합산하지 않는다."]
46
+ (TASK / "RESULTS_KO.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
47
+
48
+ original = json.loads((TASK / "Reports/original-project-sha256.json").read_text(encoding="utf-8-sig"))
49
+ changed = [item["path"] for item in original if not (SOURCE / item["path"]).exists()
50
+ or digest(SOURCE / item["path"]) != item["sha256"]]
51
+ (TASK / "Reports/original-preservation.json").write_text(
52
+ json.dumps({"checked_files": len(original), "changed_or_missing": changed}, indent=2), encoding="utf-8")
53
+ print({phase: {"files": len(rows), "episodes": sum(r["episodes"] for r in rows)} for phase, rows in result.items()})
54
+ print("Original file changes:", changed)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tests/test_validate_evaluation.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import importlib.util
3
+ import json
4
+ from pathlib import Path
5
+ import tempfile
6
+ import unittest
7
+ spec=importlib.util.spec_from_file_location('validator',Path(__file__).parents[1]/'Tools/validate_evaluation.py')
8
+ v=importlib.util.module_from_spec(spec);spec.loader.exec_module(v)
9
+
10
+ def sample(model='E1_BC_100'):
11
+ return dict(episodes=2,win_rate=50,avg_reward=3.5,reward_std=1.5,avg_steps=525,
12
+ accuracy=14,smoothness=205/1050,actual_firing_accuracy=100,
13
+ all_wins=[1,0],all_steps=[50,1000],all_rewards=[5,2],all_smoothness=[0.1,0.2],
14
+ all_accuracies=[20,8],all_actual_firing_accuracies=[100,100],
15
+ evaluation=dict(protocol_version=v.PROTOCOL,model_name=model,target_count=10,episodes_requested=2,
16
+ master_seed=42,episode_seeds=[42,43],initial_states=[dict(episode=i+1,seed=42+i,targets=[{}]*10) for i in range(2)],
17
+ initial_state_sha256=['a'*64,'b'*64],episode_action_steps=[50,1000],episode_shots_fired=[50,50],
18
+ episode_shots_hit=[10,4],episode_raycasts=[10,4],max_action_steps=1000,policy_type='BC',bc_zero_rtg=True,
19
+ initial_rtg=0,max_abs_rtg_input=[0,0],episode_terminal_rtg=[0,0],input_traces=[],
20
+ reward_protocol=v.REWARD,sequence_protocol=v.SEQUENCE,accuracy_protocol=v.ACCURACY,
21
+ action_history_protocol='raw-continuous-and-actual-fire-v1',fixed_delta_time=0.02,rotation_speed=100,
22
+ shoot_distance=50,step_penalty=-0.001,unity_version='test-version',enemy_layer_mask=64,queries_hit_triggers=True))
23
+
24
+ class LogChecks(unittest.TestCase):
25
+ def run_case(self,data,**kwargs):
26
+ with tempfile.TemporaryDirectory() as tmp:
27
+ for i,d in enumerate(data):Path(tmp,f'{i}.json').write_text(json.dumps(d))
28
+ return v.validate(tmp,2,**kwargs)
29
+ def check_change(self,change,keyword):
30
+ a=sample();change(a)
31
+ errors=self.run_case([a])[0]
32
+ self.assertTrue(any(keyword in e for e in errors),errors)
33
+ def test_accepts_matching_episodes(self):
34
+ self.assertEqual(self.run_case([sample(),sample('E2_BC_100')])[0],[])
35
+ def test_seed_schedule(self):
36
+ self.check_change(lambda a:a['evaluation'].update(episode_seeds=[42,42]),'seed schedule')
37
+ def test_environment_mismatch(self):
38
+ a=sample('E2_BC_100');a['evaluation']['initial_states'][1]['targets'][0]={'position':[1,2,3]}
39
+ self.assertTrue(any('initial_states' in e for e in self.run_case([sample(),a])[0]))
40
+ def test_bc_input_drift(self):
41
+ self.check_change(lambda a:a['evaluation'].update(max_abs_rtg_input=[0,0.001]),'BC RTG')
42
+ def test_bc_terminal_drift(self):
43
+ self.check_change(lambda a:a['evaluation'].update(episode_terminal_rtg=[0,-2]),'BC RTG')
44
+ def test_timeout_overrun(self):
45
+ self.check_change(lambda a:a.update(all_steps=[50,1001]),'step limit')
46
+ def test_early_failed_episode(self):
47
+ self.check_change(lambda a:a.update(all_steps=[50,999]),'before timeout')
48
+ def test_old_logs(self):
49
+ self.check_change(lambda a:a.pop('evaluation'),'protocol')
50
+ def test_duplicates(self):
51
+ self.assertTrue(any('duplicate' in e for e in self.run_case([sample(),sample()])[0]))
52
+ def test_reward_mismatch_is_error(self):
53
+ self.check_change(lambda a:a['evaluation'].update(reward_protocol='old'),'reward protocol')
54
+ def test_missing_target_condition(self):
55
+ self.assertTrue(any('target conditions' in e for e in self.run_case([sample()],expected_targets=[10,15,20])[0]))
56
+ def test_wrong_accuracy_denominator(self):
57
+ self.check_change(lambda a:a.update(all_accuracies=[100,100]),'request accuracy')
58
+ def test_impossible_raycast_count(self):
59
+ self.check_change(lambda a:a['evaluation'].update(episode_raycasts=[5,4]),'counts invalid')
60
+ def test_rtg_includes_terminal_bonus(self):
61
+ a=sample();a['evaluation'].update(policy_type='DT',initial_rtg=35,bc_zero_rtg=False,max_abs_rtg_input=[35,35],episode_terminal_rtg=[30,33])
62
+ self.assertEqual(self.run_case([a])[0],[])
63
+ a['evaluation']['episode_terminal_rtg'][0]=32
64
+ self.assertTrue(any('terminal RTG' in e for e in self.run_case([a])[0]))
65
+ def test_summary_reward(self):
66
+ self.check_change(lambda a:a.update(avg_reward=99),'avg_reward')
67
+ def test_nan_input(self):
68
+ self.check_change(lambda a:a.update(all_rewards=[float('nan'),2]),'all_rewards')
69
+ def test_null_metadata_array(self):
70
+ self.check_change(lambda a:a['evaluation'].update(episode_shots_hit=None),'episode_shots_hit')
71
+ def test_sensitivity_separation(self):
72
+ self.check_change(lambda a:a['evaluation'].update(rtg_sensitivity=True),'sensitivity')
73
+ def test_trace_timestep_rollover(self):
74
+ a=sample();a['evaluation']['input_traces']=[dict(timestep=32,valid_length=32,timesteps=list(range(32)),
75
+ observations=[0]*288,actions=[0]*96,returns_to_go=[0]*32)]
76
+ self.assertTrue(any('absolute timesteps' in e for e in self.run_case([a])[0]))
77
+
78
+ if __name__=='__main__':unittest.main()
Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tools/validate_evaluation.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validate aligned-evaluation-v2.1 JSON logs. Python 3 standard library only; never edits logs."""
2
+ import argparse
3
+ from collections import defaultdict
4
+ import json
5
+ import math
6
+ from pathlib import Path
7
+
8
+ PROTOCOL = 'aligned-evaluation-v2.1'
9
+ REWARD = 'ShootingAgent-shaped-angle-v1'
10
+ SEQUENCE = 'dataset-shifted-action-right-pad32-v2'
11
+ ACCURACY = 'hits-per-fire-request-percent-v1'
12
+
13
+ def number(v):
14
+ return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v)
15
+
16
+ def close(a, b):
17
+ return number(a) and math.isclose(a, b, rel_tol=2e-5, abs_tol=2e-4)
18
+
19
+ def validate(root, expected_episodes=50, expected_models=None, expected_targets=None, allow_rtg_sensitivity=False):
20
+ errors, warnings, results = [], [], []
21
+ for path in sorted(Path(root).rglob('*.json')):
22
+ try:
23
+ data = json.loads(path.read_text(encoding='utf-8-sig'))
24
+ except (ValueError, OSError) as exc:
25
+ errors.append(f'{path}: unreadable JSON: {exc}'); continue
26
+ if not isinstance(data, dict) or 'all_wins' not in data:
27
+ continue
28
+ meta = data.get('evaluation')
29
+ if not isinstance(meta, dict) or meta.get('protocol_version') != PROTOCOL:
30
+ errors.append(f'{path}: missing/current protocol metadata; do not mix old logs'); continue
31
+ label = f'{meta.get("target_count")}/{meta.get("model_name")}'
32
+ def fail(message): errors.append(f'{label}: {message}')
33
+ n = data.get('episodes')
34
+ if type(n) is not int or n < 1:
35
+ fail('invalid episode count'); continue
36
+ if n != expected_episodes or meta.get('episodes_requested') != n:
37
+ fail(f'expected {expected_episodes} episodes, got {n}')
38
+ policy = meta.get('policy_type')
39
+ if policy not in ('PPO', 'DT', 'BC'): fail('unknown policy type')
40
+ if type(meta.get('target_count')) is not int or meta['target_count'] <= 0:
41
+ fail('invalid target count'); continue
42
+ if not isinstance(meta.get('model_name'), str) or not meta['model_name']:
43
+ fail('missing model name')
44
+ if meta.get('reward_protocol') != REWARD: fail('reward protocol differs from common training-based reward')
45
+ if meta.get('accuracy_protocol') != ACCURACY: fail('accuracy protocol mismatch')
46
+ expected_sequence = 'PPO' if policy == 'PPO' else SEQUENCE
47
+ if meta.get('sequence_protocol') != expected_sequence: fail('sequence protocol mismatch')
48
+ expected_history = 'PPO' if policy == 'PPO' else 'raw-continuous-and-actual-fire-v1'
49
+ if meta.get('action_history_protocol') != expected_history: fail('action history protocol mismatch')
50
+ if meta.get('rtg_sensitivity') and not allow_rtg_sensitivity:
51
+ fail('RTG sensitivity run mixed with main evaluation; use a separate folder or --allow-rtg-sensitivity')
52
+ for key in ['fixed_delta_time', 'rotation_speed', 'shoot_distance']:
53
+ if not number(meta.get(key)) or meta[key] <= 0: fail(f'invalid setting {key}')
54
+ if not number(meta.get('step_penalty')): fail('invalid step penalty')
55
+ if not isinstance(meta.get('unity_version'), str) or not meta['unity_version']: fail('missing Unity version')
56
+ if type(meta.get('queries_hit_triggers')) is not bool: fail('missing physics trigger setting')
57
+ if type(meta.get('enemy_layer_mask')) is not int: fail('missing enemy layer mask')
58
+ limit = meta.get('max_action_steps')
59
+ if type(limit) is not int or limit < 1: fail('invalid step limit'); limit = 0
60
+ elif policy in ('DT', 'BC') and limit > 4096: fail('timestep capacity exceeded')
61
+
62
+ numeric_arrays = ['all_wins', 'all_rewards', 'all_steps', 'all_smoothness',
63
+ 'all_accuracies', 'all_actual_firing_accuracies']
64
+ valid_arrays = True
65
+ for key in numeric_arrays:
66
+ values = data.get(key)
67
+ if not isinstance(values, list) or len(values) != n or not all(number(v) for v in values):
68
+ fail(f'{key} invalid/length mismatch'); valid_arrays = False
69
+ counts_valid = True
70
+ for key in ['episode_action_steps', 'episode_shots_fired', 'episode_shots_hit', 'episode_raycasts']:
71
+ values = meta.get(key)
72
+ if not isinstance(values, list) or len(values) != n or not all(type(v) is int and v >= 0 for v in values):
73
+ fail(f'{key} invalid/length mismatch'); counts_valid = False
74
+ for key in ['episode_seeds', 'initial_states', 'initial_state_sha256']:
75
+ if not isinstance(meta.get(key), list) or len(meta[key]) != n: fail(f'{key} length mismatch')
76
+ master = meta.get('master_seed')
77
+ if type(master) is not int or meta.get('episode_seeds') != list(range(master, master + n)):
78
+ fail('seed schedule mismatch')
79
+ states = meta.get('initial_states')
80
+ if isinstance(states, list):
81
+ for i, state in enumerate(states):
82
+ if not isinstance(state, dict) or state.get('episode') != i + 1 or type(master) is not int or state.get('seed') != master + i:
83
+ fail('initial_states episode/seed mismatch'); break
84
+ if not isinstance(state.get('targets'), list) or len(state['targets']) != meta['target_count']:
85
+ fail('initial_states target count mismatch'); break
86
+ hashes = meta.get('initial_state_sha256')
87
+ if isinstance(hashes, list) and any(not isinstance(v, str) or len(v) != 64 or any(c not in '0123456789abcdef' for c in v) for v in hashes):
88
+ fail('invalid initial state hash')
89
+ if valid_arrays:
90
+ wins, steps, rewards = (data[k] for k in ('all_wins', 'all_steps', 'all_rewards'))
91
+ if not all(type(v) is int and v in (0, 1) for v in wins): fail('non-binary wins')
92
+ if not close(data.get('win_rate'), 100 * sum(wins) / n): fail('win_rate inconsistent')
93
+ if meta.get('episode_action_steps') != steps: fail('action counts differ from all_steps')
94
+ if any(type(v) is not int or not 1 <= v <= limit for v in steps): fail('step limit violated')
95
+ if any(w == 0 and step != limit for w, step in zip(wins, steps)): fail('failed episode ended before timeout')
96
+ if any(not 0 <= v <= 100 for k in ['all_accuracies', 'all_actual_firing_accuracies'] for v in data[k]): fail('accuracy outside 0-100')
97
+ if any(v < 0 for v in data['all_smoothness']): fail('negative smoothness')
98
+ mean = sum(rewards) / n
99
+ if not close(data.get('avg_reward'), mean): fail('avg_reward inconsistent')
100
+ if not close(data.get('avg_steps'), sum(steps) / n): fail('avg_steps inconsistent')
101
+ if not close(data.get('reward_std'), math.sqrt(sum((v-mean)**2 for v in rewards) / n)): fail('reward_std inconsistent with population SD')
102
+ if not close(data.get('smoothness'), sum(a*b for a,b in zip(data['all_smoothness'],steps))/max(1,sum(steps))):
103
+ fail('smoothness summary inconsistent with step-weighted aggregation')
104
+ if counts_valid:
105
+ requests, hits, casts = (meta[k] for k in ['episode_shots_fired','episode_shots_hit','episode_raycasts'])
106
+ for i, (req, hit, cast, step) in enumerate(zip(requests,hits,casts,steps)):
107
+ if not 0 <= hit <= cast <= req <= step: fail(f'episode {i+1}: hit/raycast/request counts invalid')
108
+ if hit > meta['target_count'] or (wins[i] == 1) != (hit == meta['target_count']): fail(f'episode {i+1}: hits inconsistent with success')
109
+ if not close(data['all_accuracies'][i], 100*hit/req if req else 0): fail('per-episode request accuracy inconsistent')
110
+ if not close(data['all_actual_firing_accuracies'][i], 100*hit/cast if cast else 0): fail('per-episode actual firing accuracy inconsistent')
111
+ if not close(data.get('accuracy'), 100*sum(hits)/sum(requests) if sum(requests) else 0): fail('accuracy summary inconsistent')
112
+ if not close(data.get('actual_firing_accuracy'), 100*sum(hits)/sum(casts) if sum(casts) else 0): fail('actual firing accuracy summary inconsistent')
113
+ if policy in ('BC', 'DT'):
114
+ values = meta.get('max_abs_rtg_input')
115
+ terminal = meta.get('episode_terminal_rtg')
116
+ initial = meta.get('initial_rtg')
117
+ rtg_valid = (number(initial) and isinstance(values,list) and len(values)==n and all(number(v) and v>=0 for v in values)
118
+ and isinstance(terminal,list) and len(terminal)==n and all(number(v) for v in terminal))
119
+ if not rtg_valid: fail('RTG diagnostics invalid')
120
+ elif policy == 'BC':
121
+ if meta.get('bc_zero_rtg') is not True or initial != 0 or any(v != 0 for v in values + terminal): fail('BC RTG was not always zero')
122
+ else:
123
+ if meta.get('bc_zero_rtg') is not False: fail('DT incorrectly marked zero-RTG')
124
+ if valid_arrays and any(not math.isclose(t,initial-r,rel_tol=2e-4,abs_tol=0.02) for t,r in zip(terminal,data['all_rewards'])):
125
+ fail('terminal RTG does not include the full episode reward')
126
+ traces = meta.get('input_traces', [])
127
+ if not isinstance(traces, list):
128
+ fail('invalid input_traces'); traces = []
129
+ for trace in traces:
130
+ if not isinstance(trace,dict): fail('invalid input trace'); continue
131
+ t, valid = trace.get('timestep'), trace.get('valid_length')
132
+ if type(t) is not int or t < 0 or valid != min(t+1,32): fail('trace valid length mismatch'); continue
133
+ expected = list(range(max(0,t-31), t+1)) + [0]*(32-valid)
134
+ if trace.get('timesteps') != expected: fail('trace absolute timesteps mismatch')
135
+ for key,dim in [('observations',9),('actions',3),('returns_to_go',1)]:
136
+ a = trace.get(key)
137
+ if not isinstance(a,list) or len(a)!=32*dim or not all(number(v) for v in a): fail(f'trace {key} invalid'); continue
138
+ if any(v!=0 for v in a[valid*dim:]): fail('trace right padding is not zero')
139
+ if key=='actions' and any(v not in (0,1) for v in a[2:valid*3:3]): fail('trace fire history is not binary')
140
+ if key=='returns_to_go' and policy=='BC' and any(v!=0 for v in a): fail('BC RTG trace drift')
141
+ results.append((label,path,data,meta))
142
+
143
+ if not results: errors.append('No evaluation results found')
144
+ by_target = defaultdict(list)
145
+ for row in results: by_target[row[3]['target_count']].append(row)
146
+ if expected_targets is not None and set(by_target) != set(expected_targets):
147
+ errors.append(f'Expected target conditions {sorted(expected_targets)}, found {sorted(by_target)}')
148
+ for target, rows in by_target.items():
149
+ identities = [(r[3].get('model_name'),r[3].get('initial_rtg'),r[3].get('policy_type')) for r in rows]
150
+ if len(set(identities)) != len(identities): errors.append(f'{target}: duplicate model/RTG runs; select one run per comparison')
151
+ models = {r[3].get('model_name') for r in rows}
152
+ if not allow_rtg_sensitivity and len(models) != len(rows): errors.append(f'{target}: more than one run for a model')
153
+ if expected_models is not None and len(models) != expected_models: errors.append(f'{target}: expected {expected_models} distinct models, got {len(models)}')
154
+ baseline = rows[0][3]
155
+ for label,path,data,meta in rows[1:]:
156
+ for key in ['episode_seeds','initial_states','initial_state_sha256','fixed_delta_time','max_action_steps',
157
+ 'rotation_speed','shoot_distance','step_penalty','enemy_layer_mask','unity_version','queries_hit_triggers',
158
+ 'reward_protocol','accuracy_protocol']:
159
+ if meta.get(key) != baseline.get(key): errors.append(f'{label}: paired condition mismatch: {key}')
160
+ if not allow_rtg_sensitivity:
161
+ rtgs = {r[3].get('initial_rtg') for r in rows if r[3].get('policy_type') == 'DT'}
162
+ if len(rtgs)>1: errors.append(f'{target}: DT models have different initial RTGs')
163
+ return errors,warnings,results
164
+
165
+ def main():
166
+ p=argparse.ArgumentParser(description=__doc__)
167
+ p.add_argument('root',type=Path)
168
+ p.add_argument('--expected-episodes',type=int,default=50)
169
+ p.add_argument('--expected-models',type=int)
170
+ p.add_argument('--expected-targets',type=int,nargs='+')
171
+ p.add_argument('--allow-rtg-sensitivity',action='store_true')
172
+ args=p.parse_args()
173
+ errors,warnings,rows=validate(args.root,args.expected_episodes,args.expected_models,args.expected_targets,args.allow_rtg_sensitivity)
174
+ for m in errors: print('ERROR:',m)
175
+ for m in warnings: print('WARNING:',m)
176
+ print(f'Checked {len(rows)} result files; {len(errors)} errors.')
177
+ if not errors: print('Protocol/seed/initial-state/metric checks passed. Model validity and statistical assumptions require separate assessment.')
178
+ raise SystemExit(1 if errors else 0)
179
+
180
+ if __name__=='__main__': main()
Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tools/verify_model_pair.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate reproducible Unity buffer/model fixtures from the supplied Python dataset and model.
2
+ Requires numpy, torch, onnx, onnxruntime. No training or checkpoint modification.
3
+ """
4
+ import argparse
5
+ import hashlib
6
+ import importlib.util
7
+ import json
8
+ from pathlib import Path
9
+
10
+ def load_module(path,name):
11
+ spec=importlib.util.spec_from_file_location(name,path)
12
+ module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
13
+ return module
14
+
15
+ def main():
16
+ import numpy as np
17
+ import torch
18
+ import onnx
19
+ import onnxruntime as ort
20
+ p=argparse.ArgumentParser(description=__doc__)
21
+ for name in ['model-py','dataset-py','pth','onnx','output']:p.add_argument('--'+name,type=Path,required=True)
22
+ a=p.parse_args();a.output.mkdir(parents=True,exist_ok=True)
23
+ torch.set_num_threads(2)
24
+ model_module=load_module(a.model_py,'verified_model')
25
+ dataset_module=load_module(a.dataset_py,'verified_dataset')
26
+ model=model_module.DecisionTransformer(9,3,hidden=256,n_layers=4,n_heads=4,max_len=4096)
27
+ state=torch.load(a.pth,map_location='cpu',weights_only=True)
28
+ model.load_state_dict(state,strict=True);model.eval()
29
+ graph=onnx.load(a.onnx);onnx.checker.check_model(graph,full_check=True)
30
+ options=ort.SessionOptions();options.intra_op_num_threads=2;options.inter_op_num_threads=1
31
+ session=ort.InferenceSession(str(a.onnx),sess_options=options,providers=['CPUExecutionProvider'])
32
+ rng=np.random.default_rng(20260910)
33
+ observations=rng.uniform(-1,1,(1000,9)).astype(np.float32)
34
+ actions=rng.uniform(-1,1,(1000,3)).astype(np.float32);actions[:,2]=(actions[:,2]>0).astype(np.float32)
35
+ rewards=rng.uniform(-0.01,0.08,(1000,1)).astype(np.float32)
36
+ ds=dataset_module.TrajectoryDataset.__new__(dataset_module.TrajectoryDataset)
37
+ rows=[dict(obs=torch.from_numpy(observations[i]),act=torch.from_numpy(actions[i]),
38
+ rew=torch.from_numpy(rewards[i]),done=torch.tensor([float(i==999)])) for i in range(1000)]
39
+ ep=ds._process_episode(rows)
40
+ fixtures=dict(model=a.onnx.stem,onnx_sha256=hashlib.sha256(a.onnx.read_bytes()).hexdigest(),seq_len=32,
41
+ steps=[dict(observations=observations[i].tolist(),recorded_action=actions[i].tolist(),rtg=float(ep['rtg'][i].item())) for i in range(1000)],cases=[])
42
+ report=dict(strict_state_load=True,onnx_full_check=True,checkpoint_tensors=len(state),
43
+ model_sha256=fixtures['onnx_sha256'],source_sha256={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in [a.model_py,a.dataset_py,a.pth]},
44
+ versions=dict(torch=torch.__version__,onnx=onnx.__version__,onnxruntime=ort.__version__),tests=[])
45
+ for is_bc in [False,True]:
46
+ for t in [0,1,7,30,31,32,33,100,999]:
47
+ start=max(0,t-31);n=t-start+1
48
+ live=dict(observations=ep['obs'][start:t+1].numpy()[None,:],
49
+ actions=ep['shifted_act'][start:t+1].numpy()[None,:],
50
+ returns_to_go=np.zeros((1,n,1),np.float32) if is_bc else ep['rtg'][start:t+1].numpy()[None,:],
51
+ timesteps=ep['timesteps'][start:t+1].numpy()[None,:].astype(np.int32))
52
+ with torch.inference_mode():
53
+ expected=model(torch.from_numpy(live['observations']),torch.from_numpy(live['actions']),
54
+ torch.from_numpy(live['returns_to_go']),torch.from_numpy(live['timesteps'].astype(np.int64))).numpy()[0,-1]
55
+ padded={k:np.zeros((1,32)+x.shape[2:],dtype=x.dtype) for k,x in live.items()}
56
+ for k,x in live.items():padded[k][:,:n]=x
57
+ actual=session.run(None,padded)[0][0,n-1]
58
+ diff=float(np.max(np.abs(actual-expected)))
59
+ passed=bool(np.allclose(actual,expected,atol=1e-4,rtol=1e-4))
60
+ report['tests'].append(dict(timestep=t,is_bc=is_bc,valid_length=n,max_abs_error=diff,passed=passed))
61
+ assert passed,(t,is_bc,diff)
62
+ fixtures['cases'].append(dict(timestep=t,is_bc=is_bc,valid_length=n,
63
+ expected_action=expected.tolist(),**{k:v.reshape(-1).tolist() for k,v in padded.items()}))
64
+ report['all_passed']=all(t['passed'] for t in report['tests'])
65
+ report['maximum_absolute_error']=max(t['max_abs_error'] for t in report['tests'])
66
+ (a.output/'E_1_DT_C_5_sequence_fixtures.json').write_text(json.dumps(fixtures,indent=2))
67
+ (a.output/'model_verification.json').write_text(json.dumps(report,indent=2))
68
+ print(json.dumps({k:report[k] for k in ['all_passed','maximum_absolute_error','checkpoint_tensors']}))
69
+ print('Passed',len(report['tests']),'dataset / PyTorch / fixed-32 ONNX cases. Unity execution is a separate test.')
70
+
71
+ if __name__=='__main__':main()
Upload/01_Source_Code/Python_Training/dataset_dt.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dataset_dt.py
2
+ import json
3
+ import torch
4
+ from torch.utils.data import Dataset
5
+ import glob
6
+ import os
7
+ import pickle
8
+ import numpy as np
9
+
10
+ class TrajectoryDataset(Dataset):
11
+ def __init__(self, log_dir, seq_len=32, specific_file=None):
12
+ self.seq_len = seq_len
13
+ self.raw_data = []
14
+
15
+ # NEW APPROACH: Load data into EPISODES.
16
+
17
+ raw_rows = []
18
+
19
+ if specific_file:
20
+ print(f"[INFO] Loading specific file: {specific_file}...")
21
+ try:
22
+ with open(specific_file, "rb") as f:
23
+ raw_rows = pickle.load(f)
24
+ print(f"[INFO] Loaded {len(raw_rows)} steps from {specific_file}.")
25
+ except (EOFError, pickle.UnpicklingError) as e:
26
+ print(f"[ERROR] Failed to load {specific_file}: {e}")
27
+ raw_rows = []
28
+ else:
29
+ # Check for split pickle files first (Memory Efficient Load)
30
+ split_files = sorted(glob.glob(os.path.join(log_dir, "trajectory_data_part_*.pkl")))
31
+ if not split_files:
32
+ split_files = sorted(glob.glob("trajectory_data_part_*.pkl"))
33
+
34
+ if split_files:
35
+ raise RuntimeError(
36
+ f"[ERROR] Found {len(split_files)} split files but 'specific_file' was not provided.\n"
37
+ "Cannot load all split files at once due to memory constraints.\n"
38
+ "Please use 'train_sequential.py' to train sequentially on chunks."
39
+ )
40
+ else:
41
+ # Fallback to single pickle or JSON
42
+ pickle_file = "trajectory_data.pkl"
43
+ if os.path.exists(pickle_file):
44
+ print(f"[INFO] Loading data from {pickle_file}...")
45
+ try:
46
+ with open(pickle_file, "rb") as f:
47
+ raw_rows = pickle.load(f)
48
+ print(f"[INFO] Loaded {len(raw_rows)} steps from pickle.")
49
+ except Exception as e:
50
+ print(f"[WARNING] Pickle corrupted: {e}")
51
+ raw_rows = []
52
+
53
+ if not raw_rows:
54
+ json_files = sorted(glob.glob(os.path.join(log_dir, "*.json")))
55
+ if json_files:
56
+ print(f"[INFO] Loading {len(json_files)} JSON files...")
57
+ for jf in json_files:
58
+ try:
59
+ with open(jf, 'r') as f:
60
+ data = json.load(f)
61
+ rows = data.get("data", [])
62
+ # Normalize JSON rows to lists if needed
63
+ for r in rows:
64
+ vals = r["values"] if isinstance(r, dict) else r
65
+ if isinstance(vals, list) and len(vals) == 14:
66
+ raw_rows.append(vals)
67
+ except: pass
68
+
69
+ # Process raw_rows into Episodes => Steps
70
+ # We need to reconstruct episodes to calculate RTG correctly.
71
+ self.episodes = []
72
+ current_episode = []
73
+
74
+ for row in raw_rows:
75
+ # row can be a dict (from convert_added_json_to_pickle) OR a list (legacy raw values)
76
+
77
+ if isinstance(row, dict):
78
+ # Format: {'obs': tensor, 'act': tensor, 'rew': tensor, 'done': tensor}
79
+ obs = row['obs']
80
+ act = row['act']
81
+ rew = row['rew']
82
+ done = row['done']
83
+
84
+ # Ensure they are tensors
85
+ if not isinstance(obs, torch.Tensor): obs = torch.tensor(obs, dtype=torch.float32)
86
+ if not isinstance(act, torch.Tensor): act = torch.tensor(act, dtype=torch.float32)
87
+ if not isinstance(rew, torch.Tensor): rew = torch.tensor(rew, dtype=torch.float32)
88
+ if not isinstance(done, torch.Tensor): done = torch.tensor(done, dtype=torch.float32)
89
+
90
+ else:
91
+ # Legacy List format
92
+ vals = torch.tensor(row, dtype=torch.float32)
93
+ obs = vals[:9]
94
+ act_cont = vals[9:11]
95
+ act_fire = vals[11:12]
96
+ rew = vals[12:13]
97
+ done = vals[13:14]
98
+ act = torch.cat([act_cont, act_fire])
99
+
100
+ step = {
101
+ "obs": obs,
102
+ "act": act,
103
+ "rew": rew,
104
+ "done": done
105
+ }
106
+ current_episode.append(step)
107
+
108
+ # Check done flag (assuming scalar tensor)
109
+ if done.item() > 0.5:
110
+ self.episodes.append(self._process_episode(current_episode))
111
+ current_episode = []
112
+
113
+ # Handle trailing data
114
+ if current_episode:
115
+ self.episodes.append(self._process_episode(current_episode))
116
+
117
+ # Flatten for indexing
118
+ self.indices = []
119
+ for ep_idx, ep in enumerate(self.episodes):
120
+ length = len(ep["obs"])
121
+ for t in range(length):
122
+ self.indices.append((ep_idx, t))
123
+
124
+ print(f"[INFO] Processed {len(self.episodes)} episodes. Total {len(self.indices)} samples.")
125
+
126
+ def _process_episode(self, episode_steps):
127
+ # Calculate RTG for this episode
128
+ rews = torch.stack([s["rew"] for s in episode_steps]) # (L, 1)
129
+
130
+ # RTG calculation: Cumulative sum from back to front
131
+ rtg = torch.flip(torch.cumsum(torch.flip(rews, dims=[0]), dim=0), dims=[0])
132
+ if rtg.dim() == 1:
133
+ rtg = rtg.unsqueeze(-1) # Ensure (L, 1)
134
+
135
+ # Timesteps
136
+ timesteps = torch.arange(len(episode_steps), dtype=torch.long)
137
+
138
+ # Stack others
139
+ obs = torch.stack([s["obs"] for s in episode_steps])
140
+ acts = torch.stack([s["act"] for s in episode_steps])
141
+ dones = torch.stack([s["done"] for s in episode_steps])
142
+
143
+ # Create Shifted Actions (a_{t-1}) for input
144
+ # Prepend zero, remove last
145
+ first_zero = torch.zeros(1, acts.shape[1])
146
+ shifted_acts = torch.cat([first_zero, acts[:-1]], dim=0)
147
+
148
+ return {
149
+ "obs": obs,
150
+ "act": acts, # Target (a_t)
151
+ "shifted_act": shifted_acts, # Input (a_{t-1})
152
+ "rtg": rtg,
153
+ "timesteps": timesteps,
154
+ "len": len(episode_steps)
155
+ }
156
+
157
+ def __len__(self):
158
+ return len(self.indices)
159
+
160
+ def __getitem__(self, idx):
161
+ ep_idx, start_t = self.indices[idx]
162
+ ep = self.episodes[ep_idx]
163
+
164
+ end_t = start_t + self.seq_len
165
+ real_end = min(end_t, ep["len"])
166
+
167
+ # Aligned Slicing: All tensors sliced identically [start : end]
168
+ # Logic: Input(s_t, a_{t-1}) -> Target(a_t)
169
+
170
+ obs = ep["obs"][start_t : real_end]
171
+ rtg = ep["rtg"][start_t : real_end]
172
+ time = ep["timesteps"][start_t : real_end]
173
+
174
+ target = ep["act"][start_t : real_end] # a_t (Target)
175
+ act = ep["shifted_act"][start_t : real_end] # a_{t-1} (Input)
176
+
177
+ # Pad if necessary to match seq_len
178
+ # Pad to full seq_len
179
+ if obs.shape[0] < self.seq_len:
180
+ pad = self.seq_len - obs.shape[0]
181
+ obs = torch.cat([obs, torch.zeros(pad, obs.shape[1])])
182
+ act = torch.cat([act, torch.zeros(pad, act.shape[1])])
183
+ target = torch.cat([target, torch.zeros(pad, act.shape[1])])
184
+ rtg = torch.cat([rtg, torch.zeros(pad, 1)])
185
+ time = torch.cat([time, torch.zeros(pad, dtype=torch.long)])
186
+
187
+ return {
188
+ "observations": obs,
189
+ "actions": act, # Input a_{t-1}
190
+ "returns_to_go": rtg,
191
+ "timesteps": time,
192
+ "target_actions": target # Target a_t
193
+ }
Upload/01_Source_Code/Python_Training/finetuning_ext_RLStep.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch.utils.data import DataLoader, Subset
3
+ from dataset_dt import TrajectoryDataset
4
+ from model_dt import DecisionTransformer
5
+ import glob
6
+ import os
7
+ import gc
8
+ import time
9
+ import argparse
10
+ import sys
11
+ import re
12
+
13
+ def finetuning_rl_steps(
14
+ data_prefix="trajectory_data_part_",
15
+ output_model="dt_model_finetuned.pth",
16
+ load_model="dt_model_trained.pth",
17
+ target_rl_steps=1000000,
18
+ epochs=5,
19
+ learning_rate=1e-5
20
+ ):
21
+ log_dir = "./logs"
22
+ seq_len = 32
23
+ batch_size = 32
24
+
25
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
26
+ print(f"[INFO] Using device: {device}")
27
+
28
+ # 1. Initialize Model Dimensions (Peek Logic)
29
+ pattern = f"{data_prefix}*.pkl"
30
+ split_files = sorted(glob.glob(pattern))
31
+ if not split_files:
32
+ split_files = sorted(glob.glob(os.path.join(log_dir, pattern)))
33
+
34
+ if not split_files:
35
+ # Fallback for spelling
36
+ if "Trajectoy" in data_prefix:
37
+ fallback = data_prefix.replace("Trajectoy", "Trajectory")
38
+ split_files = sorted(glob.glob(fallback))
39
+
40
+ if not split_files:
41
+ print(f"[ERROR] No files found matching prefix: {data_prefix}")
42
+ return
43
+
44
+ print(f"[INFO] Found {len(split_files)} split files matching '{data_prefix}'.")
45
+
46
+ print("[INFO] Peeking at first file for dimensions...")
47
+ temp_dataset = TrajectoryDataset(log_dir, seq_len=seq_len, specific_file=split_files[0])
48
+ if len(temp_dataset) > 0:
49
+ obs_dim = temp_dataset[0]["observations"].shape[-1]
50
+ act_dim = temp_dataset[0]["actions"].shape[-1]
51
+ else:
52
+ obs_dim = 9
53
+ act_dim = 3
54
+
55
+ del temp_dataset
56
+ gc.collect()
57
+
58
+ print(f"[INFO] Obs Dim: {obs_dim}, Act Dim: {act_dim}")
59
+
60
+ # 2. Model Setup
61
+ model = DecisionTransformer(
62
+ obs_dim=obs_dim,
63
+ act_dim=act_dim,
64
+ hidden=256,
65
+ n_layers=4,
66
+ n_heads=4,
67
+ max_len=4096
68
+ ).to(device)
69
+
70
+ # Load Checkpoint
71
+ if os.path.exists(load_model):
72
+ print(f"[INFO] Loading checkpoint: {load_model}")
73
+ try:
74
+ state_dict = torch.load(load_model, map_location=device)
75
+ model.load_state_dict(state_dict)
76
+ print("[INFO] Model loaded successfully.")
77
+ except Exception as e:
78
+ print(f"[ERROR] Failed to load checkpoint: {e}")
79
+ return
80
+ else:
81
+ print(f"[WARNING] Checkpoint '{load_model}' not found! Starting FRESH training (Fine-Tuning aborted).")
82
+ # Depending on user intent, we might want to return here.
83
+ # But usually we proceed if user insists, though it's technically Pre-training then.
84
+
85
+ print(f"[INFO] Learning rate: {learning_rate}")
86
+ optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
87
+
88
+ print(f"[INFO] Starting FINE-TUNING with RL STEP LIMIT: {target_rl_steps} (Pre-loading to Memory)")
89
+
90
+ # 3. Pre-load Data Phase
91
+ loaded_datasets = []
92
+ total_rl_steps_loaded = 0
93
+
94
+ for i, pkl_file in enumerate(split_files):
95
+ steps_needed = target_rl_steps - total_rl_steps_loaded
96
+ if steps_needed <= 0:
97
+ break
98
+
99
+ print(f"[INFO] Pre-loading chunk {i+1}/{len(split_files)}: {pkl_file}")
100
+ dataset = TrajectoryDataset(log_dir, seq_len=seq_len, specific_file=pkl_file)
101
+ dataset_len = len(dataset)
102
+
103
+ if dataset_len == 0:
104
+ continue
105
+
106
+ if dataset_len > steps_needed:
107
+ print(f" [LIMIT] Trimming chunk to {steps_needed} samples.")
108
+ dataset = Subset(dataset, range(steps_needed))
109
+ loaded_datasets.append(dataset)
110
+ total_rl_steps_loaded += steps_needed
111
+ break
112
+ else:
113
+ loaded_datasets.append(dataset)
114
+ total_rl_steps_loaded += dataset_len
115
+
116
+ print(f" [PROGRESS] Memory Buffer: {total_rl_steps_loaded} / {target_rl_steps}")
117
+
118
+ if not loaded_datasets:
119
+ print("[ERROR] No data loaded! Check file paths.")
120
+ return
121
+
122
+ # 4. Training Phase (Sequential Chunk Processing)
123
+ print(f"[INFO] Data Pre-loading Complete. Starting Fine-Tuning on {len(loaded_datasets)} chunks.")
124
+
125
+ global_gradient_steps = 0
126
+
127
+ for epoch in range(epochs):
128
+ print(f"\n=== Fine-Tuning Epoch {epoch+1}/{epochs} ===")
129
+ epoch_start_time = time.time()
130
+
131
+ for i, dataset in enumerate(loaded_datasets):
132
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
133
+
134
+ model.train()
135
+ chunk_loss = 0.0
136
+ chunk_steps = 0
137
+
138
+ for batch in loader:
139
+ states = batch['observations'].to(device)
140
+ actions = batch['actions'].to(device)
141
+ returns = batch['returns_to_go'].to(device)
142
+ timesteps = batch['timesteps'].to(device)
143
+
144
+ # Forward
145
+ action_preds = model(
146
+ obs=states,
147
+ act=actions,
148
+ rtg=returns,
149
+ timesteps=timesteps
150
+ )
151
+
152
+ # Loss
153
+ action_target = batch['target_actions'].to(device)
154
+ loss = torch.mean((action_preds - action_target) ** 2)
155
+
156
+ optimizer.zero_grad()
157
+ loss.backward()
158
+ optimizer.step()
159
+
160
+ chunk_loss += loss.item()
161
+ chunk_steps += 1
162
+ global_gradient_steps += 1
163
+
164
+ if chunk_steps % 100 == 0:
165
+ print(f" Grad Step {chunk_steps}, Loss: {loss.item():.4f}", end="\r")
166
+
167
+ avg_chunk_loss = chunk_loss / chunk_steps if chunk_steps > 0 else 0
168
+ print(f" Chunk {i+1} Finished. Avg Loss: {avg_chunk_loss:.4f}")
169
+
170
+ print(f"Epoch {epoch+1} completed in {time.time() - epoch_start_time:.2f}s.")
171
+
172
+ # Save Model per Epoch
173
+ dir_name, file_name = os.path.split(output_model)
174
+ epoch_model_path = os.path.join(dir_name, f"E_{epoch+1}_{file_name}")
175
+ torch.save(model.state_dict(), epoch_model_path)
176
+ print(f"[INFO] Saved Epoch {epoch+1} Checkpoint to: {epoch_model_path}")
177
+
178
+ print(f"\n[DONE] Fine-Tuning Finished.")
179
+ print(f" Total RL Steps Processed (cached): {total_rl_steps_loaded}")
180
+ print(f" Total Gradient Steps: {global_gradient_steps}")
181
+
182
+ # Save Final Model
183
+ torch.save(model.state_dict(), output_model)
184
+ print(f"[INFO] Saved Final Model to: {output_model}")
185
+
186
+ if __name__ == "__main__":
187
+ parser = argparse.ArgumentParser(description="Fine-Tune Decision Transformer with RL Step Limit")
188
+ parser.add_argument("--prefix", type=str, default="trajectory_data_part_", help="Prefix of the pickle files to load")
189
+ parser.add_argument("--output", type=str, default="dt_model_finetuned.pth", help="Output filename")
190
+ parser.add_argument("--load_model", type=str, required=True, help="Path to pre-trained model checkpoint")
191
+ parser.add_argument("--target_rl_steps", type=int, default=1000000, help="Total RL steps (samples) to train on")
192
+ parser.add_argument("--epochs", type=int, default=5, help="Number of epochs")
193
+ parser.add_argument("--learning_rate", type=float, default=1e-5, help="Learning rate (default: 1e-5)")
194
+
195
+ args = parser.parse_args()
196
+
197
+ target_steps = args.target_steps if args.target_steps > 0 else 1000000
198
+
199
+ print("\n[Configuration]")
200
+ print(f" Load Model: {args.load_model}")
201
+ print(f" Prefix: {args.prefix}")
202
+ print(f" Output: {args.output}")
203
+ print(f" Target RL Steps: {target_steps}")
204
+ print(f" Epochs: {args.epochs}")
205
+ print(f" LR: {args.learning_rate}")
206
+ print("-" * 30)
207
+
208
+ finetuning_rl_steps(
209
+ data_prefix=args.prefix,
210
+ output_model=args.output,
211
+ load_model=args.load_model,
212
+ target_rl_steps=target_steps,
213
+ epochs=args.epochs,
214
+ learning_rate=args.learning_rate
215
+ )
Upload/01_Source_Code/Python_Training/model_dt.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ import math
4
+
5
+ class DecisionTransformer(nn.Module):
6
+ def __init__(self, obs_dim, act_dim, hidden=256, n_layers=4, n_heads=4, max_len=1024):
7
+ super().__init__()
8
+ self.hidden = hidden
9
+ self.n_heads = n_heads
10
+ self.max_len = max_len
11
+
12
+ # 각 입력을 hidden 차원으로 임베딩
13
+ self.obs_embed = nn.Linear(obs_dim, hidden)
14
+ self.act_embed = nn.Linear(act_dim, hidden)
15
+ self.rtg_embed = nn.Linear(1, hidden)
16
+ self.time_embed = nn.Embedding(max_len, hidden)
17
+
18
+ # Transformer Encoder
19
+ layer = nn.TransformerEncoderLayer(
20
+ d_model=hidden,
21
+ nhead=n_heads,
22
+ dim_feedforward=hidden * 4,
23
+ batch_first=True,
24
+ dropout=0.1
25
+ )
26
+ self.transformer = nn.TransformerEncoder(layer, num_layers=n_layers)
27
+
28
+ # 행동 예측 헤드
29
+ self.act_head = nn.Linear(hidden, act_dim)
30
+
31
+ # Causal Mask 생성을 위한 버퍼 등록 (ONNX export시 사용 안 할 수도 있지만 호환성 위해 유지)
32
+ # self.register_buffer("mask", torch.tril(torch.ones(max_len * 3, max_len * 3)))
33
+
34
+ def forward(self, obs, act, rtg, timesteps):
35
+ # obs: (B, T, obs_dim)
36
+ # act: (B, T, act_dim)
37
+ # rtg: (B, T, 1)
38
+ # timesteps: (B, T)
39
+
40
+ B, T, _ = obs.shape
41
+
42
+ # 1. 임베딩 (Embedding)
43
+ obs_emb = self.obs_embed(obs) # (B, T, hidden)
44
+ act_emb = self.act_embed(act) # (B, T, hidden)
45
+ rtg_emb = self.rtg_embed(rtg) # (B, T, hidden)
46
+ time_emb = self.time_embed(timesteps) # (B, T, hidden)
47
+
48
+ # 2. Timestep Embedding 더하기
49
+ # 논문에서는 R, s, a 모두에 timestep embedding을 더함
50
+ obs_emb = obs_emb + time_emb
51
+ act_emb = act_emb + time_emb
52
+ rtg_emb = rtg_emb + time_emb
53
+
54
+ # 3. Stacking (R_t, s_t, a_t) 순서로 쌓기
55
+ # (B, T, 3, hidden) -> (B, 3*T, hidden)
56
+ # dim=2에 stack 후 flatten
57
+ stacked_inputs = torch.stack((rtg_emb, obs_emb, act_emb), dim=2)
58
+ stacked_inputs = stacked_inputs.view(B, T * 3, self.hidden)
59
+
60
+ # 4. Causal Masking
61
+ # 현재 시퀀스 길이(3*T)에 맞는 마스크 동적 생성
62
+ seq_len = T * 3
63
+ # (seq_len, seq_len) 크기의 마스크 생성
64
+ # 대각선 위쪽(미래)을 -inf로 채움 (Attention에서 무시됨)
65
+ # 대각선 포함 아래쪽(과거+현재)은 0으로 유지
66
+ causal_mask = torch.triu(torch.full((seq_len, seq_len), float('-inf'), device=obs.device), diagonal=1)
67
+
68
+ # 5. Transformer Forward
69
+ # is_causal=True를 명시하여 내부적인 마스크 검사(data-dependent check)를 우회
70
+ x = self.transformer(stacked_inputs, mask=causal_mask, is_causal=True)
71
+
72
+ # 6. Action Prediction
73
+ # 입력 순서가 (R_t, s_t, a_t) 이므로,
74
+ # s_t의 출력(index 1, 4, 7...)을 사용하여 a_t를 예측해야 함
75
+ # x: (B, 3*T, hidden)
76
+ # reshape -> (B, T, 3, hidden)
77
+ x = x.view(B, T, 3, self.hidden)
78
+
79
+ # s_t에 해당하는 임베딩 추출 (index 1)
80
+ # R_t(0), s_t(1), a_t(2)
81
+ state_preds = x[:, :, 1, :] # (B, T, hidden)
82
+
83
+ action_preds = self.act_head(state_preds) # (B, T, act_dim)
84
+
85
+ return action_preds
Upload/01_Source_Code/Python_Training/train_sequential_ext_RLStep.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch.utils.data import DataLoader, Subset
3
+ from dataset_dt import TrajectoryDataset
4
+ from model_dt import DecisionTransformer
5
+ import glob
6
+ import os
7
+ import gc
8
+ import time
9
+ import argparse
10
+ import sys
11
+
12
+ def train_sequential_rl_steps(data_prefix="trajectory_data_part_", output_model="dt_model_trained_rl_limited.pth", target_rl_steps=1000000):
13
+ log_dir = "./logs"
14
+ seq_len = 32
15
+ batch_size = 32
16
+ learning_rate = 1e-4
17
+ epochs = 3
18
+ # Note: Epochs are still iterated, but we will likely break early in the first epoch
19
+ # if target_rl_steps is reached. If target is huge, it might run multiple epochs.
20
+ # But for "1M RL Steps" on a dataset of >2M, it will stop in Epoch 1.
21
+
22
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
23
+ print(f"[INFO] Using device: {device}")
24
+
25
+ # 1. Initialize Model Dimensions
26
+ pattern = f"{data_prefix}*.pkl"
27
+ split_files = sorted(glob.glob(pattern))
28
+ if not split_files:
29
+ split_files = sorted(glob.glob(os.path.join(log_dir, pattern)))
30
+
31
+ if not split_files:
32
+ print(f"[ERROR] No files found matching prefix: {data_prefix}")
33
+ return
34
+
35
+ print(f"[INFO] Found {len(split_files)} split files matching '{data_prefix}'.")
36
+
37
+ # Peek at first file for dimensions
38
+ print("[INFO] Peeking at first file for dimensions...")
39
+ temp_dataset = TrajectoryDataset(log_dir, seq_len=seq_len, specific_file=split_files[0])
40
+ if len(temp_dataset) > 0:
41
+ obs_dim = temp_dataset[0]["observations"].shape[-1]
42
+ act_dim = temp_dataset[0]["actions"].shape[-1]
43
+ else:
44
+ # Fallback defaults
45
+ obs_dim = 9
46
+ act_dim = 3
47
+
48
+ del temp_dataset
49
+ gc.collect()
50
+
51
+ print(f"[INFO] Obs Dim: {obs_dim}, Act Dim: {act_dim}")
52
+
53
+ model = DecisionTransformer(
54
+ obs_dim=obs_dim,
55
+ act_dim=act_dim,
56
+ hidden=256,
57
+ n_layers=4,
58
+ n_heads=4,
59
+ max_len=4096
60
+ ).to(device)
61
+
62
+ optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
63
+
64
+ print(f"[INFO] Starting FRESH training with RL STEP LIMIT: {target_rl_steps} (Pre-loading to Memory)")
65
+
66
+ # 1. Pre-load Data Phase
67
+ loaded_datasets = []
68
+ total_rl_steps_loaded = 0
69
+
70
+ for i, pkl_file in enumerate(split_files):
71
+ steps_needed = target_rl_steps - total_rl_steps_loaded
72
+ if steps_needed <= 0:
73
+ break
74
+
75
+ print(f"[INFO] Pre-loading chunk {i+1}/{len(split_files)}: {pkl_file}")
76
+ dataset = TrajectoryDataset(log_dir, seq_len=seq_len, specific_file=pkl_file)
77
+ dataset_len = len(dataset)
78
+
79
+ if dataset_len == 0:
80
+ continue
81
+
82
+ if dataset_len > steps_needed:
83
+ print(f" [LIMIT] Trimming chunk to {steps_needed} samples.")
84
+ dataset = Subset(dataset, range(steps_needed))
85
+ loaded_datasets.append(dataset)
86
+ total_rl_steps_loaded += steps_needed
87
+ break
88
+ else:
89
+ loaded_datasets.append(dataset)
90
+ total_rl_steps_loaded += dataset_len
91
+
92
+ print(f" [PROGRESS] Memory Buffer: {total_rl_steps_loaded} / {target_rl_steps}")
93
+
94
+ if not loaded_datasets:
95
+ print("[ERROR] No data loaded! Check file paths.")
96
+ return
97
+
98
+ # 2. Training Phase (Sequential per Chunk, just like original)
99
+ print(f"[INFO] Data Pre-loading Complete. Starting Training on {len(loaded_datasets)} chunks.")
100
+
101
+ global_gradient_steps = 0
102
+
103
+ for epoch in range(epochs):
104
+ print(f"\n=== Epoch {epoch+1}/{epochs} ===")
105
+ epoch_start_time = time.time()
106
+
107
+ for i, dataset in enumerate(loaded_datasets):
108
+ # Same Batch/Shuffle logic as original
109
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
110
+
111
+ model.train()
112
+ chunk_loss = 0.0
113
+ chunk_steps = 0
114
+
115
+ for batch in loader:
116
+ states = batch['observations'].to(device)
117
+ actions = batch['actions'].to(device)
118
+ returns = batch['returns_to_go'].to(device)
119
+ timesteps = batch['timesteps'].to(device)
120
+
121
+ # Forward
122
+ action_preds = model(
123
+ obs=states,
124
+ act=actions,
125
+ rtg=returns,
126
+ timesteps=timesteps
127
+ )
128
+
129
+ # Loss
130
+ action_target = batch['target_actions'].to(device)
131
+ loss = torch.mean((action_preds - action_target) ** 2)
132
+
133
+ optimizer.zero_grad()
134
+ loss.backward()
135
+ optimizer.step()
136
+
137
+ chunk_loss += loss.item()
138
+ chunk_steps += 1
139
+ global_gradient_steps += 1
140
+
141
+ if chunk_steps % 100 == 0:
142
+ print(f" Grad Step {chunk_steps}, Loss: {loss.item():.4f}", end="\r")
143
+
144
+ avg_chunk_loss = chunk_loss / chunk_steps if chunk_steps > 0 else 0
145
+ print(f" Chunk {i+1} Finished. Avg Loss: {avg_chunk_loss:.4f}")
146
+
147
+ print(f"Epoch {epoch+1} completed in {time.time() - epoch_start_time:.2f}s.")
148
+
149
+ # Save Model per Epoch
150
+ dir_name, file_name = os.path.split(output_model)
151
+ epoch_model_path = os.path.join(dir_name, f"E_{epoch+1}_{file_name}")
152
+ torch.save(model.state_dict(), epoch_model_path)
153
+ print(f"[INFO] Saved Epoch {epoch+1} Model to: {epoch_model_path}")
154
+
155
+ print(f"\n[DONE] Training Finished.")
156
+ print(f" Total RL Steps Processed (cached): {total_rl_steps_loaded}")
157
+ print(f" Total Gradient Steps: {global_gradient_steps}")
158
+
159
+ print(f"\n[DONE] Training Finished.")
160
+ print(f" Total RL Steps Processed: {total_rl_steps_loaded}")
161
+ print(f" Total Gradient Steps: {global_gradient_steps}")
162
+
163
+ # Save Final Model
164
+ torch.save(model.state_dict(), output_model)
165
+ print(f"[INFO] Saved Final Model to: {output_model}")
166
+
167
+ if __name__ == "__main__":
168
+ parser = argparse.ArgumentParser(description="Train Decision Transformer with RL Step Limit")
169
+ parser.add_argument("--prefix", type=str, default="trajectory_data_part_", help="Prefix of the pickle files to load")
170
+ parser.add_argument("--output", type=str, default="dt_model_1M_RL.pth", help="Output filename")
171
+ parser.add_argument("--target_rl_steps", type=int, default=1000000, help="Total RL steps (samples) to train on")
172
+
173
+ args = parser.parse_args()
174
+
175
+ print("\n[Configuration]")
176
+ print(f" Prefix: {args.prefix}")
177
+ print(f" Output: {args.output}")
178
+ print(f" Target RL Steps: {args.target_rl_steps}")
179
+ print("-" * 30)
180
+
181
+ train_sequential_rl_steps(data_prefix=args.prefix, output_model=args.output, target_rl_steps=args.target_rl_steps)
Upload/01_Source_Code/Python_Training/train_sequential_ext_RLStep_For_BC.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch.utils.data import DataLoader, Subset
3
+ from dataset_dt import TrajectoryDataset
4
+ from model_dt import DecisionTransformer
5
+ import glob
6
+ import os
7
+ import gc
8
+ import time
9
+ import argparse
10
+ import sys
11
+
12
+ def train_sequential_rl_steps_BC(data_prefix="trajectory_data_part_", output_model="dt_model_trained_rl_limited.pth", target_rl_steps=1000000):
13
+ log_dir = "./logs"
14
+ seq_len = 32
15
+ batch_size = 32
16
+ learning_rate = 1e-4
17
+ epochs = 3
18
+ # Note: Epochs are still iterated, but we will likely break early in the first epoch
19
+ # if target_rl_steps is reached. If target is huge, it might run multiple epochs.
20
+ # But for "1M RL Steps" on a dataset of >2M, it will stop in Epoch 1.
21
+
22
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
23
+ print(f"[INFO] Using device: {device}")
24
+
25
+ # 1. Initialize Model Dimensions
26
+ pattern = f"{data_prefix}*.pkl"
27
+ split_files = sorted(glob.glob(pattern))
28
+ if not split_files:
29
+ split_files = sorted(glob.glob(os.path.join(log_dir, pattern)))
30
+
31
+ if not split_files:
32
+ print(f"[ERROR] No files found matching prefix: {data_prefix}")
33
+ return
34
+
35
+ print(f"[INFO] Found {len(split_files)} split files matching '{data_prefix}'.")
36
+
37
+ # Peek at first file for dimensions
38
+ print("[INFO] Peeking at first file for dimensions...")
39
+ temp_dataset = TrajectoryDataset(log_dir, seq_len=seq_len, specific_file=split_files[0])
40
+ if len(temp_dataset) > 0:
41
+ obs_dim = temp_dataset[0]["observations"].shape[-1]
42
+ act_dim = temp_dataset[0]["actions"].shape[-1]
43
+ else:
44
+ # Fallback defaults
45
+ obs_dim = 9
46
+ act_dim = 3
47
+
48
+ del temp_dataset
49
+ gc.collect()
50
+
51
+ print(f"[INFO] Obs Dim: {obs_dim}, Act Dim: {act_dim}")
52
+
53
+ model = DecisionTransformer(
54
+ obs_dim=obs_dim,
55
+ act_dim=act_dim,
56
+ hidden=256,
57
+ n_layers=4,
58
+ n_heads=4,
59
+ max_len=4096
60
+ ).to(device)
61
+
62
+ optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
63
+
64
+ print(f"[INFO] Starting FRESH training with RL STEP LIMIT: {target_rl_steps} (Pre-loading to Memory)")
65
+
66
+ # 1. Pre-load Data Phase
67
+ loaded_datasets = []
68
+ total_rl_steps_loaded = 0
69
+
70
+ for i, pkl_file in enumerate(split_files):
71
+ steps_needed = target_rl_steps - total_rl_steps_loaded
72
+ if steps_needed <= 0:
73
+ break
74
+
75
+ print(f"[INFO] Pre-loading chunk {i+1}/{len(split_files)}: {pkl_file}")
76
+ dataset = TrajectoryDataset(log_dir, seq_len=seq_len, specific_file=pkl_file)
77
+ dataset_len = len(dataset)
78
+
79
+ if dataset_len == 0:
80
+ continue
81
+
82
+ if dataset_len > steps_needed:
83
+ print(f" [LIMIT] Trimming chunk to {steps_needed} samples.")
84
+ dataset = Subset(dataset, range(steps_needed))
85
+ loaded_datasets.append(dataset)
86
+ total_rl_steps_loaded += steps_needed
87
+ break
88
+ else:
89
+ loaded_datasets.append(dataset)
90
+ total_rl_steps_loaded += dataset_len
91
+
92
+ print(f" [PROGRESS] Memory Buffer: {total_rl_steps_loaded} / {target_rl_steps}")
93
+
94
+ if not loaded_datasets:
95
+ print("[ERROR] No data loaded! Check file paths.")
96
+ return
97
+
98
+ # 2. Training Phase (Sequential per Chunk, just like original)
99
+ print(f"[INFO] Data Pre-loading Complete. Starting Training on {len(loaded_datasets)} chunks.")
100
+
101
+ global_gradient_steps = 0
102
+
103
+ for epoch in range(epochs):
104
+ print(f"\n=== Epoch {epoch+1}/{epochs} ===")
105
+ epoch_start_time = time.time()
106
+
107
+ for i, dataset in enumerate(loaded_datasets):
108
+ # Same Batch/Shuffle logic as original
109
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
110
+
111
+ model.train()
112
+ chunk_loss = 0.0
113
+ chunk_steps = 0
114
+
115
+ for batch in loader:
116
+ states = batch['observations'].to(device)
117
+ actions = batch['actions'].to(device)
118
+ returns = batch['returns_to_go'].to(device)
119
+ timesteps = batch['timesteps'].to(device)
120
+
121
+ returns = torch.zeros_like(returns)
122
+
123
+ # Forward
124
+ action_preds = model(
125
+ obs=states,
126
+ act=actions,
127
+ rtg=returns,
128
+ timesteps=timesteps
129
+ )
130
+
131
+ # Loss
132
+ action_target = batch['target_actions'].to(device)
133
+ loss = torch.mean((action_preds - action_target) ** 2)
134
+
135
+ optimizer.zero_grad()
136
+ loss.backward()
137
+ optimizer.step()
138
+
139
+ chunk_loss += loss.item()
140
+ chunk_steps += 1
141
+ global_gradient_steps += 1
142
+
143
+ if chunk_steps % 100 == 0:
144
+ print(f" Grad Step {chunk_steps}, Loss: {loss.item():.4f}", end="\r")
145
+
146
+ avg_chunk_loss = chunk_loss / chunk_steps if chunk_steps > 0 else 0
147
+ print(f" Chunk {i+1} Finished. Avg Loss: {avg_chunk_loss:.4f}")
148
+
149
+ print(f"Epoch {epoch+1} completed in {time.time() - epoch_start_time:.2f}s.")
150
+
151
+ # Save Model per Epoch
152
+ dir_name, file_name = os.path.split(output_model)
153
+ epoch_model_path = os.path.join(dir_name, f"E_{epoch+1}_{file_name}")
154
+ torch.save(model.state_dict(), epoch_model_path)
155
+ print(f"[INFO] Saved Epoch {epoch+1} Model to: {epoch_model_path}")
156
+
157
+ print(f"\n[DONE] Training Finished.")
158
+ print(f" Total RL Steps Processed (cached): {total_rl_steps_loaded}")
159
+ print(f" Total Gradient Steps: {global_gradient_steps}")
160
+
161
+ print(f"\n[DONE] Training Finished.")
162
+ print(f" Total RL Steps Processed: {total_rl_steps_loaded}")
163
+ print(f" Total Gradient Steps: {global_gradient_steps}")
164
+
165
+ # Save Final Model
166
+ torch.save(model.state_dict(), output_model)
167
+ print(f"[INFO] Saved Final Model to: {output_model}")
168
+
169
+ if __name__ == "__main__":
170
+ parser = argparse.ArgumentParser(description="Train Decision Transformer with RL Step Limit")
171
+ parser.add_argument("--prefix", type=str, default="trajectory_data_part_", help="Prefix of the pickle files to load")
172
+ parser.add_argument("--output", type=str, default="dt_model_1M_RL.pth", help="Output filename")
173
+ parser.add_argument("--target_rl_steps", type=int, default=1000000, help="Total RL steps (samples) to train on")
174
+
175
+ args = parser.parse_args()
176
+
177
+ print("\n[Configuration]")
178
+ print(f" Prefix: {args.prefix}")
179
+ print(f" Output: {args.output}")
180
+ print(f" Target RL Steps: {args.target_rl_steps}")
181
+ print("-" * 30)
182
+
183
+ train_sequential_rl_steps(data_prefix=args.prefix, output_model=args.output, target_rl_steps=args.target_rl_steps)
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations.meta ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 3a5f16fdbeaac7d41bd7fa31ffce8afc
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem.demo ADDED
Binary file (1.02 kB). View file
 
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem.demo.meta ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 102e6e2721a20154892b1daacc50240d
3
+ ScriptedImporter:
4
+ internalIDToNameTable: []
5
+ externalObjects: {}
6
+ serializedVersion: 2
7
+ userData: ' (Unity.MLAgents.Demonstrations.DemonstrationSummary)'
8
+ assetBundleName:
9
+ assetBundleVariant:
10
+ script: {fileID: 11500000, guid: 7bd65ce151aaa4a41a45312543c56be1, type: 3}
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem_0.demo ADDED
Binary file (69.5 kB). View file
 
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem_0.demo.meta ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 181cb179f1a3e6246b03b33023c7dde2
3
+ ScriptedImporter:
4
+ internalIDToNameTable: []
5
+ externalObjects: {}
6
+ serializedVersion: 2
7
+ userData: ' (Unity.MLAgents.Demonstrations.DemonstrationSummary)'
8
+ assetBundleName:
9
+ assetBundleVariant:
10
+ script: {fileID: 11500000, guid: 7bd65ce151aaa4a41a45312543c56be1, type: 3}
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/bulletdodgedemo.demo ADDED
Binary file (26 kB). View file
 
Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/bulletdodgedemo.demo.meta ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 199173662a2d4ee48b5b90995ce28412
3
+ ScriptedImporter:
4
+ internalIDToNameTable: []
5
+ externalObjects: {}
6
+ serializedVersion: 2
7
+ userData: ' (Unity.MLAgents.Demonstrations.DemonstrationSummary)'
8
+ assetBundleName:
9
+ assetBundleVariant:
10
+ script: {fileID: 11500000, guid: 7bd65ce151aaa4a41a45312543c56be1, type: 3}
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor.meta ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 95fadbe24a716154b8fd85c4582021ac
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation.meta ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: cb6e73d0ddbcd4849a005dc4a8cfb10e
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #if UNITY_EDITOR
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.IO;
5
+ using System.Linq;
6
+ using Unity.InferenceEngine;
7
+ using UnityEditor;
8
+ using UnityEngine;
9
+
10
+ public static class AdditionalEvaluationTools
11
+ {
12
+ [Serializable] public class InputCase
13
+ {
14
+ public string model, sha256, label;
15
+ public int valid_length;
16
+ public float[] observations, actions, returns_to_go, expected_output;
17
+ public int[] timesteps;
18
+ }
19
+ [Serializable] public class Fixture { public InputCase[] cases; }
20
+ [Serializable] public class CaseResult { public string label; public float maximum_absolute_error; }
21
+ [Serializable] public class Verification
22
+ {
23
+ public string status, error, utc, gpu, graphics_api, unity_version;
24
+ public bool reset_tests_passed;
25
+ public EvaluationModelParityTests.Result original_model_parity;
26
+ public List<CaseResult> actual_input_cases = new List<CaseResult>();
27
+ }
28
+ [Serializable] public class LatencyRow
29
+ {
30
+ public string model, backend, sha256, scope;
31
+ public int warmup, measurements;
32
+ public double mean_ms, median_ms, p95_ms, minimum_ms, maximum_ms;
33
+ public double[] raw_ms;
34
+ }
35
+ [Serializable] public class LatencyReport
36
+ {
37
+ public string status, error, utc, gpu, graphics_api, unity_version, cpu, os, limitation;
38
+ public int memory_mb;
39
+ public List<LatencyRow> models = new List<LatencyRow>();
40
+ }
41
+
42
+ static Fixture Load()
43
+ {
44
+ if (!Application.isBatchMode || Application.isPlaying)
45
+ throw new InvalidOperationException("Run this explicit batch entry point outside Play mode.");
46
+ if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null)
47
+ throw new InvalidOperationException("A real graphics device is required.");
48
+ var result = JsonUtility.FromJson<Fixture>(File.ReadAllText(RevisionEvaluationBatch.Argument("-evaluationFixtures")));
49
+ if (result.cases == null || result.cases.Length != 96) throw new InvalidOperationException("Expected 96 real input cases.");
50
+ foreach (var c in result.cases)
51
+ if (Hash(c.model) != c.sha256) throw new InvalidOperationException("Model SHA differs: " + c.model);
52
+ return result;
53
+ }
54
+ static string Hash(string path)
55
+ {
56
+ using (var sha = System.Security.Cryptography.SHA256.Create())
57
+ return BitConverter.ToString(sha.ComputeHash(File.ReadAllBytes(path))).Replace("-", "").ToLowerInvariant();
58
+ }
59
+ static void Save(object value)
60
+ {
61
+ string path = RevisionEvaluationBatch.Argument("-evaluationReport");
62
+ if (File.Exists(path)) throw new IOException("Preserve prior report: " + path);
63
+ File.WriteAllText(path, JsonUtility.ToJson(value, true));
64
+ }
65
+ public static void Verify()
66
+ {
67
+ var report = new Verification { status = "failed", utc = DateTime.UtcNow.ToString("o"),
68
+ gpu = SystemInfo.graphicsDeviceName, graphics_api = SystemInfo.graphicsDeviceType.ToString(), unity_version = Application.unityVersion };
69
+ int code = 1;
70
+ try
71
+ {
72
+ var fixtures = Load();
73
+ EvaluationSmokeTests.Run();
74
+ report.reset_tests_passed = true;
75
+ report.original_model_parity = EvaluationModelParityTests.Run(
76
+ AssetDatabase.LoadAssetAtPath<ModelAsset>("Assets/Model/FinalModel/E_1_DT_C_5.onnx"),
77
+ RevisionEvaluationBatch.Argument("-originalFixtures"));
78
+ foreach (var group in fixtures.cases.GroupBy(c => c.model))
79
+ {
80
+ var asset = AssetDatabase.LoadAssetAtPath<ModelAsset>(group.Key);
81
+ using (var model = new DTModel(ref asset))
82
+ foreach (var c in group)
83
+ {
84
+ var output = model.Predict(c.observations, c.actions, c.returns_to_go, c.timesteps, 32);
85
+ if (output.Length != c.expected_output.Length) throw new InvalidOperationException("Output shape differs.");
86
+ float maximum = 0f;
87
+ for (int i = 0; i < output.Length; i++)
88
+ {
89
+ float error = Mathf.Abs(output[i] - c.expected_output[i]);
90
+ if (float.IsNaN(error) || error > 1e-4f + 1e-4f * Mathf.Abs(c.expected_output[i]))
91
+ throw new InvalidOperationException(c.label + " index=" + i + " error=" + error);
92
+ maximum = Mathf.Max(maximum, error);
93
+ }
94
+ report.actual_input_cases.Add(new CaseResult { label = c.label, maximum_absolute_error = maximum });
95
+ }
96
+ }
97
+ report.status = "passed"; code = 0;
98
+ Debug.Log("[Additional] Real-input ONNX Runtime/Unity parity passed: " + report.actual_input_cases.Count);
99
+ }
100
+ catch (Exception e) { report.error = e.ToString(); Debug.LogException(e); }
101
+ finally { Save(report); EditorApplication.Exit(code); }
102
+ }
103
+
104
+ static LatencyRow Measure(string path, string backend, Action<int> predict)
105
+ {
106
+ const int warmup = 200, n = 2000;
107
+ for (int i = 0; i < warmup; i++) predict(i);
108
+ var values = new double[n];
109
+ for (int i = 0; i < n; i++)
110
+ {
111
+ long start = System.Diagnostics.Stopwatch.GetTimestamp();
112
+ predict(i + warmup);
113
+ values[i] = 1000.0 * (System.Diagnostics.Stopwatch.GetTimestamp() - start) / System.Diagnostics.Stopwatch.Frequency;
114
+ }
115
+ var sorted = values.OrderBy(x => x).ToArray();
116
+ return new LatencyRow { model = path, backend = backend, sha256 = Hash(path), warmup = warmup, measurements = n,
117
+ scope = "Synchronous model call including tensor construction/scheduling/all action-output readback/disposal; excludes source array preparation and scene control",
118
+ raw_ms = values, mean_ms = values.Average(), median_ms = .5 * (sorted[n/2-1] + sorted[n/2]),
119
+ p95_ms = sorted[(int)Math.Ceiling(.95*n)-1], minimum_ms = sorted[0], maximum_ms = sorted[n-1] };
120
+ }
121
+ public static void MeasureLatency()
122
+ {
123
+ var report = new LatencyReport { status = "failed", utc = DateTime.UtcNow.ToString("o"),
124
+ gpu = SystemInfo.graphicsDeviceName, graphics_api = SystemInfo.graphicsDeviceType.ToString(), unity_version = Application.unityVersion,
125
+ cpu = SystemInfo.processorType, os = SystemInfo.operatingSystem, memory_mb = SystemInfo.systemMemorySize,
126
+ limitation = "Batch Editor, fixed hardware/backend, one measurement session. Not rendered FPS or deployment-build timing. PPO raw-model call excludes ML-Agents policy overhead, which is included in separate scene loop measurements." };
127
+ int code = 1;
128
+ try
129
+ {
130
+ var fixtures = Load();
131
+ foreach (string path in new[] { "Assets/Model/FinalModel/E_3_DT_S_100.onnx", "Assets/Model/FinalModel/BCModels/E_3_DT_BC_100.onnx" })
132
+ {
133
+ var inputs = fixtures.cases.Where(c => c.model == path).ToArray();
134
+ var asset = AssetDatabase.LoadAssetAtPath<ModelAsset>(path);
135
+ using (var model = new DTModel(ref asset))
136
+ report.models.Add(Measure(path, model.BackendName, i => {
137
+ var c = inputs[i % inputs.Length];
138
+ model.Predict(c.observations, c.actions, c.returns_to_go, c.timesteps, 32);
139
+ }));
140
+ }
141
+ string ppoPath = "Assets/Model/V12/V12 PPO.onnx";
142
+ var ppoAsset = AssetDatabase.LoadAssetAtPath<ModelAsset>(ppoPath);
143
+ var ppoObservations = fixtures.cases.Where(c => c.model.EndsWith("E_3_DT_S_100.onnx"))
144
+ .Select(c => c.observations.Skip(9*(c.valid_length-1)).Take(9).ToArray()).ToArray();
145
+ var mask = new[] { 1f, 1f, 1f, 1f };
146
+ using (var worker = new Worker(ModelLoader.Load(ppoAsset), Unity.InferenceEngine.DeviceType.CPU))
147
+ report.models.Add(Measure(ppoPath, worker.backendType.ToString(), i => {
148
+ using (var obs = new Tensor<float>(new TensorShape(1,9), ppoObservations[i % ppoObservations.Length]))
149
+ using (var masks = new Tensor<float>(new TensorShape(1,4), mask))
150
+ {
151
+ worker.SetInput("obs_0", obs); worker.SetInput("action_masks", masks); worker.Schedule();
152
+ var continuous = worker.PeekOutput("continuous_actions") as Tensor<float>;
153
+ var discrete = worker.PeekOutput("discrete_actions") as Tensor<int>;
154
+ if (continuous == null || discrete == null) throw new InvalidOperationException("PPO output type differs.");
155
+ continuous.DownloadToArray(); discrete.DownloadToArray();
156
+ }
157
+ }));
158
+ report.status = "passed"; code = 0;
159
+ }
160
+ catch (Exception e) { report.error = e.ToString(); Debug.LogException(e); }
161
+ finally { Save(report); EditorApplication.Exit(code); }
162
+ }
163
+ }
164
+ #endif
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs.meta ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 0432a35114a16284b91b3ceafb8bee02
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationModelParityTests.cs ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #if UNITY_EDITOR
2
+ using System;
3
+ using System.IO;
4
+ using Unity.InferenceEngine;
5
+ using UnityEditor;
6
+ using UnityEngine;
7
+
8
+ // Uses a synthetic episode processed by the supplied dataset_dt.py and model_dt.py.
9
+ // Validates the actual C# buffer, output selection, and Unity inference together.
10
+ public static class EvaluationModelParityTests
11
+ {
12
+ [Serializable] public class Step
13
+ {
14
+ public float[] observations, recorded_action;
15
+ public float rtg;
16
+ }
17
+ [Serializable] public class Fixture
18
+ {
19
+ public int timestep, valid_length;
20
+ public bool is_bc;
21
+ public float[] observations, actions, returns_to_go, expected_action;
22
+ public int[] timesteps;
23
+ }
24
+ [Serializable] public class FixtureSet
25
+ {
26
+ public string model, onnx_sha256;
27
+ public int seq_len;
28
+ public Step[] steps;
29
+ public Fixture[] cases;
30
+ }
31
+
32
+ [MenuItem("Tools/Revision Evaluation/Run Selected Model Parity Tests")]
33
+ public static void Run()
34
+ {
35
+ if (Application.isPlaying) throw new InvalidOperationException("Run parity tests outside Play mode.");
36
+ var asset = Selection.activeObject as ModelAsset;
37
+ if (asset == null) throw new InvalidOperationException("Select the supplied E_1_DT_C_5 ONNX asset in Project first.");
38
+ string path = EditorUtility.OpenFilePanel("Select E_1_DT_C_5_sequence_fixtures.json", "", "json");
39
+ if (string.IsNullOrEmpty(path)) return;
40
+ Run(asset, path);
41
+ }
42
+
43
+ [Serializable] public class Result
44
+ {
45
+ public int cases_passed;
46
+ public float maximum_absolute_error;
47
+ public string backend;
48
+ }
49
+
50
+ public static Result Run(ModelAsset asset, string path)
51
+ {
52
+ if (Application.isPlaying) throw new InvalidOperationException("Run parity tests outside Play mode.");
53
+ if (asset == null) throw new ArgumentNullException(nameof(asset));
54
+ var fixtures = JsonUtility.FromJson<FixtureSet>(File.ReadAllText(path));
55
+ if (fixtures == null || fixtures.cases == null || fixtures.cases.Length != 18 ||
56
+ fixtures.steps == null || fixtures.steps.Length != 1000 || fixtures.seq_len != 32)
57
+ throw new InvalidOperationException("Select the fixture file included in this package.");
58
+ string assetPath = AssetDatabase.GetAssetPath(asset);
59
+ if (!assetPath.EndsWith(".onnx", StringComparison.OrdinalIgnoreCase))
60
+ throw new InvalidOperationException("Select the ONNX asset itself.");
61
+ using (var sha = System.Security.Cryptography.SHA256.Create())
62
+ {
63
+ string hash = BitConverter.ToString(sha.ComputeHash(File.ReadAllBytes(assetPath))).Replace("-", "").ToLowerInvariant();
64
+ if (hash != fixtures.onnx_sha256)
65
+ throw new InvalidOperationException("Selected ONNX differs from the verified E_1_DT_C_5 file.");
66
+ }
67
+ int passed = 0; float maximumError = 0f;
68
+ string backend;
69
+ using (var model = new DTModel(ref asset))
70
+ {
71
+ backend = model.BackendName;
72
+ foreach (bool isBC in new[] { false, true })
73
+ {
74
+ var buffer = new DTSequenceBuffer(32, 9, 3);
75
+ var previous = new float[3];
76
+ for (int t = 0; t < fixtures.steps.Length; t++)
77
+ {
78
+ Step step = fixtures.steps[t];
79
+ buffer.AddStep(step.observations, previous, isBC ? 0f : step.rtg);
80
+ foreach (var c in fixtures.cases)
81
+ {
82
+ if (c.timestep != t || c.is_bc != isBC) continue;
83
+ string label = $"t={t}, zeroRTG={isBC}";
84
+ if (buffer.CurrentLength() != c.valid_length) throw new InvalidOperationException(label + ": valid length differs.");
85
+ CheckArray(buffer.GetObsArray(), c.observations, label + " observations", 1e-6f);
86
+ CheckArray(buffer.GetActArray(), c.actions, label + " actions", 1e-6f);
87
+ CheckArray(buffer.GetRTGArray(), c.returns_to_go, label + " RTG", 1e-6f);
88
+ var times = buffer.GetTimestepArray();
89
+ for (int j = 0; j < 32; j++)
90
+ if (times[j] != c.timesteps[j]) throw new InvalidOperationException(label + ": absolute timestep differs.");
91
+ var output = model.Predict(buffer.GetObsArray(), buffer.GetActArray(), buffer.GetRTGArray(), times, 32);
92
+ var selected = buffer.SelectCurrentAction(output);
93
+ maximumError = Mathf.Max(maximumError, CheckArray(selected, c.expected_action, label + " prediction", 1e-4f));
94
+ passed++;
95
+ }
96
+ previous = step.recorded_action;
97
+ }
98
+ }
99
+ }
100
+ if (passed != 18) throw new InvalidOperationException("Not every fixture was executed.");
101
+ Debug.Log($"[Evaluation] {passed} buffer/Unity/PyTorch parity cases passed. Maximum absolute prediction error={maximumError:G9}. Synthetic inputs; not an evaluation success rate.");
102
+ return new Result { cases_passed = passed, maximum_absolute_error = maximumError, backend = backend };
103
+ }
104
+
105
+ private static float CheckArray(float[] actual, float[] expected, string label, float tolerance)
106
+ {
107
+ if (actual == null || expected == null || actual.Length != expected.Length)
108
+ throw new InvalidOperationException(label + ": shape differs.");
109
+ float maxError = 0f;
110
+ for (int i = 0; i < actual.Length; i++)
111
+ {
112
+ float error = Mathf.Abs(actual[i] - expected[i]);
113
+ if (!EvaluationProtocol.Finite(actual[i]) || error > tolerance + tolerance * Mathf.Abs(expected[i]))
114
+ throw new InvalidOperationException($"{label}, index {i}: difference={error:G9}");
115
+ maxError = Mathf.Max(maxError, error);
116
+ }
117
+ return maxError;
118
+ }
119
+ }
120
+ #endif
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationModelParityTests.cs.meta ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: f34819e85eea181409a73db95d83142a
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationSmokeTests.cs ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #if UNITY_EDITOR
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.Reflection;
5
+ using UnityEditor;
6
+ using UnityEngine;
7
+
8
+ // Optional: copy this file into Assets/Editor, then use Tools/Revision Evaluation/Run Reset Tests.
9
+ // This does not load models or validate inference accuracy.
10
+ public static class EvaluationSmokeTests
11
+ {
12
+ [MenuItem("Tools/Revision Evaluation/Run Reset Tests")]
13
+ public static void Run()
14
+ {
15
+ if (Application.isPlaying) throw new InvalidOperationException("Run reset tests outside Play mode.");
16
+ var saved = UnityEngine.Random.state;
17
+ var root = new GameObject("EvaluationSmokeTest");
18
+ try
19
+ {
20
+ var barrel = new GameObject("Barrel");
21
+ barrel.transform.SetParent(root.transform, false);
22
+ var targets = new List<EnemyDummy>();
23
+ for (int i = 0; i < 20; i++)
24
+ {
25
+ var go = new GameObject("Target" + i);
26
+ go.transform.SetParent(root.transform, false);
27
+ targets.Add(go.AddComponent<EnemyDummy>());
28
+ }
29
+ string first = JsonUtility.ToJson(EvaluationProtocol.ResetEnvironment(
30
+ root.transform, barrel.transform, targets, 20, 42, 0));
31
+ EvaluationProtocol.AdvanceTargets(targets, 20);
32
+ string trajectory = JsonUtility.ToJson(targets[0].CaptureEvaluationState());
33
+ // Simulate a different preceding episode/model and unrelated random draws.
34
+ for (int i = 0; i < 100; i++) UnityEngine.Random.Range(0f, 100f);
35
+ EvaluationProtocol.ResetEnvironment(root.transform, barrel.transform, targets, 20, 42, 1);
36
+ EvaluationProtocol.AdvanceTargets(targets, 100);
37
+ string repeated = JsonUtility.ToJson(EvaluationProtocol.ResetEnvironment(
38
+ root.transform, barrel.transform, targets, 20, 42, 0));
39
+ Check(first == repeated, "Same episode seed must restore the same initial state.");
40
+ // First activation's Start must not randomize an already prepared evaluation target.
41
+ var beforeStart = JsonUtility.ToJson(targets[0].CaptureEvaluationState());
42
+ typeof(EnemyDummy).GetMethod("Start", BindingFlags.NonPublic | BindingFlags.Instance)
43
+ .Invoke(targets[0], null);
44
+ Check(beforeStart == JsonUtility.ToJson(targets[0].CaptureEvaluationState()), "Start changed evaluation state.");
45
+ EvaluationProtocol.AdvanceTargets(targets, 20);
46
+ Check(trajectory == JsonUtility.ToJson(targets[0].CaptureEvaluationState()), "Motion depends on prior episode state.");
47
+ string different = JsonUtility.ToJson(EvaluationProtocol.ResetEnvironment(
48
+ root.transform, barrel.transform, targets, 20, 42, 1));
49
+ Check(first != different, "Distinct episode seeds must produce distinct states.");
50
+ var stateBefore = UnityEngine.Random.state;
51
+ float expectedNextDraw = UnityEngine.Random.value;
52
+ UnityEngine.Random.state = stateBefore;
53
+ EvaluationProtocol.ResetEnvironment(root.transform, barrel.transform, targets, 10, 42, 0);
54
+ Check(expectedNextDraw == UnityEngine.Random.value, "Reset leaked global random state.");
55
+ Check(!targets[19].gameObject.activeSelf, "Inactive targets were not disabled.");
56
+ var buffer = new DTSequenceBuffer(2, 9, 3);
57
+ buffer.AddStep(new float[9], new float[3], 7f);
58
+ buffer.Clear();
59
+ Check(buffer.CurrentLength() == 0, "Sequence history survived Clear.");
60
+ buffer.AddStep(new float[9], new float[3], 0f);
61
+ Check(buffer.GetRTGArray()[0] == 0f, "RTG history survived Clear.");
62
+ buffer.AddStep(new float[9], new float[3], 1f);
63
+ buffer.AddStep(new float[9], new float[3], 2f);
64
+ Check(buffer.GetTimestepArray()[0] == 1 && buffer.GetTimestepArray()[1] == 2,
65
+ "Sliding windows must retain absolute episode timesteps.");
66
+ buffer.Clear();
67
+ float[] obs = new float[9]; obs[0] = 123f;
68
+ buffer.AddStep(obs, new float[3], 35f);
69
+ Check(buffer.CurrentLength() == 1 && buffer.GetObsArray()[0] == 123f &&
70
+ buffer.GetObsArray()[9] == 0f && buffer.GetRTGArray()[1] == 0f,
71
+ "Early episodes must right-pad future positions with zeros.");
72
+ float aim20 = EvaluationReward.AimScore(Vector3.forward,
73
+ Quaternion.Euler(0f,20f,0f) * Vector3.forward);
74
+ Check(!EvaluationReward.ShouldCast(true,true,aim20), "20-degree aim must not pass the training fire gate.");
75
+ Check(EvaluationReward.ShouldCast(true,true,1f), "Perfect aim should allow a requested shot.");
76
+ Check(!EvaluationReward.ShouldCast(false,true,1f), "No requested shot must not fire.");
77
+ Check(Mathf.Abs(EvaluationReward.Step(true,true,1f,-0.001f)-0.01f)<1e-7f,
78
+ "Perfect-aim firing step reward differs from training.");
79
+ Check(Mathf.Abs(EvaluationReward.Step(true,false,0f,-0.001f)+0.011f)<1e-7f,
80
+ "Unaligned non-firing penalty differs from training.");
81
+ Check(EvaluationReward.Terminal(0,1000,1000) == 2f,
82
+ "Success at the action limit must retain its victory bonus.");
83
+ Check(Mathf.Abs(EvaluationReward.Terminal(3,1000,1000)+0.03f)<1e-7f,
84
+ "Timeout must include the remaining-target penalty.");
85
+ Check(EvaluationReward.Terminal(3,999,1000) == 0f &&
86
+ !EvaluationReward.IsTerminal(3,999,1000), "Episode ended before its action limit.");
87
+ Check(Mathf.Abs(EvaluationReward.Step(true,true,0f,-0.001f)+0.002f)<1e-7f,
88
+ "Unaligned fire request must receive the training penalty without a raycast.");
89
+ buffer.Clear();
90
+ float[] previous = { 0.1f, -0.2f, 1f };
91
+ buffer.AddStep(obs, previous, 35f);
92
+ previous[2] = 0f; obs[0] = -1f;
93
+ Check(buffer.GetActArray()[2] == 1f && buffer.GetObsArray()[0] == 123f,
94
+ "Caller mutation corrupted stored history.");
95
+ float[] predictions = { 1f, 2f, 3f, 9f, 8f, 7f };
96
+ Check(buffer.SelectCurrentAction(predictions)[0] == 1f,
97
+ "Early prediction selected a padded future position.");
98
+ buffer.AddStep(new float[9], new float[3], 34f);
99
+ Check(buffer.SelectCurrentAction(predictions)[0] == 9f,
100
+ "Full-window prediction did not select the current position.");
101
+ Debug.Log("[Evaluation] Reset, motion replay, startup, RNG isolation and buffer smoke tests passed.");
102
+ }
103
+ finally
104
+ {
105
+ UnityEngine.Object.DestroyImmediate(root);
106
+ UnityEngine.Random.state = saved;
107
+ }
108
+ }
109
+
110
+ private static void Check(bool condition, string message)
111
+ {
112
+ if (!condition) throw new InvalidOperationException(message);
113
+ }
114
+ }
115
+ #endif
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationSmokeTests.cs.meta ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: 22355b2e48385834ea8de2593e418ad7
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/RevisionEvaluationBatch.cs ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #if UNITY_EDITOR
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.IO;
5
+ using System.Linq;
6
+ using Unity.InferenceEngine;
7
+ using Unity.MLAgents;
8
+ using Unity.MLAgents.Policies;
9
+ using UnityEditor;
10
+ using UnityEditor.SceneManagement;
11
+ using UnityEngine;
12
+ using UnityEngine.SceneManagement;
13
+
14
+ // Batch entry points run only when explicitly requested with -executeMethod.
15
+ [InitializeOnLoad]
16
+ public static class RevisionEvaluationBatch
17
+ {
18
+ const string Pending = "RevisionEvaluationBatch.pending";
19
+ const string Failure = "RevisionEvaluationBatch.failure";
20
+ const string Started = "RevisionEvaluationBatch.started";
21
+ const string Played = "RevisionEvaluationBatch.played";
22
+ const string Done = "RevisionEvaluationBatch.done";
23
+ const string PpoModel = "Assets/Model/V12/V12 PPO.onnx";
24
+ const string DtModel = "Assets/Model/FinalModel/E_1_DT_C_5.onnx";
25
+
26
+ [Serializable] public class Job
27
+ {
28
+ public string id, policy, scene, output, report;
29
+ public string[] models;
30
+ public int episodes = 2, targets = 10, seed = 42, max_steps = 1000;
31
+ public float initial_rtg = 35f;
32
+ public bool rtg_sensitivity, bc_legacy_rtg, record_timing;
33
+ public float time_scale_override;
34
+ public string experiment_kind;
35
+ public double timeout_seconds = 7200;
36
+ }
37
+ [Serializable] class Verification
38
+ {
39
+ public string status, error, unity_version, gpu, graphics_api, utc;
40
+ public bool compilation_passed, reset_tests_passed;
41
+ public EvaluationModelParityTests.Result parity;
42
+ }
43
+ [Serializable] class RunReport
44
+ {
45
+ public string status, error, utc, unity_version, gpu, graphics_api;
46
+ public Job job;
47
+ public string source_scene = "Assets/Scenes/Shooting.unity";
48
+ public string rig_source, ppo_model, ppo_inference_device;
49
+ public bool ppo_deterministic;
50
+ public int[] ppo_discrete_branch_sizes;
51
+ public float fixed_delta_time, time_scale, rotation_speed, shoot_distance, step_penalty;
52
+ public string[] files;
53
+ public double wall_seconds;
54
+ }
55
+
56
+ static RevisionEvaluationBatch()
57
+ {
58
+ EditorApplication.update += Monitor;
59
+ Application.logMessageReceived += OnLog;
60
+ }
61
+
62
+ public static string Argument(string name)
63
+ {
64
+ var args = Environment.GetCommandLineArgs();
65
+ for (int i = 0; i + 1 < args.Length; i++) if (args[i] == name) return args[i + 1];
66
+ throw new ArgumentException("Missing command-line argument " + name);
67
+ }
68
+
69
+ public static void Verify()
70
+ {
71
+ var result = new Verification {
72
+ status = "failed", unity_version = Application.unityVersion,
73
+ gpu = SystemInfo.graphicsDeviceName, graphics_api = SystemInfo.graphicsDeviceType.ToString(),
74
+ utc = DateTime.UtcNow.ToString("o"), compilation_passed = true
75
+ };
76
+ int code = 1;
77
+ try
78
+ {
79
+ if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null)
80
+ throw new InvalidOperationException("GPU verification requires a graphics device; omit -nographics.");
81
+ EvaluationSmokeTests.Run();
82
+ result.reset_tests_passed = true;
83
+ result.parity = EvaluationModelParityTests.Run(
84
+ AssetDatabase.LoadAssetAtPath<ModelAsset>(DtModel), Argument("-evaluationFixtures"));
85
+ result.status = "passed";
86
+ code = 0;
87
+ }
88
+ catch (Exception error) { result.error = error.ToString(); Debug.LogException(error); }
89
+ finally
90
+ {
91
+ WriteJson(Argument("-evaluationReport"), result);
92
+ EditorApplication.Exit(code);
93
+ }
94
+ }
95
+
96
+ public static void Run()
97
+ {
98
+ try
99
+ {
100
+ if (!Application.isBatchMode) throw new InvalidOperationException("Use this entry point only in batch mode on the working copy.");
101
+ Job job = JsonUtility.FromJson<Job>(File.ReadAllText(Argument("-evaluationJob")));
102
+ if (job == null || job.episodes < 1 || (job.targets != 10 && job.targets != 15 && job.targets != 20) ||
103
+ (job.policy != "PPO" && job.policy != "DT") || job.models == null || job.models.Length == 0)
104
+ throw new InvalidOperationException("Invalid evaluation job.");
105
+ if (Directory.Exists(job.output) && Directory.GetFiles(job.output, "*.json", SearchOption.AllDirectories).Length > 0)
106
+ throw new InvalidOperationException("Use a fresh result directory for every job.");
107
+ Directory.CreateDirectory(job.output);
108
+ var report = Prepare(job);
109
+ WriteJson(job.report, report);
110
+ SessionState.SetString(Pending, JsonUtility.ToJson(job));
111
+ SessionState.SetString(Failure, "");
112
+ SessionState.SetString(Started, DateTime.UtcNow.ToString("o"));
113
+ SessionState.SetBool(Played, false);
114
+ SessionState.SetBool(Done, false);
115
+ EditorApplication.isPlaying = true;
116
+ }
117
+ catch (Exception error)
118
+ {
119
+ Debug.LogException(error);
120
+ EditorApplication.Exit(1);
121
+ }
122
+ }
123
+
124
+ static RunReport Prepare(Job job)
125
+ {
126
+ if (job.time_scale_override > 0) Time.timeScale = job.time_scale_override;
127
+ var scene = EditorSceneManager.OpenScene("Assets/Scenes/Shooting.unity", OpenSceneMode.Single);
128
+ var ppo = scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<DroneAgent_For_Testing>(true)).Single();
129
+ string rigSource = HierarchyPath(ppo.transform);
130
+ var barrel = ppo.gunBarrel;
131
+ var targets = new List<EnemyDummy>(ppo.enemies);
132
+ if (barrel == null || targets.Count != 10 || targets.Any(e => e == null) || targets.Distinct().Count() != 10)
133
+ throw new InvalidOperationException("Expected the original PPO rig with ten unique targets.");
134
+ var behavior = ppo.GetComponent<BehaviorParameters>();
135
+ var requester = ppo.GetComponent<DecisionRequester>();
136
+ if (behavior == null || requester == null || AssetDatabase.GetAssetPath(behavior.Model) != PpoModel)
137
+ throw new InvalidOperationException("Original PPO rig/model differs from the inspected project.");
138
+ if (behavior.BrainParameters.VectorObservationSize != 9 ||
139
+ behavior.BrainParameters.ActionSpec.NumContinuousActions != 2 ||
140
+ !behavior.BrainParameters.ActionSpec.BranchSizes.SequenceEqual(new[] { 2, 2 }))
141
+ throw new InvalidOperationException("PPO observation/action contract mismatch.");
142
+
143
+ // Disable the original scene before configuring anything; activate only the common rig.
144
+ foreach (var go in scene.GetRootGameObjects()) go.SetActive(false);
145
+ foreach (var enemy in scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<EnemyDummy>(true)))
146
+ enemy.gameObject.SetActive(false);
147
+ foreach (var agent in scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<Agent>(true)))
148
+ {
149
+ agent.enabled = false;
150
+ if (agent.gameObject != ppo.gameObject) agent.gameObject.SetActive(false);
151
+ }
152
+ foreach (var controller in scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<DTController_For_Testing>(true)))
153
+ {
154
+ controller.enabled = false;
155
+ if (controller.gameObject != ppo.gameObject) controller.gameObject.SetActive(false);
156
+ }
157
+ foreach (var decision in scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<DecisionRequester>(true)))
158
+ decision.enabled = false;
159
+
160
+ // 15/20-target conditions extend the same collider/motion settings, preserving the first ten and their order.
161
+ for (int i = targets.Count; i < job.targets; i++)
162
+ {
163
+ var clone = UnityEngine.Object.Instantiate(targets[i % 10].gameObject, targets[i % 10].transform.parent);
164
+ clone.name = "RevisionTarget_" + i;
165
+ clone.SetActive(false);
166
+ targets.Add(clone.GetComponent<EnemyDummy>());
167
+ }
168
+ foreach (var enemy in targets)
169
+ {
170
+ if (enemy.GetComponent<Collider>() == null || (ppo.enemyLayer.value & (1 << enemy.gameObject.layer)) == 0 ||
171
+ enemy.GetComponentsInChildren<Rigidbody>(true).Any(r => !r.isKinematic) ||
172
+ enemy.GetComponentsInChildren<Animator>(true).Any(a => a.enabled) ||
173
+ enemy.GetComponents<MonoBehaviour>().Any(b => b != enemy && b.enabled))
174
+ throw new InvalidOperationException("Target has unexpected collision or external movement settings: " + enemy.name);
175
+ ActivateAncestors(enemy.transform.parent);
176
+ }
177
+ ActivateAncestors(ppo.transform);
178
+ foreach (var target in targets) target.gameObject.SetActive(false); // ResetEnvironment owns first activation.
179
+ foreach (var other in ppo.GetComponents<Agent>().Where(a => a != ppo).ToArray())
180
+ UnityEngine.Object.DestroyImmediate(other);
181
+
182
+ ppo.maxTestEpisodes = job.episodes;
183
+ ppo.maxEpisodeSteps = job.max_steps;
184
+ ppo.testSeed = job.seed;
185
+ ppo.EnemyCount = job.targets;
186
+ ppo.enemies = targets;
187
+ ppo.rotationSpeed = 100f;
188
+ ppo.shootDistance = 50f;
189
+ ppo.stepPenalty = EvaluationReward.DefaultStepPenalty;
190
+ ppo.outputFileName = "V12_PPO.json";
191
+ ppo.recordTiming = job.record_timing;
192
+ ppo.experimentKind = job.experiment_kind;
193
+ behavior.BehaviorType = BehaviorType.InferenceOnly;
194
+ requester.DecisionPeriod = 1;
195
+ requester.DecisionStep = 0;
196
+ requester.TakeActionsBetweenDecisions = false;
197
+
198
+ if (job.policy == "PPO")
199
+ {
200
+ if (job.models.Length != 1 || job.models[0] != PpoModel) throw new InvalidOperationException("Unexpected PPO model.");
201
+ ppo.enabled = true;
202
+ requester.enabled = true;
203
+ }
204
+ else
205
+ {
206
+ // No PPO component is allowed to initialize an Academy/policy during DT evaluation.
207
+ var go = ppo.gameObject;
208
+ var mask = ppo.enemyLayer;
209
+ UnityEngine.Object.DestroyImmediate(requester);
210
+ UnityEngine.Object.DestroyImmediate(ppo);
211
+ var controller = go.GetComponent<DTController_For_Testing>() ?? go.AddComponent<DTController_For_Testing>();
212
+ controller.modelAssets = job.models.Select(path => AssetDatabase.LoadAssetAtPath<ModelAsset>(path)).ToList();
213
+ if (controller.modelAssets.Any(model => model == null)) throw new InvalidOperationException("A requested model is missing.");
214
+ controller.gunBarrel = barrel;
215
+ controller.enemies = targets;
216
+ controller.enemyLayer = mask;
217
+ controller.EnemyCount = job.targets;
218
+ controller.maxTestEpisodes = job.episodes;
219
+ controller.maxEpisodeSteps = job.max_steps;
220
+ controller.testSeed = job.seed;
221
+ controller.initialRTG = job.initial_rtg;
222
+ controller.RTG_TEST = job.rtg_sensitivity;
223
+ controller.diagnosticLegacyBcRtg = job.bc_legacy_rtg;
224
+ controller.recordTiming = job.record_timing;
225
+ controller.experimentKind = job.experiment_kind;
226
+ controller.policyMode = DTController_For_Testing.PolicyMode.AutoFromModelName;
227
+ controller.rotationSpeed = 100f;
228
+ controller.shootDistance = 50f;
229
+ controller.stepPenalty = EvaluationReward.DefaultStepPenalty;
230
+ controller.traceEpisodes = 2;
231
+ controller.laserLine = controller.laserLineToEnemy = null;
232
+ controller.enabled = true;
233
+ }
234
+ // Keep the original time scale and fixed timestep; target simulation advances by action count.
235
+ var activeAgents = scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<Agent>(true))
236
+ .Where(a => a.isActiveAndEnabled).ToArray();
237
+ var activeDt = scene.GetRootGameObjects().SelectMany(g => g.GetComponentsInChildren<DTController_For_Testing>(true))
238
+ .Where(a => a.isActiveAndEnabled).ToArray();
239
+ if (activeAgents.Length != (job.policy == "PPO" ? 1 : 0) || activeDt.Length != (job.policy == "DT" ? 1 : 0))
240
+ throw new InvalidOperationException("The scene must have exactly one active evaluator.");
241
+ Directory.CreateDirectory(Path.GetDirectoryName(job.scene));
242
+ if (!EditorSceneManager.SaveScene(scene, job.scene)) throw new IOException("Could not save evaluation scene.");
243
+ AssetDatabase.SaveAssets();
244
+ return new RunReport {
245
+ status = "running", job = job, utc = DateTime.UtcNow.ToString("o"),
246
+ unity_version = Application.unityVersion, gpu = SystemInfo.graphicsDeviceName,
247
+ graphics_api = SystemInfo.graphicsDeviceType.ToString(), rig_source = rigSource,
248
+ ppo_model = AssetDatabase.GetAssetPath(behavior.Model),
249
+ ppo_inference_device = behavior.InferenceDevice.ToString(), ppo_deterministic = behavior.DeterministicInference,
250
+ ppo_discrete_branch_sizes = behavior.BrainParameters.ActionSpec.BranchSizes,
251
+ fixed_delta_time = Time.fixedDeltaTime, time_scale = Time.timeScale,
252
+ rotation_speed = 100f, shoot_distance = 50f, step_penalty = EvaluationReward.DefaultStepPenalty
253
+ };
254
+ }
255
+
256
+ static void ActivateAncestors(Transform transform)
257
+ {
258
+ if (transform == null) return;
259
+ ActivateAncestors(transform.parent);
260
+ transform.gameObject.SetActive(true);
261
+ }
262
+ static string HierarchyPath(Transform transform)
263
+ {
264
+ return transform.parent == null ? transform.name : HierarchyPath(transform.parent) + "/" + transform.name;
265
+ }
266
+ static void OnLog(string condition, string stack, LogType type)
267
+ {
268
+ if (string.IsNullOrEmpty(SessionState.GetString(Pending, ""))) return;
269
+ if (type == LogType.Error || type == LogType.Exception || type == LogType.Assert)
270
+ SessionState.SetString(Failure, condition + "\n" + stack);
271
+ }
272
+ static void Monitor()
273
+ {
274
+ string json = SessionState.GetString(Pending, "");
275
+ if (string.IsNullOrEmpty(json)) return;
276
+ try
277
+ {
278
+ var job = JsonUtility.FromJson<Job>(json);
279
+ if (EditorApplication.isPlaying) SessionState.SetBool(Played, true);
280
+ double elapsed = (DateTime.UtcNow - DateTime.Parse(SessionState.GetString(Started, ""), null,
281
+ System.Globalization.DateTimeStyles.RoundtripKind)).TotalSeconds;
282
+ var files = Directory.GetFiles(job.output, "*.json", SearchOption.AllDirectories);
283
+ string failure = SessionState.GetString(Failure, "");
284
+ if (elapsed > job.timeout_seconds) failure = "Evaluation exceeded the job timeout.";
285
+ bool finished = SessionState.GetBool(Played, false) && files.Length == job.models.Length;
286
+ if (SessionState.GetBool(Played, false) && !EditorApplication.isPlayingOrWillChangePlaymode && !SessionState.GetBool(Done, false))
287
+ failure = "Play mode stopped before expected results were saved.";
288
+ if (!finished && string.IsNullOrEmpty(failure)) return;
289
+ SessionState.SetBool(Done, true);
290
+ if (EditorApplication.isPlayingOrWillChangePlaymode)
291
+ {
292
+ if (!string.IsNullOrEmpty(failure)) SessionState.SetString(Failure, failure);
293
+ EditorApplication.isPlaying = false;
294
+ return;
295
+ }
296
+ var report = JsonUtility.FromJson<RunReport>(File.ReadAllText(job.report));
297
+ report.status = string.IsNullOrEmpty(failure) && finished ? "passed" : "failed";
298
+ report.error = failure;
299
+ report.files = files;
300
+ report.wall_seconds = elapsed;
301
+ WriteJson(job.report, report);
302
+ SessionState.EraseString(Pending);
303
+ EditorApplication.Exit(report.status == "passed" ? 0 : 1);
304
+ }
305
+ catch (Exception error)
306
+ {
307
+ SessionState.EraseString(Pending);
308
+ Debug.LogException(error);
309
+ EditorApplication.Exit(1);
310
+ }
311
+ }
312
+ static void WriteJson(string path, object value)
313
+ {
314
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
315
+ File.WriteAllText(path, JsonUtility.ToJson(value, true));
316
+ }
317
+ }
318
+ #endif
Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/RevisionEvaluationBatch.cs.meta ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: dd26dc65c3c7b2a4cb1925a8043b9afe
Upload/01_Source_Code/Unity_Evaluation/Assets/ML-Agents.meta ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: a4feaeeb0ad999f469361613b1ef969e
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
Upload/01_Source_Code/Unity_Evaluation/Assets/ML-Agents/Configs.meta ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fileFormatVersion: 2
2
+ guid: b7fd679a87b446e409cdf1e1f333624b
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant: