Upload 765 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +15 -0
- Upload/01_Source_Code/Notebooks/DicisionTransformer.ipynb +0 -0
- Upload/01_Source_Code/Notebooks/Performance_Analysis_RTG.ipynb +0 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/Run-Unity.ps1 +32 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_diagnostics.py +63 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_reward_components.py +44 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/analyze_statistics.py +116 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/audit_additional.py +124 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/fetch_reference_weights.py +37 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/inspect_public_data.py +45 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/prepare_additional.py +72 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/progress.py +18 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/report_additional.py +100 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/run_additional.py +93 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/validate_additional.py +185 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionAdditional_20260911/Tools/verify_pytorch_weights.py +36 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/Run-Unity.ps1 +28 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/capture_changes.py +52 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/finalize_report.py +57 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/prepare_jobs.py +76 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/progress.py +28 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/run_phase.py +93 -0
- Upload/01_Source_Code/Python_Evaluation/RevisionEvaluation_Local_20260910/Tools/summarize_results.py +58 -0
- Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tests/test_validate_evaluation.py +78 -0
- Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tools/validate_evaluation.py +180 -0
- Upload/01_Source_Code/Python_Evaluation/ShootingGame_IDE_Handoff/evaluation_code/Tools/verify_model_pair.py +71 -0
- Upload/01_Source_Code/Python_Training/dataset_dt.py +193 -0
- Upload/01_Source_Code/Python_Training/finetuning_ext_RLStep.py +215 -0
- Upload/01_Source_Code/Python_Training/model_dt.py +85 -0
- Upload/01_Source_Code/Python_Training/train_sequential_ext_RLStep.py +181 -0
- Upload/01_Source_Code/Python_Training/train_sequential_ext_RLStep_For_BC.py +183 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations.meta +8 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem.demo +0 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem.demo.meta +10 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem_0.demo +0 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/ShootingAgentDem_0.demo.meta +10 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/bulletdodgedemo.demo +0 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Demonstrations/bulletdodgedemo.demo.meta +10 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor.meta +8 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation.meta +8 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs +164 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/AdditionalEvaluationTools.cs.meta +2 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationModelParityTests.cs +120 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationModelParityTests.cs.meta +2 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationSmokeTests.cs +115 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/EvaluationSmokeTests.cs.meta +2 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/RevisionEvaluationBatch.cs +318 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/Editor/RevisionEvaluation/RevisionEvaluationBatch.cs.meta +2 -0
- Upload/01_Source_Code/Unity_Evaluation/Assets/ML-Agents.meta +8 -0
- 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:
|