runops 2.7.0__tar.gz → 2.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. {runops-2.7.0 → runops-2.8.0}/PKG-INFO +1 -1
  2. {runops-2.7.0 → runops-2.8.0}/SPEC.md +3 -2
  3. {runops-2.7.0 → runops-2.8.0}/docs/guide.md +6 -1
  4. {runops-2.7.0 → runops-2.8.0}/docs/reference.md +4 -2
  5. {runops-2.7.0 → runops-2.8.0}/pyproject.toml +1 -1
  6. {runops-2.7.0 → runops-2.8.0}/src/runops/__init__.py +1 -1
  7. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/create.py +92 -9
  8. {runops-2.7.0 → runops-2.8.0}/src/runops/skills/runops-analyze/SKILL.md +9 -4
  9. runops-2.8.0/src/runops/skills/runops-analyze/example-report/README.md +97 -0
  10. runops-2.8.0/src/runops/skills/runops-analyze/example-report/out/fig1_maps.png +0 -0
  11. runops-2.8.0/src/runops/skills/runops-analyze/example-report/out/fig2_dependence.png +0 -0
  12. runops-2.8.0/src/runops/skills/runops-analyze/example-report/out/values.csv +6 -0
  13. runops-2.8.0/src/runops/skills/runops-analyze/example-report/report.html +149 -0
  14. runops-2.8.0/src/runops/skills/runops-analyze/example-report/scripts/build_html.py +72 -0
  15. runops-2.8.0/src/runops/skills/runops-analyze/example-report/scripts/make_figures.py +122 -0
  16. runops-2.8.0/src/runops/skills/runops-analyze/report-guide.md +91 -0
  17. {runops-2.7.0 → runops-2.8.0}/tests/test_safety.py +51 -0
  18. {runops-2.7.0 → runops-2.8.0}/tests/test_skills.py +43 -0
  19. {runops-2.7.0 → runops-2.8.0}/uv.lock +1 -1
  20. runops-2.7.0/.harness/manifest.toml +0 -15
  21. {runops-2.7.0 → runops-2.8.0}/.agents/skills/check/SKILL.md +0 -0
  22. {runops-2.7.0 → runops-2.8.0}/.agents/skills/improve-harness/SKILL.md +0 -0
  23. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/SKILL.md +0 -0
  24. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/agents/openai.yaml +0 -0
  25. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/examples/refactor-request.md +0 -0
  26. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/anti-patterns.md +0 -0
  27. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/api-compatibility.md +0 -0
  28. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/packaging-modernization.md +0 -0
  29. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/refactor-playbook.md +0 -0
  30. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/references/verification.md +0 -0
  31. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/api_surface_snapshot.py +0 -0
  32. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/import_smoke.py +0 -0
  33. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/inspect_python_package.py +0 -0
  34. {runops-2.7.0 → runops-2.8.0}/.agents/skills/python-package-refactor/scripts/refactor_quality_gate.py +0 -0
  35. {runops-2.7.0 → runops-2.8.0}/.agents/skills/release/SKILL.md +0 -0
  36. {runops-2.7.0 → runops-2.8.0}/.agents/skills/simulator-adapter/SKILL.md +0 -0
  37. {runops-2.7.0 → runops-2.8.0}/.agents/skills/spec-reviewer/SKILL.md +0 -0
  38. {runops-2.7.0 → runops-2.8.0}/.agents/skills/sync-docs/SKILL.md +0 -0
  39. {runops-2.7.0 → runops-2.8.0}/.claude/agents/spec-reviewer.md +0 -0
  40. {runops-2.7.0 → runops-2.8.0}/.claude/rules/architecture.md +0 -0
  41. {runops-2.7.0 → runops-2.8.0}/.claude/rules/dev-workflow.md +0 -0
  42. {runops-2.7.0 → runops-2.8.0}/.claude/rules/gotchas.md +0 -0
  43. {runops-2.7.0 → runops-2.8.0}/.claude/settings.json +0 -0
  44. {runops-2.7.0 → runops-2.8.0}/.claude/skills/check/SKILL.md +0 -0
  45. {runops-2.7.0 → runops-2.8.0}/.claude/skills/improve-harness/SKILL.md +0 -0
  46. {runops-2.7.0 → runops-2.8.0}/.claude/skills/python-package-refactor/SKILL.md +0 -0
  47. {runops-2.7.0 → runops-2.8.0}/.claude/skills/release/SKILL.md +0 -0
  48. {runops-2.7.0 → runops-2.8.0}/.claude/skills/simulator-adapter/SKILL.md +0 -0
  49. {runops-2.7.0 → runops-2.8.0}/.claude/skills/spec-reviewer/SKILL.md +0 -0
  50. {runops-2.7.0 → runops-2.8.0}/.claude/skills/sync-docs/SKILL.md +0 -0
  51. {runops-2.7.0 → runops-2.8.0}/.codex/README.md +0 -0
  52. {runops-2.7.0 → runops-2.8.0}/.codex/agents/spec-reviewer.toml +0 -0
  53. {runops-2.7.0 → runops-2.8.0}/.codex/config.toml +0 -0
  54. {runops-2.7.0 → runops-2.8.0}/.codex/rules/runops.rules +0 -0
  55. {runops-2.7.0 → runops-2.8.0}/.dockerignore +0 -0
  56. {runops-2.7.0 → runops-2.8.0}/.gitattributes +0 -0
  57. {runops-2.7.0 → runops-2.8.0}/.github/assets/conversation-dark.svg +0 -0
  58. {runops-2.7.0 → runops-2.8.0}/.github/assets/conversation-light.svg +0 -0
  59. {runops-2.7.0 → runops-2.8.0}/.github/assets/hero-dark.svg +0 -0
  60. {runops-2.7.0 → runops-2.8.0}/.github/assets/hero-light.svg +0 -0
  61. {runops-2.7.0 → runops-2.8.0}/.github/assets/make_readme_art.py +0 -0
  62. {runops-2.7.0 → runops-2.8.0}/.github/workflows/ci.yml +0 -0
  63. {runops-2.7.0 → runops-2.8.0}/.github/workflows/publish.yml +0 -0
  64. {runops-2.7.0 → runops-2.8.0}/.gitignore +0 -0
  65. {runops-2.7.0 → runops-2.8.0}/.pre-commit-config.yaml +0 -0
  66. {runops-2.7.0 → runops-2.8.0}/.vscode/settings.json +0 -0
  67. {runops-2.7.0 → runops-2.8.0}/AGENTS.md +0 -0
  68. {runops-2.7.0 → runops-2.8.0}/CLAUDE.md +0 -0
  69. {runops-2.7.0 → runops-2.8.0}/LICENSE +0 -0
  70. {runops-2.7.0 → runops-2.8.0}/README.md +0 -0
  71. {runops-2.7.0 → runops-2.8.0}/docs/adapter.md +0 -0
  72. {runops-2.7.0 → runops-2.8.0}/src/runops/adapters/__init__.py +0 -0
  73. {runops-2.7.0 → runops-2.8.0}/src/runops/adapters/base.py +0 -0
  74. {runops-2.7.0 → runops-2.8.0}/src/runops/adapters/generic.py +0 -0
  75. {runops-2.7.0 → runops-2.8.0}/src/runops/adapters/registry.py +0 -0
  76. {runops-2.7.0 → runops-2.8.0}/src/runops/case.py +0 -0
  77. {runops-2.7.0 → runops-2.8.0}/src/runops/cli.py +0 -0
  78. {runops-2.7.0 → runops-2.8.0}/src/runops/config.py +0 -0
  79. {runops-2.7.0 → runops-2.8.0}/src/runops/errors.py +0 -0
  80. {runops-2.7.0 → runops-2.8.0}/src/runops/harness.py +0 -0
  81. {runops-2.7.0 → runops-2.8.0}/src/runops/jobgen.py +0 -0
  82. {runops-2.7.0 → runops-2.8.0}/src/runops/launchers.py +0 -0
  83. {runops-2.7.0 → runops-2.8.0}/src/runops/legacy_adapters/__init__.py +0 -0
  84. {runops-2.7.0 → runops-2.8.0}/src/runops/legacy_adapters/beach.py +0 -0
  85. {runops-2.7.0 → runops-2.8.0}/src/runops/legacy_adapters/emses.py +0 -0
  86. {runops-2.7.0 → runops-2.8.0}/src/runops/manifest.py +0 -0
  87. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/__init__.py +0 -0
  88. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/analysis.py +0 -0
  89. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/common.py +0 -0
  90. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/listing.py +0 -0
  91. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/migrate.py +0 -0
  92. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/notes.py +0 -0
  93. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/progress.py +0 -0
  94. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/setup.py +0 -0
  95. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/skills.py +0 -0
  96. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/submit.py +0 -0
  97. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/sync.py +0 -0
  98. {runops-2.7.0 → runops-2.8.0}/src/runops/ops/tidy.py +0 -0
  99. {runops-2.7.0 → runops-2.8.0}/src/runops/project.py +0 -0
  100. {runops-2.7.0 → runops-2.8.0}/src/runops/runtime.py +0 -0
  101. {runops-2.7.0 → runops-2.8.0}/src/runops/skills/runops-check-status/SKILL.md +0 -0
  102. {runops-2.7.0 → runops-2.8.0}/src/runops/skills/runops-create-run/SKILL.md +0 -0
  103. {runops-2.7.0 → runops-2.8.0}/src/runops/skills/runops-maintain/SKILL.md +0 -0
  104. {runops-2.7.0 → runops-2.8.0}/src/runops/skills/runops-reference/SKILL.md +0 -0
  105. {runops-2.7.0 → runops-2.8.0}/src/runops/skills/runops-tidy/SKILL.md +0 -0
  106. {runops-2.7.0 → runops-2.8.0}/src/runops/slurm.py +0 -0
  107. {runops-2.7.0 → runops-2.8.0}/src/runops/util.py +0 -0
  108. {runops-2.7.0 → runops-2.8.0}/tests/__init__.py +0 -0
  109. {runops-2.7.0 → runops-2.8.0}/tests/conftest.py +0 -0
  110. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/.codex/rules/a.md +0 -0
  111. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/.runops/test-runs/T20260701-0001/receipt.toml +0 -0
  112. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/.runops/work/beach-x-abc/bin/beach +0 -0
  113. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/AGENTS.md +0 -0
  114. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/CLAUDE.md +0 -0
  115. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/beach/plane/beach.toml +0 -0
  116. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/beach/plane/case.toml +0 -0
  117. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/old/beach.toml +0 -0
  118. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/cases/old/case.toml +0 -0
  119. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/launchers.toml +0 -0
  120. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/research/CURRENT.md +0 -0
  121. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runops.toml +0 -0
  122. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/AGENTS.md +0 -0
  123. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/_archive/plane/R20260601-0001/manifest.toml +0 -0
  124. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/input/beach.toml +0 -0
  125. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/plane/R20260701-0001--a/manifest.toml +0 -0
  126. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/plane/R20260702-0001/manifest.toml +0 -0
  127. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/scan/R20260701-0002/manifest.toml +0 -0
  128. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/runs/scan/survey.toml +0 -0
  129. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/simulators.toml +0 -0
  130. {runops-2.7.0 → runops-2.8.0}/tests/fixtures/v1-project/site.toml +0 -0
  131. {runops-2.7.0 → runops-2.8.0}/tests/golden/mpi_standard.sh +0 -0
  132. {runops-2.7.0 → runops-2.8.0}/tests/golden/openmp_rsc.sh +0 -0
  133. {runops-2.7.0 → runops-2.8.0}/tests/test_adapters.py +0 -0
  134. {runops-2.7.0 → runops-2.8.0}/tests/test_analysis_layout.py +0 -0
  135. {runops-2.7.0 → runops-2.8.0}/tests/test_archive_series.py +0 -0
  136. {runops-2.7.0 → runops-2.8.0}/tests/test_build.py +0 -0
  137. {runops-2.7.0 → runops-2.8.0}/tests/test_dev_harness.py +0 -0
  138. {runops-2.7.0 → runops-2.8.0}/tests/test_jobgen.py +0 -0
  139. {runops-2.7.0 → runops-2.8.0}/tests/test_legacy_adapters.py +0 -0
  140. {runops-2.7.0 → runops-2.8.0}/tests/test_lifecycle.py +0 -0
  141. {runops-2.7.0 → runops-2.8.0}/tests/test_manifest_config.py +0 -0
  142. {runops-2.7.0 → runops-2.8.0}/tests/test_migrate.py +0 -0
  143. {runops-2.7.0 → runops-2.8.0}/tests/test_mv_naming.py +0 -0
  144. {runops-2.7.0 → runops-2.8.0}/tests/test_progress.py +0 -0
  145. {runops-2.7.0 → runops-2.8.0}/tests/test_project_scope.py +0 -0
  146. {runops-2.7.0 → runops-2.8.0}/tests/test_run_job.py +0 -0
  147. {runops-2.7.0 → runops-2.8.0}/tests/test_scratch_tidy.py +0 -0
  148. {runops-2.7.0 → runops-2.8.0}/tests/test_setup_misc.py +0 -0
  149. {runops-2.7.0 → runops-2.8.0}/tests/test_sweep.py +0 -0
  150. {runops-2.7.0 → runops-2.8.0}/tests/test_sync_evidence.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: runops
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Run ledger and Slurm job submission for HPC simulation projects
5
5
  Project-URL: Homepage, https://github.com/Nkzono99/runops
6
6
  Project-URL: Documentation, https://github.com/Nkzono99/runops/blob/main/docs/reference.md
@@ -729,14 +729,14 @@ runops は `init` と `update` で次を行う。
729
729
  runops は次の 6 skill だけを同梱し、simulator skill と同じ仕組みで展開する。
730
730
  利用者が覚えるコマンドは `runo init` だけとし、それ以外(simulator の追加、更新、v1 移行を含む)は
731
731
  エージェントへの依頼で行う。
732
- 各 SKILL.md は 60 行以内に収める。
732
+ 各 SKILL.md は 60 行以内に収める。詳しい指針や例が要る skill は、同じ directory に別 file として置き、SKILL.md から相対リンクで参照する。
733
733
 
734
734
  | skill | 内容 |
735
735
  |---|---|
736
736
  | `runops-create-run` | `new` / `sweep` の使い方。`--why` を必ず書く、scratch の使い分け |
737
737
  | `runops-check-status` | `sync` → `list` → `log` の順。失敗時の `retry` と `tidy --purge-work` |
738
738
  | `runops-tidy` | `tidy` を定期的に実行する判断基準。pin の使い方 |
739
- | `runops-analyze` | 依存する run の範囲で調査の置き場所を決め、script・結果・log・README をまとめる |
739
+ | `runops-analyze` | 依存する run の範囲で調査の置き場所を決め、script・結果・log・README をまとめる。読み手に渡す比較レポートの書き方は同梱の `report-guide.md` と `example-report/` に置く |
740
740
  | `runops-reference` | command 一覧(§7.3 の表をそのまま載せる) |
741
741
  | `runops-maintain` | simulator の追加、runops と simulator package の更新、`migrate-v1`、`doctor` の読み方 |
742
742
 
@@ -1017,6 +1017,7 @@ v2 の文書は次の 4 ファイルだけにする。
1017
1017
 
1018
1018
  | 項目 | 決定 | 理由 |
1019
1019
  |---|---|---|
1020
+ | 比較レポートの書き方(2026-11-10) | `runops-analyze` に執筆ガイド `report-guide.md` と模擬データの例 `example-report/` を同梱する。README.md を本文の正本とし、例の `scripts/build_html.py` が図を埋め込んだ `report.html` を作る。command やレポート生成機構は足さない | 差分図と条件の列挙が中心のレポートは読み手に伝わらず、元の分布を同じ尺度で並べ、問い・観測・解釈をつなげて書き直すと伝わった(#119)。書き方は判断を伴うので、手順と例として skill に置く |
1020
1021
  | 解析の配置 2.6.0(2026-10-23) | 依存する run の範囲に応じて run 内 `analysis/`、group の `_analysis/<topic>/`、project の `analysis/<topic>/` にまとめる。skill、log の tidy、group archive、context を対応させる(§9.1) | 報告・図・script・job log が分散・混在していた。group は既存の directory として扱い、新しい entity や command は足さない。既存ファイルの整理は移動一覧を提示してから行う |
1021
1022
  | job の cwd | run_dir(v1 と同じ)。Slurm log は `work/` | v1 adapter の path 規約と実 project の job.sh をそのまま使える |
1022
1023
  | adapter の追加 hook | `configure(sim_config)`、`job_prelude(run_dir)` | generic の `command` 取得と、EMSES の `rm -f *_0000.h5` など v1 の setup command を表す |
@@ -60,7 +60,8 @@ runops が扱う名詞は case、run、sweep の 3 つです。
60
60
  作った run の `manifest.toml` と `input/` は変更しません。入力を直したいときは新しい run を作ります。
61
61
  同じ入力の run が既にあると、作成時に警告が出ます。
62
62
  `new --from` は元の run の params を引き継ぎますが、入力ファイルは今の case から作ります。
63
- 元の run を作った後に case の入力ファイルを直していると、作成時に警告が出ます。
63
+ 元の run を作った後に case の入力ファイルを直して値が変わると、作成時の警告に変わる key と値が並びます。
64
+ 元の値のまま続けたいときは `--set` で指定します。
64
65
 
65
66
  ### 名前の付け方
66
67
 
@@ -232,6 +233,10 @@ topic は `speed-by-version` のように問いの内容で名付け、同じ問
232
233
 
233
234
  エージェントは `runops-analyze` skill を使い、調査の README と、そこへのリンクを含む
234
235
  `LOG.md` の判断を更新します。`notes/` は run に依存しない知見に使います。
236
+ 比較の結果を人に見せるレポートを頼むと(「この 3 条件を比較してレポートにまとめて」)、
237
+ エージェントは skill 同梱の執筆ガイドと例に沿って、問いと結論、条件を同じ尺度で並べた図、
238
+ 図の読み方と結果の意味、再現情報の順に書きます。README.md と、図を埋め込んで単体で
239
+ 読める `report.html` の両方ができます。
235
240
  解析の追加依存は `.runops/analysis-env/` に置き、重い読み込みや作図はサイトの規則に従って
236
241
  計算ノードで実行します。sbatch は調査 directory から投入し、事前に `logs/` を作って
237
242
  `#SBATCH -o logs/%x.%j.out` と `#SBATCH -e logs/%x.%j.err` を指定します。
@@ -129,8 +129,10 @@ program を起動せずに終わり、`stage = "setup"` と exit code を記録
129
129
 
130
130
  `submit` は executable を hash し直して attempt に記録し、作成時と違えば警告します。
131
131
  `new` と `sweep --apply` は同じ入力(`input_hash`)の run が既にあれば警告します。
132
- `new --from RUN` は、元の run を作った後に case の入力ファイルが変わっていれば警告します
133
- (入力は今の case から作るため)。どれも作成や投入は拒否しません。
132
+ `new --from RUN` は、元の run を作った後に case の入力ファイルが変わり、そのために入力の値が
133
+ 元の run と違うとき、違う key と値を警告に並べます(入力は今の case から作るため)。TOML 以外の
134
+ 入力は file 名だけを示します。`--set` した key と、adapter が `--continue` のために書いた key は
135
+ 並べません。どれも作成や投入は拒否しません。
134
136
 
135
137
  `submit`、`cancel`、`retry`、`sync` は lock の中で manifest を読み直してから更新します。
136
138
  同じ run を 2 回指定しても、複数のエージェントが同時に投入しても sbatch は 1 回です。
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runops"
7
- version = "2.7.0"
7
+ version = "2.8.0"
8
8
  description = "Run ledger and Slurm job submission for HPC simulation projects"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """runops: run ledger and Slurm job submission for HPC simulation projects."""
2
2
 
3
- __version__ = "2.7.0"
3
+ __version__ = "2.8.0"
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  import itertools
6
7
  import shutil
8
+ from collections.abc import Iterable
7
9
  from dataclasses import dataclass, field
8
10
  from datetime import datetime, timedelta
9
11
  from pathlib import Path
@@ -28,7 +30,17 @@ from runops.ops.common import SimContext, resolve_dest, sim_context
28
30
  from runops.ops.submit import write_job_for
29
31
  from runops.project import ARCHIVE_DIR, SCRATCH_DIR, Project
30
32
  from runops.runtime import resolve_runtime
31
- from runops.util import hash_tree, move_noreplace, now_iso, parse_value
33
+ from runops.util import (
34
+ hash_file,
35
+ hash_tree,
36
+ load_toml,
37
+ move_noreplace,
38
+ now_iso,
39
+ parse_value,
40
+ tomllib,
41
+ )
42
+
43
+ SHOWN_INPUT_CHANGES = 10
32
44
 
33
45
 
34
46
  @dataclass
@@ -40,6 +52,7 @@ class RunSpec:
40
52
  sweep: str = ""
41
53
  job: dict[str, Any] = field(default_factory=dict)
42
54
  job_changed: list[str] = field(default_factory=list)
55
+ input_changes: list[str] = field(default_factory=list) # set for a run derived from another
43
56
 
44
57
 
45
58
  @dataclass
@@ -118,6 +131,60 @@ def _job_keys(job: dict[str, Any]) -> dict[str, Any]:
118
131
  return {k: v for k, v in job.items() if k in JOB_KEYS}
119
132
 
120
133
 
134
+ def _flat(value: Any, prefix: str = "") -> dict[str, Any]:
135
+ """The leaves of a TOML document under dotted keys, arrays of tables by index."""
136
+ items: Iterable[tuple[Any, Any]]
137
+ if isinstance(value, dict):
138
+ items = value.items()
139
+ elif isinstance(value, list) and any(isinstance(x, dict) for x in value):
140
+ items = enumerate(value)
141
+ else:
142
+ return {prefix: value}
143
+ flat: dict[str, Any] = {}
144
+ for key, item in items:
145
+ flat.update(_flat(item, f"{prefix}.{key}" if prefix else str(key)))
146
+ return flat
147
+
148
+
149
+ def _input_values(input_dir: Path) -> dict[tuple[str, str], Any]:
150
+ """What a run's input files say: ``(file, dotted key)`` for TOML, ``(file, "")`` otherwise."""
151
+ values: dict[tuple[str, str], Any] = {}
152
+ for path in sorted(p for p in input_dir.rglob("*") if p.is_file()):
153
+ rel = path.relative_to(input_dir).as_posix()
154
+ document = None
155
+ if path.suffix == ".toml":
156
+ with contextlib.suppress(tomllib.TOMLDecodeError, UnicodeDecodeError):
157
+ document = load_toml(path)
158
+ if document is None:
159
+ values[(rel, "")] = hash_file(path)
160
+ else:
161
+ values.update({(rel, key): value for key, value in _flat(document).items()})
162
+ return values
163
+
164
+
165
+ def _differing(
166
+ before: dict[tuple[str, str], Any], after: dict[tuple[str, str], Any]
167
+ ) -> list[tuple[str, str]]:
168
+ return sorted(k for k in before.keys() | after.keys() if before.get(k) != after.get(k))
169
+
170
+
171
+ def _input_changes(
172
+ before: dict[tuple[str, str], Any],
173
+ after: dict[tuple[str, str], Any],
174
+ explained: set[str],
175
+ ) -> list[str]:
176
+ """What differs between two runs' inputs, leaving out the keys in *explained*."""
177
+ lines = []
178
+ for key in _differing(before, after):
179
+ rel, name = key
180
+ if not name:
181
+ lines.append(rel)
182
+ elif not any(name == k or name.startswith(f"{k}.") for k in explained):
183
+ old, new = (repr(side[key]) if key in side else "-" for side in (before, after))
184
+ lines.append(f"{name}: {old} -> {new}")
185
+ return lines
186
+
187
+
121
188
  def _diff(label: str, before: dict[str, Any], after: dict[str, Any]) -> str:
122
189
  keys = [k for k in after if before.get(k) != after[k]]
123
190
  keys += [k for k in before if k not in after]
@@ -263,14 +330,15 @@ def create_run(project: Project, request: CreateRequest) -> CreateResult:
263
330
  line = _diff("job vs case", dict(case.job), job)
264
331
  if line:
265
332
  result.changes.append(line)
266
- if source is not None:
267
- recorded = str(source.section("origin").get("case_input_hash", ""))
268
- if recorded and recorded != case.input_hash():
269
- # Inputs come from the case as it is now, not as it was for the source.
270
- result.warnings.append(
271
- f"case {case.name!r} input files changed since {source.id} was created; "
272
- f"{result.manifests[0].id} differs from it by more than the inherited params"
273
- )
333
+ if source is not None and spec.input_changes:
334
+ # Inputs come from the case as it is now, not as it was for the source.
335
+ shown = "; ".join(spec.input_changes[:SHOWN_INPUT_CHANGES])
336
+ hidden = len(spec.input_changes) - SHOWN_INPUT_CHANGES
337
+ more = f"; and {hidden} more" if hidden > 0 else ""
338
+ result.warnings.append(
339
+ f"case {case.name!r} input files changed since {source.id} was created; "
340
+ f"{result.manifests[0].id} differs from it beyond the params in: {shown}{more}"
341
+ )
274
342
  return result
275
343
 
276
344
 
@@ -483,8 +551,13 @@ def _populate(
483
551
  dst = input_dir / rel
484
552
  dst.parent.mkdir(parents=True, exist_ok=True)
485
553
  shutil.copy2(src, dst)
554
+ recorded = str(source.section("origin").get("case_input_hash", "")) if source else ""
555
+ case_moved = bool(recorded) and recorded != case.input_hash()
556
+ rendered: dict[tuple[str, str], Any] = {}
486
557
  try:
487
558
  ctx.adapter.render_inputs(dict(spec.params), stage)
559
+ if case_moved:
560
+ rendered = _input_values(input_dir)
488
561
  if request.continue_run and source is not None:
489
562
  extend = getattr(ctx.adapter, "extend_inputs", None)
490
563
  if not callable(extend):
@@ -497,6 +570,16 @@ def _populate(
497
570
  ) from None
498
571
  except (ValueError, KeyError, OSError) as exc:
499
572
  raise RunError(f"{run_id}: render_inputs failed: {exc}") from exc
573
+ if case_moved and source is not None:
574
+ values = _input_values(input_dir)
575
+ # Keys the user set, or the adapter wrote to continue, are not case changes.
576
+ explained = {
577
+ k.replace("[", ".").replace("]", "") for k in changed_keys(source.params, spec.params)
578
+ }
579
+ explained |= {name for _, name in _differing(rendered, values) if name}
580
+ spec.input_changes = _input_changes(
581
+ _input_values(source.run_dir / "input"), values, explained
582
+ )
500
583
 
501
584
  now = now_iso()
502
585
  run: dict[str, Any] = {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: runops-analyze
3
- description: Analyze or compare run results in a runops project. Use when plotting, investigating a series or sweep, writing an analysis report, or choosing where analysis scripts and outputs belong.
3
+ description: Analyze or compare run results in a runops project. Use when plotting, investigating a series or sweep, writing an analysis or comparison report for a reader, or choosing where analysis scripts and outputs belong.
4
4
  ---
5
5
 
6
6
  # Analyze run results
@@ -30,11 +30,16 @@ description: Analyze or compare run results in a runops project. Use when plotti
30
30
  In the script use `#SBATCH -o logs/%x.%j.out` and `#SBATCH -e logs/%x.%j.err`,
31
31
  the site's resource directives, and `srun` for the payload where required.
32
32
  Keep the working directory at the investigation root so `scripts/` and `out/` resolve.
33
- 5. Update the investigation README, then append the decision and its README link to
33
+ 5. When the result is a report someone will read (a comparison of runs, a dependence,
34
+ "what changed and why"), write it by [report-guide.md](report-guide.md): question and
35
+ answer first, original fields side by side on one scale, each figure explained
36
+ before and after, provenance and checks at the end, as `README.md` plus a
37
+ self-contained `report.html`. [example-report/](example-report/README.md) shows it.
38
+ 6. Update the investigation README, then append the decision and its README link to
34
39
  `LOG.md`. Put reusable formulas, methods and pitfalls in `notes/<topic>.md`.
35
- 6. `runo tidy --only analysis-logs` previews old job logs, retaining the newest pair per
40
+ 7. `runo tidy --only analysis-logs` previews old job logs, retaining the newest pair per
36
41
  job name in each logs directory. Apply with `runo tidy --apply --only analysis-logs`.
37
42
  Archiving a whole group directory also moves its `_analysis/`; a partial archive
38
43
  leaves shared analysis with the remaining runs. `restore RUN` restores only that run.
39
- 7. For an existing project's scattered files, show source → destination paths before
44
+ 8. For an existing project's scattered files, show source → destination paths before
40
45
  moving anything, then fix affected script paths and document links. Keep results.
@@ -0,0 +1,97 @@
1
+ # ヒーターを離して置いても、センサーの温度は上がるか
2
+
3
+ > これは `runops-analyze` skill に同梱した書き方の見本です。数値は
4
+ > `scripts/make_figures.py` が作る模擬データで、実在の計算結果ではありません。
5
+ > run id などの再現情報も、書く場所を示すための例です。
6
+
7
+ ## 調べたことと結果
8
+
9
+ 金属板の中央に温度センサーがあり、板の上にヒーターを 1 個置きます。ヒーターを
10
+ センサーの真上に置けばセンサーが温まるのは当然ですが、知りたいのは、ヒーターを
11
+ センサーから離して置いたときです。センサーの温度は上がるのか。上がるなら、それは
12
+ センサーだけが局所的に熱せられているのか、板の広い範囲が温まった結果なのか。
13
+
14
+ ヒーターの出力(10 W)と板の条件は固定し、ヒーターとセンサーの距離だけを 0 cm から
15
+ 12 cm まで変えました。比較の基準は、ヒーターを切った計算です。
16
+
17
+ 結果は次のとおりです。ヒーターを 8 cm 離しても、センサーの温度は基準より約 8 K
18
+ 高くなりました。このときセンサーとその周囲の温度はほぼ同じで、センサーだけが熱い
19
+ 状態にはなっていません。離れたヒーターは、板の広い範囲を温めることを通して
20
+ センサーの温度を上げています。
21
+
22
+ ## 温度分布を並べて見る
23
+
24
+ まず、板全体の温度がどう変わるかを見ます。図 1 は、ヒーターを切った場合、センサーの
25
+ 真上に置いた場合、8 cm 離して置いた場合の温度分布を、同じ範囲と同じ色尺度で並べた
26
+ ものです。色が明るいほど高温で、白い丸がセンサー、水色の三角がヒーターの位置です。
27
+ 白い破線の円(半径 3 cm)は、あとで「センサーの周囲」として温度を平均する範囲を
28
+ 示しています。どの分布も、計算が定常に達した後の最後の 1000 step の平均です。
29
+
30
+ ![図 1: ヒーターなし、センサー直上、8 cm 離した場合の温度分布](out/fig1_maps.png)
31
+
32
+ 左の図は一様に 20 °C で、これが比較の基準です。中央の図では、ヒーターを中心に
33
+ 温度が上がり、センサーは 50 °C と板の中で最も高温になっています。右の図では高温の
34
+ 中心がヒーターとともに右へ移りますが、温まった範囲はセンサーの位置まで広がって
35
+ おり、センサーは 27.9 °C を示します。破線の円の中を見ると、中央の図では円の中心が
36
+ 周りより明らかに明るいのに対し、右の図では円の中に目立った明暗がありません。
37
+
38
+ つまり、離れたヒーターでもセンサーの温度は上がりますが、その上がり方は直上に置いた
39
+ 場合と違い、センサーの位置が特別に熱いわけではないように見えます。ただし図 1 は
40
+ 2 つの距離しか示していません。次に、距離を細かく変えてこの見方を確かめます。
41
+
42
+ ## 距離を変えたときの変化
43
+
44
+ 図 2 は、ヒーターとセンサーの距離を横軸にとり、2 つの量を示しています。(a) は
45
+ センサーの温度が基準(ヒーターなし)からどれだけ上がったか、(b) はセンサーの温度から
46
+ 周囲(図 1 の破線の円周上)の平均温度を引いた値です。(b) が正で大きいほど、
47
+ センサーの位置だけが局所的に熱いことを表し、0 に近ければセンサーは周囲と同じ温度
48
+ です。赤い点は図 1 で分布を示した 2 つの距離です。
49
+
50
+ ![図 2: センサーの温度上昇と、周囲との温度差の距離依存性](out/fig2_dependence.png)
51
+
52
+ (a) の温度上昇は距離とともにゆるやかに小さくなり、8 cm で 7.9 K、12 cm でも 4.1 K
53
+ 残ります。一方、(b) の周囲との差は 0 cm の 11.8 K から急に小さくなり、4 cm で 0.6 K、
54
+ 8 cm 以上ではほぼ 0(−0.1 K)です。
55
+
56
+ | 距離 [cm] | センサーの温度上昇 [K] | センサー − 周囲 [K] |
57
+ |---|---|---|
58
+ | 0 | 30.0 | 11.8 |
59
+ | 2 | 21.5 | 3.9 |
60
+ | 4 | 15.4 | 0.6 |
61
+ | 8 | 7.9 | −0.1 |
62
+ | 12 | 4.1 | −0.1 |
63
+
64
+ 2 つの量が違う速さで小さくなることが、最初の問いへの答えです。ヒーターを 4 cm 以上
65
+ 離すと、センサーの温度はまだ 15 K 上がっているのに、周囲との差はほとんど消えます。
66
+ この距離では、センサーはヒーターに直接熱せられているのではなく、温まった板と同じ
67
+ 温度になっています。
68
+
69
+ この結果からは判断できないこともあります。保存したのは板の温度分布だけなので、
70
+ 熱が板の中を伝わってセンサーに届いたのか、板の上の空気を通って届いたのかは
71
+ 区別できません。「板の広い範囲が温まる」という説明は分布の形から言えることで、
72
+ 熱の経路ごとの寄与を分けた結果ではありません。
73
+
74
+ ## 再現情報
75
+
76
+ | 条件 | run | 計算コード | 平均した区間 |
77
+ |---|---|---|---|
78
+ | ヒーターなし(基準) | `R20261101-0001` | plate-sim 1.4.2 | step 9001–10000 |
79
+ | 距離 0, 2, 4, 8, 12 cm | `R20261101-0002` – `0006` | plate-sim 1.4.2 | step 9001–10000 |
80
+
81
+ - 6 本とも同じ版・同じ格子・同じ step 数の計算で、違いはヒーターの有無と位置だけです。
82
+ センサーの温度が最後の 1000 step で 0.01 K 以内に収まっていることを確認し、
83
+ その区間を平均しました。
84
+ - 図に平滑化やマスクは使っていません。「周囲」は、センサーを中心とする半径 3 cm の
85
+ 円周上の 720 点の平均です。
86
+ - 図と表は `scripts/make_figures.py` が作ります。図 2 と表の元の数値は
87
+ [out/values.csv](out/values.csv) にあります。
88
+ - `report.html` は `scripts/build_html.py` がこの README から作る、図を埋め込んだ
89
+ 単体で読めるファイルです。
90
+
91
+ ## 確認記録
92
+
93
+ - ファイル: `python scripts/build_html.py --check` が、`report.html` が README と一致し、
94
+ 画像 2 枚を埋め込んでいると報告しました。
95
+ - 図の内容: 2 枚の PNG を開き、軸ラベル、色尺度、各列の条件名と温度の文字が読めること、
96
+ 表の値と図 2 の点が一致することを確認しました。
97
+ - ブラウザ表示: `report.html` をブラウザで開いた確認はしていません。
@@ -0,0 +1,6 @@
1
+ offset_cm,sensor_degC,rise_K,ring_mean_degC,sensor_minus_ring_K
2
+ 0.0,50.0,30.0,38.2,11.8
3
+ 2.0,41.5,21.5,37.63,3.86
4
+ 4.0,35.4,15.4,34.79,0.62
5
+ 8.0,27.91,7.91,28.02,-0.11
6
+ 12.0,24.06,4.06,24.19,-0.13