From d1ee871ee4b34eb2a675b1f54cb424e734a42d82 Mon Sep 17 00:00:00 2001 From: Yun Date: Fri, 11 Sep 2026 15:11:06 +0900 Subject: [PATCH] Measure the EFIT domain and grid: neither recovers a slice (#459) The #171 baseline loses most of its plasma slices to two failures that read as grid statements: `bound` cannot close a contour, and `findax` refuses a separatrix point within two cells of the box edge. This runs #459's 2x2 over the three reference discharges -- the routine box against a reduced one, at 129x129 against 129x257 -- to measure how much of that loss the domain and the grid account for. What EFIT's grid contract is, from the source: - nw and nh are argv(1) and argv(2) (efit.F90:93-104); a rectangular grid needs no rebuild. - The Green table name encodes the grid and only the grid (tables.F90:34). - EFIT has no notion of the box of its own: it reads rgrid/zgrid out of the table (tables.F90:158). So two tables for the same grid and different boxes are named identically and a wrong one is undetectable from EFIT's output. `verify_table` reads the EFUND manifest and refuses a mismatch before any case runs. Over 82 plasma slices: case box grid produced accepted collapsed findax A R .05-1.2, Z +-1.5 129x129 46 30 29 2 B R .05-1.0, Z +-1.35 129x129 36 23 29 13 C R .05-1.2, Z +-1.5 129x257 44 28 29 4 D R .05-1.0, Z +-1.35 129x257 36 23 29 13 Neither lever recovers a slice. The reduced box costs ten equilibria, and it fails exactly as the box predicts: bringing the edge in takes findax's off-grid rejections from 2 to 13. B and D agree on every count on every shot. The 29-slice collapse block is untouched in all four cases; with #588 having ruled out the seed, it is not a discretisation problem either. Halving the vertical cell moves every global quantity by under 0.3%, chi-square not at all; the axis height moves by exactly half the coarse Z cell, which is quantisation rather than a different equilibrium. All four cases are judged against one acceptance envelope (case A's), since aminor_min is a resolution floor and would otherwise move with the grid. Also fixed here, because it changed these numbers: parse_slices delimited slices on the iteration counter alone, so a slice that `bound` rejects before its first iteration was dropped and its error handed to the previous slice. It now opens a slice when a solver error names a new time. The reference set has 82 plasma slices, not the 77 the earlier studies reported; their relative comparisons stand, their absolute counts predate this. Open: case A produces 46 equilibria where the merged baseline produced 31. The runs differ in both the Green table and the mhdin.dat geometry (fresh vs packaged), so which one matters is not established; it needs its own A/B. regenerate_legacy_table.py gains --rleft/--rright/--zbotto/--ztop; EFUNDConfig already carried them. Co-Authored-By: Claude Opus 5 --- test/data/efit_domain_grid.json | 3673 +++++++++++++++++ test/test_efit_domain_grid.py | 248 ++ test/test_efit_termination_baseline.py | 36 + workflow/efit_numerics/README.md | 114 + .../efit_numerics/baseline_termination.py | 22 +- workflow/efit_numerics/domain_grid.py | 674 +++ .../efit_tables/regenerate_legacy_table.py | 20 + 7 files changed, 4783 insertions(+), 4 deletions(-) create mode 100644 test/data/efit_domain_grid.json create mode 100644 test/test_efit_domain_grid.py create mode 100644 workflow/efit_numerics/domain_grid.py diff --git a/test/data/efit_domain_grid.json b/test/data/efit_domain_grid.json new file mode 100644 index 00000000..d27fb5c0 --- /dev/null +++ b/test/data/efit_domain_grid.json @@ -0,0 +1,3673 @@ +{ + "acceptance_envelope": { + "aminor_min": 4.492187499999999, + "sha256": "2fb1270bd4a50370f0471b3b31a065c10ff2d262760ad3abf6b95aa65be19f66" + }, + "cases": { + "A": { + "cell_mm": [ + 8.984375, + 23.4375 + ], + "domain": [ + 0.05, + 1.2, + -1.5, + 1.5 + ], + "grid": [ + 129, + 129 + ], + "role": "the routine configuration", + "table": { + "directory": "/private/tmp/claude-501/-Users-yun-git-vaft--claude-worktrees-workflow-chain-enhancement-c328b7/39ea1cb1-126a-47f5-be02-a50f633690a4/scratchpad/tables/a_129x129", + "efund_config_sha256": "b98479ce0d52076be9652d5f43f4a507a37ab8858c5205dd149928a95ea25159", + "generated_at": "2026-09-07T09:06:53+00:00", + "own_aminor_min_cm": 4.492187499999999, + "staged": "/private/tmp/claude-501/dg/A", + "table_identity": "6d089af6e8857471fb70777e5253d07734a2ba4bf17ffb9e2e3d93a2d303e66b" + } + }, + "B": { + "cell_mm": [ + 7.421875, + 21.09375 + ], + "domain": [ + 0.05, + 1.0, + -1.35, + 1.35 + ], + "grid": [ + 129, + 129 + ], + "role": "the domain alone", + "table": { + "directory": "/private/tmp/claude-501/-Users-yun-git-vaft--claude-worktrees-workflow-chain-enhancement-c328b7/39ea1cb1-126a-47f5-be02-a50f633690a4/scratchpad/tables/b_129x129_reduced", + "efund_config_sha256": "7fd6178d61436fdfd3bdd30f8d4ada198485af81e80b05f179e3a8e19315a1ba", + "generated_at": "2026-09-07T09:09:08+00:00", + "own_aminor_min_cm": 3.7109375, + "staged": "/private/tmp/claude-501/dg/B", + "table_identity": "2438a5ad0580402129e9a57c50210921e7268115bc2cbaf4b0fbcc83383c23ba" + } + }, + "C": { + "cell_mm": [ + 8.984375, + 11.71875 + ], + "domain": [ + 0.05, + 1.2, + -1.5, + 1.5 + ], + "grid": [ + 129, + 257 + ], + "role": "the cell aspect ratio alone", + "table": { + "directory": "/private/tmp/claude-501/-Users-yun-git-vaft--claude-worktrees-workflow-chain-enhancement-c328b7/39ea1cb1-126a-47f5-be02-a50f633690a4/scratchpad/tables/c_129x257", + "efund_config_sha256": "0b42fbf877f195a2805cdf2c661233785aa4f3e05327bd8a2e91510b25e609a9", + "generated_at": "2026-09-07T09:01:58+00:00", + "own_aminor_min_cm": 4.492187499999999, + "staged": "/private/tmp/claude-501/dg/C", + "table_identity": "8c1abc12ecbe6d5f3f03be2b5638ff61d3b647ceb28ff9af73f3352430285f5e" + } + }, + "D": { + "cell_mm": [ + 7.421875, + 10.546875 + ], + "domain": [ + 0.05, + 1.0, + -1.35, + 1.35 + ], + "grid": [ + 129, + 257 + ], + "role": "both", + "table": { + "directory": "/private/tmp/claude-501/-Users-yun-git-vaft--claude-worktrees-workflow-chain-enhancement-c328b7/39ea1cb1-126a-47f5-be02-a50f633690a4/scratchpad/tables/d_129x257_reduced", + "efund_config_sha256": "77eb73f973942f73b55cae8f04f652e5132078893d5bc2faa416772e6b062339", + "generated_at": "2026-09-07T09:14:51+00:00", + "own_aminor_min_cm": 3.7109375, + "staged": "/private/tmp/claude-501/dg/D", + "table_identity": "0c1a6095edc8df90a8f8932febcfa6e17678102998300587ec8f42c5fc01bf58" + } + } + }, + "run_at": "2026-09-07T09:26:27+00:00", + "schema_version": 1, + "shots": { + "39915": { + "cases": { + "A": { + "returncode": 0, + "seconds": 13.776354000030551, + "slices": [ + { + "accepted": false, + "afile": null, + "chi2_final": 9.51e-10, + "chi2_initial": 94.5, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3131, + "iconvr": null, + "iterations_n": 12, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 307 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 4.76e-09, + "chi2_initial": 141.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.327, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 308 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 1.34e-08, + "chi2_initial": 172.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3251, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 309 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 1.96e-08, + "chi2_initial": 186.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3557, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 310 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.81e-08, + "chi2_initial": 207.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3567, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 311 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 3.91e-08, + "chi2_initial": 211.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3332, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 312 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 3.97e-08, + "chi2_initial": 203.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3358, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 313 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 4.03e-08, + "chi2_initial": 190.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3231, + "iconvr": null, + "iterations_n": 12, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 314 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 4.24e-08, + "chi2_initial": 173.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.2895, + "iconvr": null, + "iterations_n": 15, + "solver_errors": [ + { + "detail": "Number of contour points greater than max allowed", + "routine": "bound" + } + ], + "time_ms": 315 + }, + { + "accepted": false, + "afile": { + "aminor": 29.3358917, + "area": 4080.94595, + "betap": 0.0156797323, + "chisq": 202.15022, + "elong": 1.54248409, + "jflag": 0, + "li": 0.646005672, + "q95": 8.74047169, + "qstar": 6.49349178, + "rm": 42.7343595, + "terror": 0.00885866706, + "volume": 947504.857, + "zm": 2.34371463 + }, + "chi2_final": 2.26e-09, + "chi2_initial": 202.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008859, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 316 + }, + { + "accepted": false, + "afile": { + "aminor": 26.7894415, + "area": 3445.75363, + "betap": 0.0125699989, + "chisq": 161.747301, + "elong": 1.54667367, + "jflag": 0, + "li": 0.647117578, + "q95": 7.69867499, + "qstar": 6.04249786, + "rm": 40.0390436, + "terror": 0.00851664759, + "volume": 758045.305, + "zm": 2.34370197 + }, + "chi2_final": 3.15e-09, + "chi2_initial": 162.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008517, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 317 + }, + { + "accepted": false, + "afile": { + "aminor": 25.3509406, + "area": 3092.27656, + "betap": 0.0109667931, + "chisq": 90.0124497, + "elong": 1.54368053, + "jflag": 0, + "li": 0.649832564, + "q95": 7.18903956, + "qstar": 5.79792319, + "rm": 38.2421916, + "terror": 0.00837283514, + "volume": 657771.278, + "zm": 5.47279739e-05 + }, + "chi2_final": 3.69e-09, + "chi2_initial": 90.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008373, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 318 + }, + { + "accepted": true, + "afile": { + "aminor": 24.2581778, + "area": 2832.06242, + "betap": 0.0098390405, + "chisq": 74.4342174, + "elong": 1.54036735, + "jflag": 1, + "li": 0.652537561, + "q95": 6.77965323, + "qstar": 5.58414084, + "rm": 37.3437183, + "terror": 0.00848255656, + "volume": 586341.503, + "zm": 5.14091095e-05 + }, + "chi2_final": 4.32e-09, + "chi2_initial": 74.4, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008483, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 319 + }, + { + "accepted": true, + "afile": { + "aminor": 23.4676651, + "area": 2638.78131, + "betap": 0.00907788441, + "chisq": 43.7501633, + "elong": 1.52939666, + "jflag": 1, + "li": 0.655992339, + "q95": 6.43658515, + "qstar": 5.35232177, + "rm": 36.4452812, + "terror": 0.00838417199, + "volume": 535490.042, + "zm": 4.64090078e-05 + }, + "chi2_final": 4.72e-09, + "chi2_initial": 43.8, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008384, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 320 + }, + { + "accepted": true, + "afile": { + "aminor": 22.1366279, + "area": 2339.45192, + "betap": 0.00787220086, + "chisq": 40.9646649, + "elong": 1.51968631, + "jflag": 1, + "li": 0.661064378, + "q95": 6.20764422, + "qstar": 5.29496129, + "rm": 34.6484432, + "terror": 0.00838636131, + "volume": 457965.069, + "zm": 4.22916249e-05 + }, + "chi2_final": 4.7e-09, + "chi2_initial": 41.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008386, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 321 + }, + { + "accepted": true, + "afile": { + "aminor": 20.5966145, + "area": 2006.82869, + "betap": 0.00662775115, + "chisq": 23.8712017, + "elong": 1.50220605, + "jflag": 1, + "li": 0.661781255, + "q95": 5.95698231, + "qstar": 5.2263564, + "rm": 32.8515768, + "terror": 0.00869628438, + "volume": 376088.935, + "zm": 3.67671345e-05 + }, + "chi2_final": 5.4e-09, + "chi2_initial": 23.9, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008696, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 322 + }, + { + "accepted": true, + "afile": { + "aminor": 18.6568792, + "area": 1623.03056, + "betap": 0.00526412489, + "chisq": 10.5640198, + "elong": 1.47650533, + "jflag": 1, + "li": 0.671566919, + "q95": 5.60227827, + "qstar": 5.06902134, + "rm": 31.0546501, + "terror": 0.00842013151, + "volume": 286745.899, + "zm": 2.9946273e-05 + }, + "chi2_final": 6.21e-09, + "chi2_initial": 10.6, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.00842, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 323 + }, + { + "accepted": true, + "afile": { + "aminor": 16.791075, + "area": 1293.66039, + "betap": 0.00414857212, + "chisq": 5.01289724, + "elong": 1.45209726, + "jflag": 1, + "li": 0.682712348, + "q95": 5.29706381, + "qstar": 4.94225695, + "rm": 28.3594193, + "terror": 0.00914649423, + "volume": 214985.019, + "zm": 2.48362502e-05 + }, + "chi2_final": 7.13e-09, + "chi2_initial": 5.01, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.009146, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 324 + }, + { + "accepted": true, + "afile": { + "aminor": 15.3087325, + "area": 1055.96465, + "betap": 0.00339565006, + "chisq": 0.0558560195, + "elong": 1.42510856, + "jflag": 1, + "li": 0.687456218, + "q95": 5.00495737, + "qstar": 4.78169059, + "rm": 27.4608716, + "terror": 0.00992275918, + "volume": 166580.517, + "zm": 1.96700545e-05 + }, + "chi2_final": 8.36e-09, + "chi2_initial": 0.0559, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.009923, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 325 + }, + { + "accepted": true, + "afile": { + "aminor": 13.8703286, + "area": 847.206201, + "betap": 0.00277492124, + "chisq": 0.294439904, + "elong": 1.392393, + "jflag": 1, + "li": 0.698590789, + "q95": 4.65658412, + "qstar": 4.55019402, + "rm": 25.6640208, + "terror": 0.00700958764, + "volume": 126653.754, + "zm": 1.59968407e-05 + }, + "chi2_final": 8.59e-09, + "chi2_initial": 0.294, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.00701, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 326 + }, + { + "accepted": true, + "afile": { + "aminor": 12.6123534, + "area": 686.476417, + "betap": 0.00228888851, + "chisq": 1.85392668, + "elong": 1.36695381, + "jflag": 1, + "li": 0.71737706, + "q95": 4.37344001, + "qstar": 4.36458167, + "rm": 23.8672076, + "terror": 0.00768489094, + "volume": 97606.7625, + "zm": 1.02745101e-05 + }, + "chi2_final": 8.36e-09, + "chi2_initial": 1.85, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007685, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 327 + }, + { + "accepted": true, + "afile": { + "aminor": 10.7312893, + "area": 477.170829, + "betap": 0.00168977729, + "chisq": 4.90295482, + "elong": 1.3077958, + "jflag": 1, + "li": 0.714674244, + "q95": 3.92213251, + "qstar": 4.01219432, + "rm": 22.0702823, + "terror": 0.00874230542, + "volume": 62560.1495, + "zm": 5.81963789e-06 + }, + "chi2_final": 5.2e-09, + "chi2_initial": 4.9, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008742, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 328 + }, + { + "accepted": false, + "afile": null, + "chi2_final": null, + "chi2_initial": null, + "collapsed": false, + "exit_path": "solver_error", + "failures": [], + "gs_error": null, + "iconvr": null, + "iterations_n": 0, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 329 + }, + { + "accepted": false, + "afile": null, + "chi2_final": null, + "chi2_initial": null, + "collapsed": false, + "exit_path": "solver_error", + "failures": [], + "gs_error": null, + "iconvr": null, + "iterations_n": 0, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 330 + } + ], + "summary": { + "accepted": 10, + "bound_failures": 14, + "by_phase": { + "flat_top": { + "accepted": 2, + "collapsed": 4, + "flagged": 3 + }, + "ramp_down": { + "accepted": 8, + "no_output": 2 + }, + "ramp_up": { + "collapsed": 5 + } + }, + "collapsed": 9, + "findax_failures": 0, + "outcomes": { + "accepted": 10, + "collapsed": 9, + "flagged": 3, + "no_output": 2 + }, + "produced_an_equilibrium": 13, + "slices": 24 + } + }, + "B": { + "returncode": 0, + "seconds": 12.542144041974097, + "summary": { + "accepted": 5, + "bound_failures": 10, + "by_phase": { + "flat_top": { + "accepted": 2, + "collapsed": 4, + "flagged": 3 + }, + "ramp_down": { + "accepted": 3, + "no_output": 7 + }, + "ramp_up": { + "collapsed": 5 + } + }, + "collapsed": 9, + "findax_failures": 6, + "outcomes": { + "accepted": 5, + "collapsed": 9, + "flagged": 3, + "no_output": 7 + }, + "produced_an_equilibrium": 8, + "slices": 24 + }, + "vs_reference": { + "changed_vs_reference": { + "322": [ + "accepted", + "no_output" + ], + "323": [ + "accepted", + "no_output" + ], + "324": [ + "accepted", + "no_output" + ], + "325": [ + "accepted", + "no_output" + ], + "326": [ + "accepted", + "no_output" + ], + "327": [ + "accepted", + "no_output" + ], + "329": [ + "no_output", + "accepted" + ] + }, + "compared_on": 7, + "lost": [ + 322, + 323, + 324, + 325, + 326, + 327 + ], + "metrics": { + "aminor": { + "max_abs": 0.009292200000000861, + "median_abs": 0.003517900000002072, + "median_relative": 0.00014501913659821852, + "reference_median": 24.2581778, + "slices": 7 + }, + "area": { + "max_abs": 1.392830000000231, + "median_abs": 0.7319899999997688, + "median_relative": 0.00025846534837313675, + "reference_median": 2832.06242, + "slices": 7 + }, + "betap": { + "max_abs": 1.4650399999999869e-05, + "median_abs": 8.8656999999992e-06, + "median_relative": 0.0009010736361944236, + "reference_median": 0.0098390405, + "slices": 7 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 74.4342174, + "slices": 7 + }, + "elong": { + "max_abs": 0.010448719999999856, + "median_abs": 0.0007152100000000772, + "median_relative": 0.0004643113215818793, + "reference_median": 1.54036735, + "slices": 7 + }, + "li": { + "max_abs": 0.008209706000000039, + "median_abs": 0.0014527119999999671, + "median_relative": 0.0022262503905119526, + "reference_median": 0.652537561, + "slices": 7 + }, + "q95": { + "max_abs": 0.029555869999999818, + "median_abs": 0.006949259999999846, + "median_relative": 0.0010250170273089095, + "reference_median": 6.77965323, + "slices": 7 + }, + "qstar": { + "max_abs": 0.07602567999999987, + "median_abs": 0.017283589999999904, + "median_relative": 0.00309512071690511, + "reference_median": 5.58414084, + "slices": 7 + }, + "rm": { + "max_abs": 0.42963689999999843, + "median_abs": 0.15623269999999678, + "median_relative": 0.004183640706179941, + "reference_median": 37.3437183, + "slices": 7 + }, + "terror": { + "max_abs": 0.00030266739999999966, + "median_abs": 0.00010532582000000006, + "median_relative": 0.012416754224389169, + "reference_median": 0.00848255656, + "slices": 7 + }, + "volume": { + "max_abs": 160.49599999992643, + "median_abs": 86.66300000005867, + "median_relative": 0.00014780294343253863, + "reference_median": 586341.503, + "slices": 7 + }, + "zm": { + "max_abs": 2.1092745420261, + "median_abs": 7.8291263e-07, + "median_relative": null, + "reference_median": 5.14091095e-05, + "slices": 7 + } + }, + "recovered": [ + 329 + ] + } + }, + "C": { + "returncode": 0, + "seconds": 22.950824500003364, + "summary": { + "accepted": 10, + "bound_failures": 14, + "by_phase": { + "flat_top": { + "accepted": 2, + "collapsed": 4, + "flagged": 3 + }, + "ramp_down": { + "accepted": 8, + "no_output": 2 + }, + "ramp_up": { + "collapsed": 5 + } + }, + "collapsed": 9, + "findax_failures": 0, + "outcomes": { + "accepted": 10, + "collapsed": 9, + "flagged": 3, + "no_output": 2 + }, + "produced_an_equilibrium": 13, + "slices": 24 + }, + "vs_reference": { + "changed_vs_reference": {}, + "compared_on": 13, + "lost": [], + "metrics": { + "aminor": { + "max_abs": 0.025367000000001028, + "median_abs": 0.00859580000000193, + "median_relative": 0.0004173404323318247, + "reference_median": 20.5966145, + "slices": 13 + }, + "area": { + "max_abs": 1.3200169999999503, + "median_abs": 0.38328999999976077, + "median_relative": 0.00019099288439999369, + "reference_median": 2006.82869, + "slices": 13 + }, + "betap": { + "max_abs": 1.3195680000000019e-05, + "median_abs": 1.9006499999999968e-06, + "median_relative": 0.0002867714790408202, + "reference_median": 0.00662775115, + "slices": 13 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 23.8712017, + "slices": 13 + }, + "elong": { + "max_abs": 0.012251699999999976, + "median_abs": 0.0006584300000000987, + "median_relative": 0.00043830871270961706, + "reference_median": 1.50220605, + "slices": 13 + }, + "li": { + "max_abs": 0.00692999900000002, + "median_abs": 0.0017368009999999545, + "median_relative": 0.0026244336582183104, + "reference_median": 0.661781255, + "slices": 13 + }, + "q95": { + "max_abs": 0.04554864000000025, + "median_abs": 0.010165599999999664, + "median_relative": 0.0017065016263242293, + "reference_median": 5.95698231, + "slices": 13 + }, + "qstar": { + "max_abs": 0.0471240599999998, + "median_abs": 0.0030617100000007724, + "median_relative": 0.0005858211276982129, + "reference_median": 5.2263564, + "slices": 13 + }, + "rm": { + "max_abs": 0.8983077000000002, + "median_abs": 1.1999999998124622e-06, + "median_relative": 3.652792701909098e-08, + "reference_median": 32.8515768, + "slices": 13 + }, + "terror": { + "max_abs": 0.00078085078, + "median_abs": 0.00018088401999999913, + "median_relative": 0.021324233881677663, + "reference_median": 0.00848255656, + "slices": 13 + }, + "volume": { + "max_abs": 212.5899999999674, + "median_abs": 104.67190000000119, + "median_relative": 0.0002783168826809573, + "reference_median": 376088.935, + "slices": 13 + }, + "zm": { + "max_abs": 1.1718264600000001, + "median_abs": 1.1718082228655, + "median_relative": null, + "reference_median": 3.67671345e-05, + "slices": 13 + } + }, + "recovered": [] + } + }, + "D": { + "returncode": 0, + "seconds": 17.03001291700639, + "summary": { + "accepted": 5, + "bound_failures": 10, + "by_phase": { + "flat_top": { + "accepted": 2, + "collapsed": 4, + "flagged": 3 + }, + "ramp_down": { + "accepted": 3, + "no_output": 7 + }, + "ramp_up": { + "collapsed": 5 + } + }, + "collapsed": 9, + "findax_failures": 6, + "outcomes": { + "accepted": 5, + "collapsed": 9, + "flagged": 3, + "no_output": 7 + }, + "produced_an_equilibrium": 8, + "slices": 24 + }, + "vs_reference": { + "changed_vs_reference": { + "322": [ + "accepted", + "no_output" + ], + "323": [ + "accepted", + "no_output" + ], + "324": [ + "accepted", + "no_output" + ], + "325": [ + "accepted", + "no_output" + ], + "326": [ + "accepted", + "no_output" + ], + "327": [ + "accepted", + "no_output" + ], + "329": [ + "no_output", + "accepted" + ] + }, + "compared_on": 7, + "lost": [ + 322, + 323, + 324, + 325, + 326, + 327 + ], + "metrics": { + "aminor": { + "max_abs": 0.025167800000000184, + "median_abs": 0.0043666000000008864, + "median_relative": 0.00018000527640624707, + "reference_median": 24.2581778, + "slices": 7 + }, + "area": { + "max_abs": 1.17679999999973, + "median_abs": 0.969119999999748, + "median_relative": 0.00034219584750527776, + "reference_median": 2832.06242, + "slices": 7 + }, + "betap": { + "max_abs": 1.6755000000000242e-05, + "median_abs": 8.049620000000549e-06, + "median_relative": 0.0008181305890549539, + "reference_median": 0.0098390405, + "slices": 7 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 74.4342174, + "slices": 7 + }, + "elong": { + "max_abs": 0.01160063999999994, + "median_abs": 0.0005009400000000053, + "median_relative": 0.0003252081394740062, + "reference_median": 1.54036735, + "slices": 7 + }, + "li": { + "max_abs": 0.007366833999999933, + "median_abs": 0.0014111129999999639, + "median_relative": 0.0021625008035360644, + "reference_median": 0.652537561, + "slices": 7 + }, + "q95": { + "max_abs": 0.03858073000000006, + "median_abs": 0.00141090999999971, + "median_relative": 0.00020810946402928485, + "reference_median": 6.77965323, + "slices": 7 + }, + "qstar": { + "max_abs": 0.07328433999999984, + "median_abs": 0.015647650000000013, + "median_relative": 0.0028021589082985977, + "reference_median": 5.58414084, + "slices": 7 + }, + "rm": { + "max_abs": 0.4296369999999996, + "median_abs": 0.15623260000000272, + "median_relative": 0.004183638028353559, + "reference_median": 37.3437183, + "slices": 7 + }, + "terror": { + "max_abs": 0.00021336527999999896, + "median_abs": 9.334293999999944e-05, + "median_relative": 0.011004104639886944, + "reference_median": 0.00848255656, + "slices": 7 + }, + "volume": { + "max_abs": 197.27600000007078, + "median_abs": 101.52000000001863, + "median_relative": 0.0001731414192592446, + "reference_median": 586341.503, + "slices": 7 + }, + "zm": { + "max_abs": 1.28900914, + "median_abs": 1.0546344308905, + "median_relative": null, + "reference_median": 5.14091095e-05, + "slices": 7 + } + }, + "recovered": [ + 329 + ] + } + } + }, + "phases": { + "flat_top": 9, + "ramp_down": 11, + "ramp_up": 6 + }, + "window": { + "end": 0.33079999999997256, + "slices": 26, + "start": 0.30631999999998205 + } + }, + "41524": { + "cases": { + "A": { + "returncode": 0, + "seconds": 10.841973958013114, + "slices": [ + { + "accepted": false, + "afile": null, + "chi2_final": 5.66e-08, + "chi2_initial": 129.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.4177, + "iconvr": null, + "iterations_n": 8, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 315 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 6.47e-08, + "chi2_initial": 333.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3839, + "iconvr": null, + "iterations_n": 9, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 316 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 7.36e-08, + "chi2_initial": 438.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3569, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 317 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 9.33e-08, + "chi2_initial": 616.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3554, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 318 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 1.29e-07, + "chi2_initial": 667.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3537, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 319 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 1.75e-07, + "chi2_initial": 709.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3519, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 320 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.18e-07, + "chi2_initial": 713.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3529, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 321 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.52e-07, + "chi2_initial": 774.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3544, + "iconvr": null, + "iterations_n": 10, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 322 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.81e-07, + "chi2_initial": 694.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3329, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 323 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.87e-07, + "chi2_initial": 611.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3327, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "Number of contour points greater than max allowed", + "routine": "bound" + } + ], + "time_ms": 324 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.87e-07, + "chi2_initial": 542.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3171, + "iconvr": null, + "iterations_n": 12, + "solver_errors": [ + { + "detail": "Number of contour points greater than max allowed", + "routine": "bound" + } + ], + "time_ms": 325 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.86e-07, + "chi2_initial": 428.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3064, + "iconvr": null, + "iterations_n": 13, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 326 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.97e-07, + "chi2_initial": 374.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.2833, + "iconvr": null, + "iterations_n": 16, + "solver_errors": [ + { + "detail": "Number of contour points greater than max allowed", + "routine": "bound" + } + ], + "time_ms": 327 + }, + { + "accepted": false, + "afile": { + "aminor": 28.3696109, + "area": 4030.99529, + "betap": 0.0143759049, + "chisq": 278.52921, + "elong": 1.6328863, + "jflag": 0, + "li": 0.642439346, + "q95": 4.33079301, + "qstar": 3.38474661, + "rm": 41.6257902, + "terror": 0.00815815957, + "volume": 917041.675, + "zm": 1.29049472 + }, + "chi2_final": 1.91e-07, + "chi2_initial": 279.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008158, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 328 + }, + { + "accepted": false, + "afile": { + "aminor": 26.3463146, + "area": 3462.6994, + "betap": 0.0120050841, + "chisq": 179.382998, + "elong": 1.61174794, + "jflag": 0, + "li": 0.644646547, + "q95": 4.02974024, + "qstar": 3.26073043, + "rm": 39.4229782, + "terror": 0.00782970243, + "volume": 753743.582, + "zm": 1.07643656 + }, + "chi2_final": 2.03e-07, + "chi2_initial": 179.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.00783, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 329 + }, + { + "accepted": false, + "afile": { + "aminor": 24.3259714, + "area": 2929.30172, + "betap": 0.00987835295, + "chisq": 131.204416, + "elong": 1.58632165, + "jflag": 0, + "li": 0.650560529, + "q95": 3.80254624, + "qstar": 3.17392733, + "rm": 37.3437093, + "terror": 0.00770207124, + "volume": 607521.442, + "zm": 9.09647859e-05 + }, + "chi2_final": 2.13e-07, + "chi2_initial": 131.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.007702, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 330 + }, + { + "accepted": false, + "afile": { + "aminor": 22.4472483, + "area": 2470.11995, + "betap": 0.00813161046, + "chisq": 80.9840302, + "elong": 1.56402633, + "jflag": 0, + "li": 0.655885609, + "q95": 3.6128445, + "qstar": 3.11499481, + "rm": 35.0790753, + "terror": 0.00759430096, + "volume": 487842.409, + "zm": 0.743072337 + }, + "chi2_final": 2.23e-07, + "chi2_initial": 81.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.007594, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 331 + }, + { + "accepted": true, + "afile": { + "aminor": 20.8234002, + "area": 2095.94688, + "betap": 0.00680109138, + "chisq": 28.6758847, + "elong": 1.53578629, + "jflag": 1, + "li": 0.662222773, + "q95": 3.42026048, + "qstar": 3.02420273, + "rm": 33.2474455, + "terror": 0.00754634802, + "volume": 395631.645, + "zm": 0.558909944 + }, + "chi2_final": 2.33e-07, + "chi2_initial": 28.7, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007546, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 332 + }, + { + "accepted": true, + "afile": { + "aminor": 19.781379, + "area": 1865.73681, + "betap": 0.00603649679, + "chisq": 15.1778127, + "elong": 1.51220517, + "jflag": 1, + "li": 0.664996565, + "q95": 3.29781204, + "qstar": 2.94414754, + "rm": 31.9531562, + "terror": 0.00738711453, + "volume": 341599.444, + "zm": 5.01124994e-05 + }, + "chi2_final": 2.36e-07, + "chi2_initial": 15.2, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007387, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 333 + }, + { + "accepted": true, + "afile": { + "aminor": 18.4814444, + "area": 1607.18896, + "betap": 0.00515586567, + "chisq": 1.14164561, + "elong": 1.49023497, + "jflag": 1, + "li": 0.674566336, + "q95": 3.24387931, + "qstar": 2.96047624, + "rm": 30.5976152, + "terror": 0.00750950432, + "volume": 282561.31, + "zm": 0.328089701 + }, + "chi2_final": 2.39e-07, + "chi2_initial": 1.14, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.00751, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 334 + }, + { + "accepted": true, + "afile": { + "aminor": 12.0880768, + "area": 620.72311, + "betap": 0.00211615932, + "chisq": 2.69155651, + "elong": 1.34180892, + "jflag": 1, + "li": 0.70403387, + "q95": 2.60631376, + "qstar": 2.62503413, + "rm": 23.4091347, + "terror": 0.00745326639, + "volume": 86388.3406, + "zm": 0.202552631 + }, + "chi2_final": 2.6e-07, + "chi2_initial": 2.69, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007453, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 335 + } + ], + "summary": { + "accepted": 4, + "bound_failures": 16, + "by_phase": { + "flat_top": { + "collapsed": 6 + }, + "ramp_down": { + "accepted": 4, + "flagged": 4 + }, + "ramp_up": { + "collapsed": 7 + } + }, + "collapsed": 13, + "findax_failures": 0, + "outcomes": { + "accepted": 4, + "collapsed": 13, + "flagged": 4 + }, + "produced_an_equilibrium": 8, + "slices": 21 + } + }, + "B": { + "returncode": 0, + "seconds": 5.173235082998872, + "summary": { + "accepted": 0, + "bound_failures": 13, + "by_phase": { + "flat_top": { + "collapsed": 6 + }, + "ramp_down": { + "flagged": 3, + "no_output": 5 + }, + "ramp_up": { + "collapsed": 7 + } + }, + "collapsed": 13, + "findax_failures": 5, + "outcomes": { + "collapsed": 13, + "flagged": 3, + "no_output": 5 + }, + "produced_an_equilibrium": 3, + "slices": 21 + }, + "vs_reference": { + "changed_vs_reference": { + "331": [ + "flagged", + "no_output" + ], + "332": [ + "accepted", + "no_output" + ], + "333": [ + "accepted", + "no_output" + ], + "334": [ + "accepted", + "no_output" + ], + "335": [ + "accepted", + "no_output" + ] + }, + "compared_on": 3, + "lost": [ + 331, + 332, + 333, + 334, + 335 + ], + "metrics": { + "aminor": { + "max_abs": 0.004994699999997465, + "median_abs": 0.003298400000002033, + "median_relative": 0.00012519398064130127, + "reference_median": 26.3463146, + "slices": 3 + }, + "area": { + "max_abs": 1.4336299999999937, + "median_abs": 0.7962800000000243, + "median_relative": 0.0002299593201766299, + "reference_median": 3462.6994, + "slices": 3 + }, + "betap": { + "max_abs": 2.211319999999989e-05, + "median_abs": 1.1195199999999503e-05, + "median_relative": 0.0009325382401943775, + "reference_median": 0.0120050841, + "slices": 3 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 179.382998, + "slices": 3 + }, + "elong": { + "max_abs": 0.0009790600000001426, + "median_abs": 0.0006321600000001038, + "median_relative": 0.00039222013834253993, + "reference_median": 1.61174794, + "slices": 3 + }, + "li": { + "max_abs": 0.0034974589999999806, + "median_abs": 0.002027858000000049, + "median_relative": 0.0031456896952866934, + "reference_median": 0.644646547, + "slices": 3 + }, + "q95": { + "max_abs": 0.006240619999999808, + "median_abs": 0.0037157099999998167, + "median_relative": 0.0009220718405412199, + "reference_median": 4.02974024, + "slices": 3 + }, + "qstar": { + "max_abs": 0.010207030000000117, + "median_abs": 0.009884890000000368, + "median_relative": 0.0030314956149258766, + "reference_median": 3.26073043, + "slices": 3 + }, + "rm": { + "max_abs": 0.2585443999999981, + "median_abs": 0.15598690000000204, + "median_relative": 0.003956750786524851, + "reference_median": 39.4229782, + "slices": 3 + }, + "terror": { + "max_abs": 0.00011073807000000057, + "median_abs": 7.36468500000002e-05, + "median_relative": 0.009406085436633918, + "reference_median": 0.00782970243, + "slices": 3 + }, + "volume": { + "max_abs": 87.00500000000466, + "median_abs": 49.78000000002794, + "median_relative": 6.604368009070217e-05, + "reference_median": 753743.582, + "slices": 3 + }, + "zm": { + "max_abs": 0.9011316142141, + "median_abs": 0.8188059300000001, + "median_relative": 0.7606634338023599, + "reference_median": 1.07643656, + "slices": 3 + } + }, + "recovered": [] + } + }, + "C": { + "returncode": 0, + "seconds": 23.20320112502668, + "summary": { + "accepted": 4, + "bound_failures": 21, + "by_phase": { + "flat_top": { + "collapsed": 6 + }, + "ramp_down": { + "accepted": 4, + "flagged": 4 + }, + "ramp_up": { + "collapsed": 7 + } + }, + "collapsed": 13, + "findax_failures": 0, + "outcomes": { + "accepted": 4, + "collapsed": 13, + "flagged": 4 + }, + "produced_an_equilibrium": 8, + "slices": 21 + }, + "vs_reference": { + "changed_vs_reference": {}, + "compared_on": 8, + "lost": [], + "metrics": { + "aminor": { + "max_abs": 0.02430549999999876, + "median_abs": 0.008961599999999237, + "median_relative": 0.00041421149488893094, + "reference_median": 21.635324249999996, + "slices": 8 + }, + "area": { + "max_abs": 1.135240000000067, + "median_abs": 0.4361319999999296, + "median_relative": 0.00019103180756551897, + "reference_median": 2283.033415, + "slices": 8 + }, + "betap": { + "max_abs": 1.2150849999999908e-05, + "median_abs": 3.609530000000822e-06, + "median_relative": 0.00048343963988245307, + "reference_median": 0.00746635092, + "slices": 8 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 54.82995745, + "slices": 8 + }, + "elong": { + "max_abs": 0.007483649999999953, + "median_abs": 0.0007816900000000571, + "median_relative": 0.0005043466143447452, + "reference_median": 1.54990631, + "slices": 8 + }, + "li": { + "max_abs": 0.002962123000000094, + "median_abs": 0.0011060565000000078, + "median_relative": 0.0016782481852088058, + "reference_median": 0.6590541910000001, + "slices": 8 + }, + "q95": { + "max_abs": 0.018102799999999863, + "median_abs": 0.005412739999999916, + "median_relative": 0.001539217746753985, + "reference_median": 3.51655249, + "slices": 8 + }, + "qstar": { + "max_abs": 0.01697039999999994, + "median_abs": 0.001208574999999934, + "median_relative": 0.00039372409573904476, + "reference_median": 3.06959877, + "slices": 8 + }, + "rm": { + "max_abs": 0.2822883000000047, + "median_abs": 0.011400699999999375, + "median_relative": 0.0003337122940408631, + "reference_median": 34.1632604, + "slices": 8 + }, + "terror": { + "max_abs": 0.0006436335599999997, + "median_abs": 7.896866499999995e-05, + "median_relative": 0.010431344799593914, + "reference_median": 0.0075703244900000004, + "slices": 8 + }, + "volume": { + "max_abs": 263.35600000002887, + "median_abs": 155.9079999999958, + "median_relative": 0.00035294301919588873, + "reference_median": 441737.027, + "slices": 8 + }, + "zm": { + "max_abs": 1.1717567852141, + "median_abs": 0.006879331499999947, + "median_relative": 0.015511463930743617, + "reference_median": 0.44349982250000003, + "slices": 8 + } + }, + "recovered": [] + } + }, + "D": { + "returncode": 0, + "seconds": 17.47955991595518, + "summary": { + "accepted": 0, + "bound_failures": 13, + "by_phase": { + "flat_top": { + "collapsed": 6 + }, + "ramp_down": { + "flagged": 3, + "no_output": 5 + }, + "ramp_up": { + "collapsed": 7 + } + }, + "collapsed": 13, + "findax_failures": 5, + "outcomes": { + "collapsed": 13, + "flagged": 3, + "no_output": 5 + }, + "produced_an_equilibrium": 3, + "slices": 21 + }, + "vs_reference": { + "changed_vs_reference": { + "331": [ + "flagged", + "no_output" + ], + "332": [ + "accepted", + "no_output" + ], + "333": [ + "accepted", + "no_output" + ], + "334": [ + "accepted", + "no_output" + ], + "335": [ + "accepted", + "no_output" + ] + }, + "compared_on": 3, + "lost": [ + 331, + 332, + 333, + 334, + 335 + ], + "metrics": { + "aminor": { + "max_abs": 0.006365699999999919, + "median_abs": 0.0037602000000021008, + "median_relative": 0.0001427220488744221, + "reference_median": 26.3463146, + "slices": 3 + }, + "area": { + "max_abs": 1.9736100000000079, + "median_abs": 1.1327099999998609, + "median_relative": 0.0003271176238976623, + "reference_median": 3462.6994, + "slices": 3 + }, + "betap": { + "max_abs": 1.5493099999999704e-05, + "median_abs": 1.1474499999998902e-05, + "median_relative": 0.0009558033833348074, + "reference_median": 0.0120050841, + "slices": 3 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 179.382998, + "slices": 3 + }, + "elong": { + "max_abs": 0.0016388099999999906, + "median_abs": 0.0008363499999999302, + "median_relative": 0.0005189086824580834, + "reference_median": 1.61174794, + "slices": 3 + }, + "li": { + "max_abs": 0.0032387510000000397, + "median_abs": 0.0012709000000000747, + "median_relative": 0.001971467939934649, + "reference_median": 0.644646547, + "slices": 3 + }, + "q95": { + "max_abs": 0.0039280399999999105, + "median_abs": 0.00041703999999942454, + "median_relative": 0.00010349054161352707, + "reference_median": 4.02974024, + "slices": 3 + }, + "qstar": { + "max_abs": 0.012782790000000155, + "median_abs": 0.00914174000000001, + "median_relative": 0.0028035865571383674, + "reference_median": 3.26073043, + "slices": 3 + }, + "rm": { + "max_abs": 0.4295814999999976, + "median_abs": 0.2822862000000015, + "median_relative": 0.007160448370184307, + "reference_median": 39.4229782, + "slices": 3 + }, + "terror": { + "max_abs": 8.152804000000069e-05, + "median_abs": 6.64528000000001e-05, + "median_relative": 0.008487270186077825, + "reference_median": 0.00782970243, + "slices": 3 + }, + "volume": { + "max_abs": 209.44900000002235, + "median_abs": 165.99300000001676, + "median_relative": 0.00022022476073304298, + "reference_median": 753743.582, + "slices": 3 + }, + "zm": { + "max_abs": 1.0545813552141001, + "median_abs": 0.23578591000000015, + "median_relative": 0.2190430154100304, + "reference_median": 1.07643656, + "slices": 3 + } + }, + "recovered": [] + } + } + }, + "phases": { + "flat_top": 6, + "ramp_down": 9, + "ramp_up": 7 + }, + "window": { + "end": 0.3364399999999704, + "slices": 22, + "start": 0.31455999999997886 + } + }, + "41672": { + "cases": { + "A": { + "returncode": 0, + "seconds": 28.355229749984574, + "slices": [ + { + "accepted": false, + "afile": null, + "chi2_final": 1.76e-08, + "chi2_initial": 136.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.4291, + "iconvr": null, + "iterations_n": 8, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 315 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.1e-08, + "chi2_initial": 359.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.383, + "iconvr": null, + "iterations_n": 9, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + }, + { + "detail": "Less than 3 contour points found", + "routine": "bound" + } + ], + "time_ms": 316 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.19e-08, + "chi2_initial": 437.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3938, + "iconvr": null, + "iterations_n": 9, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 317 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 2.53e-08, + "chi2_initial": 619.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.3354, + "iconvr": null, + "iterations_n": 11, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 318 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 3.09e-08, + "chi2_initial": 643.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.314, + "iconvr": null, + "iterations_n": 13, + "solver_errors": [ + { + "detail": "Number of contour points greater than max allowed", + "routine": "bound" + } + ], + "time_ms": 319 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 3.85e-08, + "chi2_initial": 702.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.2927, + "iconvr": null, + "iterations_n": 15, + "solver_errors": [ + { + "detail": "Number of contour points greater than max allowed", + "routine": "bound" + } + ], + "time_ms": 320 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 5.02e-08, + "chi2_initial": 692.0, + "collapsed": true, + "exit_path": "solver_error", + "failures": [], + "gs_error": 0.2642, + "iconvr": null, + "iterations_n": 19, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 321 + }, + { + "accepted": false, + "afile": { + "aminor": 28.3320282, + "area": 4092.98824, + "betap": 0.014277282, + "chisq": 682.355809, + "elong": 1.64980682, + "jflag": 0, + "li": 0.641838138, + "q95": 9.75899506, + "qstar": 7.50692547, + "rm": 41.8359107, + "terror": 0.00978273238, + "volume": 927529.345, + "zm": 2.34378184 + }, + "chi2_final": 3.79e-08, + "chi2_initial": 682.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.009783, + "iconvr": 2, + "iterations_n": 13, + "solver_errors": [], + "time_ms": 322 + }, + { + "accepted": false, + "afile": { + "aminor": 27.7839889, + "area": 3949.6386, + "betap": 0.0136252744, + "chisq": 585.397262, + "elong": 1.6524021, + "jflag": 0, + "li": 0.642724576, + "q95": 8.35702428, + "qstar": 6.54048496, + "rm": 40.9375083, + "terror": 0.00882296926, + "volume": 885180.939, + "zm": 2.34377316 + }, + "chi2_final": 4.37e-08, + "chi2_initial": 585.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008823, + "iconvr": 2, + "iterations_n": 13, + "solver_errors": [], + "time_ms": 323 + }, + { + "accepted": false, + "afile": { + "aminor": 27.5272944, + "area": 3840.45924, + "betap": 0.0133046595, + "chisq": 568.344113, + "elong": 1.63838098, + "jflag": 0, + "li": 0.64337194, + "q95": 7.5321813, + "qstar": 5.91437008, + "rm": 40.9374693, + "terror": 0.00909635913, + "volume": 855910.642, + "zm": 2.34375133 + }, + "chi2_final": 4.74e-08, + "chi2_initial": 568.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.009096, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 324 + }, + { + "accepted": false, + "afile": { + "aminor": 26.1854177, + "area": 3447.72105, + "betap": 0.0117866869, + "chisq": 441.199129, + "elong": 1.61848925, + "jflag": 0, + "li": 0.64847413, + "q95": 6.69260027, + "qstar": 5.36545565, + "rm": 39.140638, + "terror": 0.00812409363, + "volume": 746065.514, + "zm": 2.3437326 + }, + "chi2_final": 5.3e-08, + "chi2_initial": 441.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008124, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 325 + }, + { + "accepted": false, + "afile": { + "aminor": 24.8593867, + "area": 3088.04451, + "betap": 0.0103589299, + "chisq": 361.007195, + "elong": 1.60312544, + "jflag": 0, + "li": 0.64800889, + "q95": 6.14167156, + "qstar": 5.05655907, + "rm": 37.3438151, + "terror": 0.00975201535, + "volume": 647570.82, + "zm": 2.3437081 + }, + "chi2_final": 5.9e-08, + "chi2_initial": 361.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.009752, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 326 + }, + { + "accepted": false, + "afile": { + "aminor": 23.4942152, + "area": 2742.21912, + "betap": 0.00902848781, + "chisq": 269.544759, + "elong": 1.58781124, + "jflag": 0, + "li": 0.653778576, + "q95": 5.62356446, + "qstar": 4.7354645, + "rm": 36.4452775, + "terror": 0.00948264723, + "volume": 555745.335, + "zm": 2.3436879 + }, + "chi2_final": 6.68e-08, + "chi2_initial": 270.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.009483, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 327 + }, + { + "accepted": false, + "afile": { + "aminor": 23.8054312, + "area": 2818.27621, + "betap": 0.00934803936, + "chisq": 170.444656, + "elong": 1.5887768, + "jflag": 0, + "li": 0.65118197, + "q95": 5.18765558, + "qstar": 4.33827177, + "rm": 36.4453412, + "terror": 0.00868640743, + "volume": 576382.042, + "zm": 2.34366484 + }, + "chi2_final": 7.51e-08, + "chi2_initial": 170.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008686, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 328 + }, + { + "accepted": false, + "afile": { + "aminor": 23.6504172, + "area": 2750.29287, + "betap": 0.00919606172, + "chisq": 114.603969, + "elong": 1.57088732, + "jflag": 0, + "li": 0.651276573, + "q95": 4.70832165, + "qstar": 3.92651157, + "rm": 36.4453058, + "terror": 0.00831882073, + "volume": 560119.507, + "zm": 7.98044373e-05 + }, + "chi2_final": 7.22e-08, + "chi2_initial": 115.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.008319, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 329 + }, + { + "accepted": false, + "afile": { + "aminor": 23.3634148, + "area": 2678.25124, + "betap": 0.00891655313, + "chisq": 109.626835, + "elong": 1.56490552, + "jflag": 0, + "li": 0.652303869, + "q95": 4.5030092, + "qstar": 3.75622223, + "rm": 36.0879906, + "terror": 0.00791602931, + "volume": 541252.386, + "zm": 0.911624315 + }, + "chi2_final": 7.54e-08, + "chi2_initial": 110.0, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [ + { + "code": 1, + "criterion": "Chisq > 80.", + "value": null + } + ], + "gs_error": 0.007916, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 330 + }, + { + "accepted": true, + "afile": { + "aminor": 23.4102547, + "area": 2682.9725, + "betap": 0.00895814442, + "chisq": 57.774592, + "elong": 1.5617609, + "jflag": 1, + "li": 0.653473409, + "q95": 4.77749517, + "qstar": 3.97454845, + "rm": 36.4452511, + "terror": 0.00772048505, + "volume": 542680.049, + "zm": 6.62304832e-05 + }, + "chi2_final": 7.65e-08, + "chi2_initial": 57.8, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.00772, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 331 + }, + { + "accepted": true, + "afile": { + "aminor": 23.3030809, + "area": 2654.67143, + "betap": 0.00884351589, + "chisq": 66.3060719, + "elong": 1.5594985, + "jflag": 1, + "li": 0.65257131, + "q95": 4.84674772, + "qstar": 4.04969925, + "rm": 36.0037772, + "terror": 0.0077352494, + "volume": 535253.288, + "zm": 0.729047607 + }, + "chi2_final": 7.72e-08, + "chi2_initial": 66.3, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007735, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 332 + }, + { + "accepted": true, + "afile": { + "aminor": 22.9331328, + "area": 2560.18307, + "betap": 0.00850613254, + "chisq": 22.7598683, + "elong": 1.55147637, + "jflag": 1, + "li": 0.654358052, + "q95": 4.83553348, + "qstar": 4.07090759, + "rm": 35.5468811, + "terror": 0.00763108836, + "volume": 511090.69, + "zm": 4.89443833e-05 + }, + "chi2_final": 7.9e-08, + "chi2_initial": 22.8, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007631, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 333 + }, + { + "accepted": true, + "afile": { + "aminor": 22.6614861, + "area": 2497.26441, + "betap": 0.00825275851, + "chisq": 19.1547644, + "elong": 1.54924486, + "jflag": 1, + "li": 0.656249601, + "q95": 4.92606213, + "qstar": 4.15557091, + "rm": 35.5468225, + "terror": 0.00728060493, + "volume": 494702.757, + "zm": 4.27058446e-05 + }, + "chi2_final": 7.95e-08, + "chi2_initial": 19.2, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007281, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 334 + }, + { + "accepted": true, + "afile": { + "aminor": 22.2239319, + "area": 2398.63749, + "betap": 0.00786379118, + "chisq": 9.3074348, + "elong": 1.54552766, + "jflag": 1, + "li": 0.657874918, + "q95": 4.98750085, + "qstar": 4.22450477, + "rm": 34.6484603, + "terror": 0.00723306974, + "volume": 469405.129, + "zm": 3.49542606e-05 + }, + "chi2_final": 8.1e-08, + "chi2_initial": 9.31, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007233, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 335 + }, + { + "accepted": true, + "afile": { + "aminor": 21.8547287, + "area": 2315.3358, + "betap": 0.00754976676, + "chisq": 7.49481197, + "elong": 1.54126665, + "jflag": 1, + "li": 0.659894613, + "q95": 5.06216931, + "qstar": 4.30338216, + "rm": 34.6483842, + "terror": 0.00720834011, + "volume": 448472.918, + "zm": 2.78960524e-05 + }, + "chi2_final": 8.23e-08, + "chi2_initial": 7.49, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007208, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 336 + }, + { + "accepted": true, + "afile": { + "aminor": 21.4602491, + "area": 2235.8124, + "betap": 0.00721734093, + "chisq": 1.5577773, + "elong": 1.54097131, + "jflag": 1, + "li": 0.656437623, + "q95": 5.18109045, + "qstar": 4.45965879, + "rm": 33.7500286, + "terror": 0.00709209939, + "volume": 428162.547, + "zm": 2.3640148e-05 + }, + "chi2_final": 8.31e-08, + "chi2_initial": 1.56, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007092, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 337 + }, + { + "accepted": true, + "afile": { + "aminor": 21.0147543, + "area": 2142.65927, + "betap": 0.00686397939, + "chisq": 0.445936143, + "elong": 1.53863183, + "jflag": 1, + "li": 0.656165473, + "q95": 5.25370578, + "qstar": 4.54557433, + "rm": 33.7499404, + "terror": 0.00686787397, + "volume": 405095.945, + "zm": 1.75191567e-05 + }, + "chi2_final": 8.45e-08, + "chi2_initial": 0.446, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.006868, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 338 + }, + { + "accepted": true, + "afile": { + "aminor": 20.500388, + "area": 2040.66305, + "betap": 0.00646023955, + "chisq": 1.55960904, + "elong": 1.53949923, + "jflag": 1, + "li": 0.662256273, + "q95": 5.34853996, + "qstar": 4.66360524, + "rm": 32.8515588, + "terror": 0.00699767702, + "volume": 379959.187, + "zm": 1.32427435e-05 + }, + "chi2_final": 8.55e-08, + "chi2_initial": 1.56, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.006998, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 339 + }, + { + "accepted": true, + "afile": { + "aminor": 19.910254, + "area": 1916.43688, + "betap": 0.00602880854, + "chisq": 2.55053793, + "elong": 1.52754765, + "jflag": 1, + "li": 0.657409524, + "q95": 5.38177751, + "qstar": 4.70253566, + "rm": 31.9531608, + "terror": 0.00675982128, + "volume": 350675.069, + "zm": 6.77993998e-06 + }, + "chi2_final": 8.72e-08, + "chi2_initial": 2.55, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.00676, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 340 + }, + { + "accepted": true, + "afile": { + "aminor": 19.3620546, + "area": 1817.7827, + "betap": 0.00564108827, + "chisq": 0.0257444928, + "elong": 1.53335122, + "jflag": 1, + "li": 0.667676085, + "q95": 5.51712929, + "qstar": 4.91900684, + "rm": 31.9530574, + "terror": 0.00678503085, + "volume": 327013.495, + "zm": 5.81665506e-06 + }, + "chi2_final": 8.76e-08, + "chi2_initial": 0.0257, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.006785, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 341 + }, + { + "accepted": true, + "afile": { + "aminor": 18.6266812, + "area": 1669.98632, + "betap": 0.00516103147, + "chisq": 4.95005212, + "elong": 1.51789538, + "jflag": 1, + "li": 0.664245441, + "q95": 5.47576184, + "qstar": 4.90229868, + "rm": 31.0546313, + "terror": 0.00768791389, + "volume": 293682.795, + "zm": -7.11016817e-07 + }, + "chi2_final": 8.99e-08, + "chi2_initial": 4.95, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007688, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 342 + }, + { + "accepted": true, + "afile": { + "aminor": 18.0583895, + "area": 1569.21021, + "betap": 0.00480223733, + "chisq": 4.36549129, + "elong": 1.51793804, + "jflag": 1, + "li": 0.668940835, + "q95": 5.58232761, + "qstar": 5.09222887, + "rm": 30.1562374, + "terror": 0.00746355191, + "volume": 270909.396, + "zm": -2.09168305e-06 + }, + "chi2_final": 9.03e-08, + "chi2_initial": 4.37, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007464, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 343 + }, + { + "accepted": true, + "afile": { + "aminor": 18.4739482, + "area": 1641.66799, + "betap": 0.00506776814, + "chisq": 4.04050521, + "elong": 1.51587108, + "jflag": 1, + "li": 0.666777316, + "q95": 5.71191329, + "qstar": 5.12408382, + "rm": 30.1563124, + "terror": 0.00757837261, + "volume": 287377.845, + "zm": -5.27015833e-06 + }, + "chi2_final": 8.86e-08, + "chi2_initial": 4.04, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007578, + "iconvr": 2, + "iterations_n": 11, + "solver_errors": [], + "time_ms": 344 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 9.36e-08, + "chi2_initial": 8.43, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.006523, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [ + { + "detail": "1st separatrix point is off grid", + "routine": "findax" + } + ], + "time_ms": 345 + }, + { + "accepted": true, + "afile": { + "aminor": 12.0740855, + "area": 649.366042, + "betap": 0.00206565853, + "chisq": 10.7100829, + "elong": 1.40158752, + "jflag": 1, + "li": 0.713641638, + "q95": 4.72739043, + "qstar": 4.72168214, + "rm": 22.9688189, + "terror": 0.00823908192, + "volume": 89881.5424, + "zm": -1.3744124e-05 + }, + "chi2_final": 9.35e-08, + "chi2_initial": 10.7, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008239, + "iconvr": 2, + "iterations_n": 12, + "solver_errors": [], + "time_ms": 346 + }, + { + "accepted": false, + "afile": null, + "chi2_final": 9.47e-08, + "chi2_initial": 6.02, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.007272, + "iconvr": 2, + "iterations_n": 13, + "solver_errors": [ + { + "detail": "1st separatrix point is off grid", + "routine": "findax" + } + ], + "time_ms": 347 + }, + { + "accepted": true, + "afile": { + "aminor": 7.68719809, + "area": 236.3507, + "betap": 0.000961172529, + "chisq": 25.7801562, + "elong": 1.26428575, + "jflag": 1, + "li": 0.755047703, + "q95": 3.4553447, + "qstar": 3.67128304, + "rm": 18.4765696, + "terror": 0.00829713966, + "volume": 26590.163, + "zm": -4.41437469e-05 + }, + "chi2_final": 9.65e-08, + "chi2_initial": 25.8, + "collapsed": false, + "exit_path": "iconvr=2", + "failures": [], + "gs_error": 0.008297, + "iconvr": 2, + "iterations_n": 13, + "solver_errors": [], + "time_ms": 348 + }, + { + "accepted": false, + "afile": null, + "chi2_final": null, + "chi2_initial": null, + "collapsed": false, + "exit_path": "solver_error", + "failures": [], + "gs_error": null, + "iconvr": null, + "iterations_n": 0, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 349 + }, + { + "accepted": false, + "afile": null, + "chi2_final": null, + "chi2_initial": null, + "collapsed": false, + "exit_path": "solver_error", + "failures": [], + "gs_error": null, + "iconvr": null, + "iterations_n": 0, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 350 + }, + { + "accepted": false, + "afile": null, + "chi2_final": null, + "chi2_initial": null, + "collapsed": false, + "exit_path": "solver_error", + "failures": [], + "gs_error": null, + "iconvr": null, + "iterations_n": 0, + "solver_errors": [ + { + "detail": "First and last contour points are too far apart", + "routine": "bound" + } + ], + "time_ms": 351 + } + ], + "summary": { + "accepted": 16, + "bound_failures": 11, + "by_phase": { + "flat_top": { + "accepted": 4, + "flagged": 3 + }, + "ramp_down": { + "accepted": 12, + "no_output": 5 + }, + "ramp_up": { + "collapsed": 7, + "flagged": 6 + } + }, + "collapsed": 7, + "findax_failures": 2, + "outcomes": { + "accepted": 16, + "collapsed": 7, + "flagged": 9, + "no_output": 5 + }, + "produced_an_equilibrium": 25, + "slices": 37 + } + }, + "B": { + "returncode": 0, + "seconds": 24.366595582978334, + "summary": { + "accepted": 18, + "bound_failures": 10, + "by_phase": { + "flat_top": { + "accepted": 4, + "flagged": 2, + "no_output": 1 + }, + "ramp_down": { + "accepted": 14, + "no_output": 3 + }, + "ramp_up": { + "collapsed": 7, + "flagged": 5, + "no_output": 1 + } + }, + "collapsed": 7, + "findax_failures": 2, + "outcomes": { + "accepted": 18, + "collapsed": 7, + "flagged": 7, + "no_output": 5 + }, + "produced_an_equilibrium": 25, + "slices": 37 + }, + "vs_reference": { + "changed_vs_reference": { + "327": [ + "flagged", + "no_output" + ], + "328": [ + "flagged", + "no_output" + ], + "345": [ + "no_output", + "accepted" + ], + "347": [ + "no_output", + "accepted" + ] + }, + "compared_on": 23, + "lost": [ + 327, + 328 + ], + "metrics": { + "aminor": { + "max_abs": 0.008752699999998725, + "median_abs": 0.0035682999999977483, + "median_relative": 0.00016056114714776227, + "reference_median": 22.2239319, + "slices": 23 + }, + "area": { + "max_abs": 2.827684000000005, + "median_abs": 1.110809999999674, + "median_relative": 0.0004631004078901785, + "reference_median": 2398.63749, + "slices": 23 + }, + "betap": { + "max_abs": 1.9644200000000153e-05, + "median_abs": 8.062550000000057e-06, + "median_relative": 0.001025275190483893, + "reference_median": 0.00786379118, + "slices": 23 + }, + "chisq": { + "max_abs": 9.999999786569624e-11, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 19.1547644, + "slices": 23 + }, + "elong": { + "max_abs": 0.01851395999999994, + "median_abs": 0.001622200000000129, + "median_relative": 0.001049609167137215, + "reference_median": 1.54552766, + "slices": 23 + }, + "li": { + "max_abs": 0.015401222000000048, + "median_abs": 0.00347993000000002, + "median_relative": 0.005302753700264757, + "reference_median": 0.656249601, + "slices": 23 + }, + "q95": { + "max_abs": 0.08021228000000002, + "median_abs": 0.008504740000000233, + "median_relative": 0.0016188078198776166, + "reference_median": 5.25370578, + "slices": 23 + }, + "qstar": { + "max_abs": 0.0793660599999999, + "median_abs": 0.016125930000000288, + "median_relative": 0.003457824830817003, + "reference_median": 4.66360524, + "slices": 23 + }, + "rm": { + "max_abs": 0.585832400000001, + "median_abs": 0.1952787000000029, + "median_relative": 0.005635999357812818, + "reference_median": 34.6484603, + "slices": 23 + }, + "terror": { + "max_abs": 0.0008469700000000007, + "median_abs": 0.00013327554999999994, + "median_relative": 0.017335723566487545, + "reference_median": 0.00768791389, + "slices": 23 + }, + "volume": { + "max_abs": 334.879399999998, + "median_abs": 145.36099999997532, + "median_relative": 0.0003096706682980776, + "reference_median": 469405.129, + "slices": 23 + }, + "zm": { + "max_abs": 0.911551121766, + "median_abs": 4.546107999999975e-07, + "median_relative": null, + "reference_median": 4.27058446e-05, + "slices": 23 + } + }, + "recovered": [ + 345, + 347 + ] + } + }, + "C": { + "returncode": 0, + "seconds": 56.009572457987815, + "summary": { + "accepted": 14, + "bound_failures": 12, + "by_phase": { + "flat_top": { + "accepted": 4, + "flagged": 3 + }, + "ramp_down": { + "accepted": 10, + "no_output": 7 + }, + "ramp_up": { + "collapsed": 7, + "flagged": 6 + } + }, + "collapsed": 7, + "findax_failures": 4, + "outcomes": { + "accepted": 14, + "collapsed": 7, + "flagged": 9, + "no_output": 7 + }, + "produced_an_equilibrium": 23, + "slices": 37 + }, + "vs_reference": { + "changed_vs_reference": { + "346": [ + "accepted", + "no_output" + ], + "348": [ + "accepted", + "no_output" + ] + }, + "compared_on": 23, + "lost": [ + 346, + 348 + ], + "metrics": { + "aminor": { + "max_abs": 0.013049499999997494, + "median_abs": 0.00846289999999783, + "median_relative": 0.0003690250291489975, + "reference_median": 22.9331328, + "slices": 23 + }, + "area": { + "max_abs": 1.251700000000028, + "median_abs": 0.4321700000000419, + "median_relative": 0.0001688043347619051, + "reference_median": 2560.18307, + "slices": 23 + }, + "betap": { + "max_abs": 6.786330000000361e-06, + "median_abs": 3.286199999999115e-06, + "median_relative": 0.000386333034965749, + "reference_median": 0.00850613254, + "slices": 23 + }, + "chisq": { + "max_abs": 0.0, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 22.7598683, + "slices": 23 + }, + "elong": { + "max_abs": 0.003107720000000036, + "median_abs": 0.0008544000000001439, + "median_relative": 0.0005507012652729889, + "reference_median": 1.55147637, + "slices": 23 + }, + "li": { + "max_abs": 0.003900571000000075, + "median_abs": 0.0011059019999999364, + "median_relative": 0.0016900563791028837, + "reference_median": 0.654358052, + "slices": 23 + }, + "q95": { + "max_abs": 0.017049079999999606, + "median_abs": 0.006287109999999707, + "median_relative": 0.0011754815420692317, + "reference_median": 5.34853996, + "slices": 23 + }, + "qstar": { + "max_abs": 0.015715460000000014, + "median_abs": 0.004147760000000389, + "median_relative": 0.0008893891713699998, + "reference_median": 4.66360524, + "slices": 23 + }, + "rm": { + "max_abs": 0.45681720000000325, + "median_abs": 1.499999996212864e-06, + "median_relative": 4.219779484993591e-08, + "reference_median": 35.5468811, + "slices": 23 + }, + "terror": { + "max_abs": 0.00039947235999999997, + "median_abs": 6.585277000000032e-05, + "median_relative": 0.008565752809179854, + "reference_median": 0.00768791389, + "slices": 23 + }, + "volume": { + "max_abs": 309.65299999993294, + "median_abs": 154.04299999994691, + "median_relative": 0.0003014005205219976, + "reference_median": 511090.69, + "slices": 23 + }, + "zm": { + "max_abs": 1.17182663, + "median_abs": 1.932830000000039e-07, + "median_relative": null, + "reference_median": 4.89443833e-05, + "slices": 23 + } + }, + "recovered": [] + } + }, + "D": { + "returncode": 0, + "seconds": 69.59109208296286, + "summary": { + "accepted": 18, + "bound_failures": 10, + "by_phase": { + "flat_top": { + "accepted": 4, + "flagged": 2, + "no_output": 1 + }, + "ramp_down": { + "accepted": 14, + "no_output": 3 + }, + "ramp_up": { + "collapsed": 7, + "flagged": 5, + "no_output": 1 + } + }, + "collapsed": 7, + "findax_failures": 2, + "outcomes": { + "accepted": 18, + "collapsed": 7, + "flagged": 7, + "no_output": 5 + }, + "produced_an_equilibrium": 25, + "slices": 37 + }, + "vs_reference": { + "changed_vs_reference": { + "327": [ + "flagged", + "no_output" + ], + "330": [ + "flagged", + "no_output" + ], + "345": [ + "no_output", + "accepted" + ], + "347": [ + "no_output", + "accepted" + ] + }, + "compared_on": 23, + "lost": [ + 327, + 330 + ], + "metrics": { + "aminor": { + "max_abs": 0.018963890000000205, + "median_abs": 0.006269700000000711, + "median_relative": 0.00028211479535719376, + "reference_median": 22.2239319, + "slices": 23 + }, + "area": { + "max_abs": 2.44948500000001, + "median_abs": 1.3285299999997733, + "median_relative": 0.000553868604796873, + "reference_median": 2398.63749, + "slices": 23 + }, + "betap": { + "max_abs": 2.4162075000000015e-05, + "median_abs": 5.6266000000001e-06, + "median_relative": 0.0007155073006402111, + "reference_median": 0.00786379118, + "slices": 23 + }, + "chisq": { + "max_abs": 9.999999786569624e-11, + "median_abs": 0.0, + "median_relative": 0.0, + "reference_median": 19.1547644, + "slices": 23 + }, + "elong": { + "max_abs": 0.017792119999999967, + "median_abs": 0.0009462299999998258, + "median_relative": 0.0006122375059918538, + "reference_median": 1.54552766, + "slices": 23 + }, + "li": { + "max_abs": 0.016658938000000068, + "median_abs": 0.002535879000000074, + "median_relative": 0.0038641989208616284, + "reference_median": 0.656249601, + "slices": 23 + }, + "q95": { + "max_abs": 0.08929652999999993, + "median_abs": 0.0027909499999996257, + "median_relative": 0.0005312345450756525, + "reference_median": 5.25370578, + "slices": 23 + }, + "qstar": { + "max_abs": 0.05968769000000007, + "median_abs": 0.01236221999999998, + "median_relative": 0.0026507861115620452, + "reference_median": 4.66360524, + "slices": 23 + }, + "rm": { + "max_abs": 0.5858291000000015, + "median_abs": 0.234335999999999, + "median_relative": 0.006763244253020933, + "reference_median": 34.6484603, + "slices": 23 + }, + "terror": { + "max_abs": 0.0016584213099999998, + "median_abs": 8.862349000000012e-05, + "median_relative": 0.011527638221244453, + "reference_median": 0.00768791389, + "slices": 23 + }, + "volume": { + "max_abs": 412.80100000003586, + "median_abs": 178.08499999996275, + "median_relative": 0.00037938443574178063, + "reference_median": 469405.129, + "slices": 23 + }, + "zm": { + "max_abs": 1.28896738, + "median_abs": 1.0969243999999997e-05, + "median_relative": null, + "reference_median": 4.27058446e-05, + "slices": 23 + } + }, + "recovered": [ + 345, + 347 + ] + } + } + }, + "phases": { + "flat_top": 7, + "ramp_down": 18, + "ramp_up": 16 + }, + "window": { + "end": 0.35171999999996445, + "slices": 41, + "start": 0.31219999999997977 + } + } + }, + "toolchain": { + "efit": { + "build_revision": "4d10ed5-dirty", + "build_root": "/Users/yun/git/efit", + "mtime": "2026-09-06T13:43:02+00:00", + "path": "/Users/yun/git/efit/vaft-install/bin/efit", + "role": "efit", + "sha256": "b1d0bb21c9496503233e8b5ee7ede0c217b497e65a7f59f20f8f6e1f662f0c4d", + "size": 1144248 + }, + "efund": { + "build_revision": "4d10ed5-dirty", + "build_root": "/Users/yun/git/efit", + "mtime": "2026-09-06T13:43:02+00:00", + "path": "/Users/yun/git/efit/vaft-install/bin/efund", + "role": "efund", + "sha256": "2d9ff9d7ed5bebc71ada3c40e301f05685974d78363336ced21889b0eb67dcbb", + "size": 109048 + } + }, + "verdict": [ + "Yield against the routine configuration, over 82 plasma slices: A 46 produced / 30 accepted, B 36 produced / 23 accepted, C 44 produced / 28 accepted, D 36 produced / 23 accepted.", + "**Neither the domain nor the grid recovers a slice.** Against case A: B -10, C -2, D -10 equilibria. The slices that produce nothing are not produced by a smaller box or a finer grid either, so `bound` and `findax` are reporting something other than resolution and the cause is elsewhere.", + "**Halving the vertical cell changes the answer by less than a percent** where it changes it at all: the cell aspect ratio goes from 2.6 to 1.3 and every global quantity moves by well under 1 %. The magnetic axis height is the one that responds, and it is quoted in absolute terms because it sits at zero on an up-down symmetric machine." + ] +} diff --git a/test/test_efit_domain_grid.py b/test/test_efit_domain_grid.py new file mode 100644 index 00000000..037986cb --- /dev/null +++ b/test/test_efit_domain_grid.py @@ -0,0 +1,248 @@ +"""The domain and grid study (#459): its guards, and the result it recorded. + +The study runs EFIT; what is pinned here is the bookkeeping it rests on and +the findings, so a later change that quietly reverses either fails loudly. +The guard that matters most is the table check: EFIT reads the computational +box out of the Green table and the file name records only the grid, so a case +run against a table built for a different box is undetectable from EFIT's own +output. That check is tested against a table it must refuse. +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import numpy as np +import pytest + +TABLE = Path(__file__).resolve().parent / "data" / "efit_domain_grid.json" +SCRIPT = Path(__file__).resolve().parents[1] / "workflow" / "efit_numerics" / "domain_grid.py" + + +@pytest.fixture(scope="module") +def module(): + spec = importlib.util.spec_from_file_location("domain_grid", SCRIPT) + loaded = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[spec.name] = loaded + try: + spec.loader.exec_module(loaded) + except Exception: + del sys.modules[spec.name] + raise + yield loaded + sys.modules.pop(spec.name, None) + + +@pytest.fixture(scope="module") +def table(): + return json.loads(TABLE.read_text(encoding="utf-8")) + + +def _table_directory(tmp_path, *, nw, nh, domain): + directory = tmp_path / f"table_{nw}x{nh}" + directory.mkdir() + (directory / f"ec{nw}{nh}.ddd").write_bytes(b"") + rleft, rright, zbotto, ztop = domain + (directory / "efund_table_manifest.json").write_text( + json.dumps( + { + "efund": { + "config": { + "grid": { + "nw": nw, "nh": nh, + "rleft": rleft, "rright": rright, "zbotto": zbotto, "ztop": ztop, + } + }, + "config_sha256": "0" * 64, + }, + "table": {"identity": "1" * 64}, + "generated_at": "2026-09-07T00:00:00+00:00", + } + ), + encoding="utf-8", + ) + return directory + + +def test_a_table_built_for_another_box_is_refused(module, tmp_path): + """The check EFIT cannot make for itself. + + Two tables for the same grid and different domains are named identically, + and EFIT takes `rgrid`/`zgrid` from whichever it opens. Running case B + against case A's table would silently reconstruct a different machine and + report nothing unusual, so the manifest is read here instead. + """ + routine = _table_directory(tmp_path, nw=129, nh=129, domain=module.ROUTINE_DOMAIN) + + assert module.verify_table(routine, module.CASES["A"])["table"]["identity"] + + with pytest.raises(ValueError, match="box"): + module.verify_table(routine, module.CASES["B"]) + with pytest.raises(ValueError, match="129x129"): + module.verify_table(routine, module.CASES["C"]) + + +def test_a_directory_without_a_manifest_is_not_a_table(module, tmp_path): + """File names carry the grid and nothing else, so they are not evidence.""" + bare = tmp_path / "bare" + bare.mkdir() + (bare / "ec129129.ddd").write_bytes(b"") + with pytest.raises(FileNotFoundError, match="efund_table_manifest.json"): + module.verify_table(bare, module.CASES["A"]) + + +def test_an_incomplete_table_is_refused(module, tmp_path): + directory = _table_directory(tmp_path, nw=129, nh=129, domain=module.ROUTINE_DOMAIN) + (directory / "ec129129.ddd").unlink() + with pytest.raises(FileNotFoundError, match="ec129129.ddd"): + module.verify_table(directory, module.CASES["A"]) + + +def test_the_cases_are_a_two_by_two_and_the_cells_follow_from_them(module): + grids = {name: tuple(case["grid"]) for name, case in module.CASES.items()} + domains = {name: tuple(case["domain"]) for name, case in module.CASES.items()} + assert grids["A"] == grids["B"] and grids["C"] == grids["D"] + assert domains["A"] == domains["C"] and domains["B"] == domains["D"] + assert grids["A"] != grids["C"] and domains["A"] != domains["B"] + + # The routine box is 9.0 x 23.4 mm: 2.6 times taller than it is wide, which + # is the anisotropy #459 asks about. + width, height = module.cell_size(module.CASES["A"]) + assert width == pytest.approx((1.2 - 0.05) / 128 * 1e3) + assert height / width == pytest.approx(2.61, abs=0.01) + # Doubling nh roughly squares that away. + assert module.cell_size(module.CASES["C"])[1] / width == pytest.approx(1.30, abs=0.01) + + # `rleft` is 0.05 m in every case. #459's text quotes the current box as + # starting at R = 0, and it never has: the Green functions are singular on + # the machine axis. + assert all(domain[0] == 0.05 for domain in domains.values()) + + +def test_a_ratio_is_withheld_when_there_is_nothing_to_be_relative_to(module): + """`zm` sits at zero on an up-down symmetric machine. + + Dividing a 1 cm axis shift by a median of ~0 reports a change of millions + of percent, which is arithmetic rather than physics. The absolute numbers + are always reported; the ratio is only reported when it means something. + """ + def slices(values, key): + return [ + { + "time_ms": 300 + index, + "collapsed": False, + "solver_errors": [], + "afile": {"jflag": 1, key: value}, + } + for index, value in enumerate(values) + ] + + # A quantity that is exactly zero has no scale at all. + flat = module.compare(slices([0.0, 0.0, 0.0], "zm"), slices([1.0, 1.0, 1.0], "zm")) + assert flat["metrics"]["zm"]["median_relative"] is None + assert flat["metrics"]["zm"]["median_abs"] == pytest.approx(1.0) + + # And the shape the real runs have: a median in the tens of microns beside + # an excursion of a centimetre, which is a quantity centred on zero rather + # than a quantity of size 3e-5. + centred = module.compare( + slices([3.7e-5, -2.1e-5, 1.2], "zm"), slices([1.2, 1.2, 1.2], "zm") + ) + assert centred["metrics"]["zm"]["median_relative"] is None + + # Where there is a scale, the ratio is reported. + offset = module.compare(slices([20.0, 21.0, 22.0], "rm"), slices([20.1, 21.1, 22.1], "rm")) + assert offset["metrics"]["rm"]["median_relative"] == pytest.approx(0.1 / 21.0, rel=1e-6) + + +def test_the_phases_come_from_the_current_and_not_from_the_clock(module): + from omas import ODS + + ods = ODS(consistency_check=False) + current = [10.0, 60.0, 100.0, 95.0, 40.0] + for index, value in enumerate(current): + ods[f"equilibrium.time_slice.{index}.constraints.ip.measured"] = value * 1e3 + labels = module.phases(ods, np.asarray([0.300, 0.301, 0.302, 0.303, 0.304])) + assert labels == {300: "ramp_up", 301: "ramp_up", 302: "flat_top", 303: "flat_top", 304: "ramp_down"} + + +def test_every_case_was_run_against_a_table_that_matches_it(table): + """Without this the study is comparing four unknown configurations.""" + for name, case in table["cases"].items(): + assert case["table"]["table_identity"], name + assert case["cell_mm"][0] > 0 and case["cell_mm"][1] > 0 + identities = {case["table"]["table_identity"] for case in table["cases"].values()} + assert len(identities) == len(table["cases"]), "two cases share a table" + # One acceptance bar across the 2x2, or the cases are judged differently. + assert table["acceptance_envelope"]["aminor_min"] > 0 + + +def test_neither_the_domain_nor_the_grid_recovers_a_slice(table): + """#459's answer for EFIT, and the reason the losses go back to the solver. + + If a future change makes a smaller box or a finer grid recover the block, + this fails and the conclusion has to be revisited -- which is what it is + for. + """ + totals = {name: {"produced": 0, "accepted": 0, "slices": 0} for name in table["cases"]} + for block in table["shots"].values(): + for name, record in block["cases"].items(): + summary = record["summary"] + totals[name]["produced"] += summary["produced_an_equilibrium"] + totals[name]["accepted"] += summary["accepted"] + totals[name]["slices"] += summary["slices"] + + assert totals["A"]["slices"] == 82 + for name in ("B", "C", "D"): + assert totals[name]["slices"] == totals["A"]["slices"], name + assert totals[name]["produced"] <= totals["A"]["produced"], name + + # The reduced domain is not neutral, it is worse, and it is worse in the + # specific way the box predicts: a nearer grid edge, so `findax` starts + # rejecting separatrix points for being off grid. + assert totals["B"]["produced"] < totals["A"]["produced"] - 5 + edge_failures = { + name: sum(block["cases"][name]["summary"]["findax_failures"] for block in table["shots"].values()) + for name in table["cases"] + } + assert edge_failures["B"] > edge_failures["A"] + assert edge_failures["D"] > edge_failures["C"] + + +def test_halving_the_vertical_cell_does_not_move_the_reconstruction(table): + """The resolution answer: under a percent on every global quantity. + + `terror` is excluded because it is the Grad-Shafranov residual rather than + a physical quantity, and `zm` because it sits at zero -- its ratio is + withheld by design and its absolute change is asserted instead. + """ + physical = ("aminor", "area", "betap", "elong", "li", "q95", "qstar", "rm", "volume") + for shot, block in table["shots"].items(): + metrics = (block["cases"]["C"].get("vs_reference") or {}).get("metrics", {}) + assert metrics, shot + for name in physical: + relative = metrics[name]["median_relative"] + assert relative is not None and relative < 0.01, (shot, name, relative) + # The fit itself does not move at all: EFIT reports the same chi-square. + assert metrics["chisq"]["max_abs"] == 0.0, shot + + # Where the grid does show: the axis height is resolved to the Z cell. On + # 39915 every compared slice moves by 1.172 cm, which is half of the + # coarse cell (23.44 mm) to the digit, so this is quantisation and not a + # different equilibrium. + axis = table["shots"]["39915"]["cases"]["C"]["vs_reference"]["metrics"]["zm"] + coarse_cell_mm = (1.5 - -1.5) / 128 * 1e3 + assert axis["median_abs"] == pytest.approx(coarse_cell_mm / 2 / 10.0, rel=1e-3) + assert axis["median_abs"] == pytest.approx(axis["max_abs"], rel=1e-4) + + +def test_the_report_renders_from_the_committed_table(module, table): + text = module.markdown(table) + assert text.startswith("# The computational domain and the grid") + for shot in table["shots"]: + assert f"## {shot}" in text + assert "What this says" in text diff --git a/test/test_efit_termination_baseline.py b/test/test_efit_termination_baseline.py index 17c23410..eaee40fb 100644 --- a/test/test_efit_termination_baseline.py +++ b/test/test_efit_termination_baseline.py @@ -53,6 +53,42 @@ def module(): """ +PRE_ITERATION_COLLAPSE = """ + r= 0 t= 328 it= 1 chi2=4.90E+00 zm= 3.99E-09 err=5.364E+01 dz=-7.174E-08 chigam= 0.00E+00 + r= 0 t= 328 it= 2 chi2=1.61E-09 zm= 9.25E-09 err=1.512E+00 dz= 5.262E-09 chigam= 0.00E+00 +WARNING in fit at r= 0, t= 328: iconvr=2 satisfied, exiting +ERROR in bound at r= 0, t= 329: First and last contour points are too far apart +ERROR in bound at r= 0, t= 330: First and last contour points are too far apart +INFO in efit at r= 0, t= 331: Done processing +""" + + +def test_a_slice_that_fails_before_its_first_iteration_is_not_the_previous_one(module): + """A real run's tail, and a reading that got it wrong twice over. + + `bound` can reject a slice before the first Picard iteration, so nothing + is printed for it but the error. Delimiting on the iteration counter alone + handed both errors to slice 328 -- which had converged -- and dropped 329 + and 330 from the universe entirely, shortening the denominator and giving + a good slice two failures that were not its own. + """ + slices = module.parse_slices(PRE_ITERATION_COLLAPSE) + assert [item["time_ms"] for item in slices] == [328, 329, 330] + + converged = slices[0] + assert converged["solver_errors"] == [] + assert converged["iconvr"] == 2 + + for item in slices[1:]: + assert [error["routine"] for error in item["solver_errors"]] == ["bound"] + assert item["iterations_n"] == 0 + # No iterations means no null-solution signature to read, so these are + # not "collapsed" in the chi-square sense; they produce nothing, which + # is what the yield count is about. + assert item["collapsed"] is False + assert item["exit_path"] == "solver_error" + + def test_slices_are_delimited_by_the_iteration_counter_not_the_time(module): """EFIT prints whole milliseconds, so two slices can share a printed time.""" text = CONVERGED + COLLAPSED + BOUNDARY_ERROR diff --git a/workflow/efit_numerics/README.md b/workflow/efit_numerics/README.md index 7d78b58b..ab99ce73 100644 --- a/workflow/efit_numerics/README.md +++ b/workflow/efit_numerics/README.md @@ -306,3 +306,117 @@ solution into the block from the working side, which is a different mechanism from re-seeding. But #588's premise, that the first slice must be seeded into a good basin before continuation can help, is now answered: the basin is narrow and off-centre, and a better seed is available and justified. + +## The computational domain and the grid (#459) + +`domain_grid.py` runs #459's 2×2 over the three reference discharges: the +routine box against a reduced one, at 129×129 against 129×257. + +### What EFIT's grid contract actually is + +Phase 1A's questions have answers, and they are all in the source rather than +in the documentation. + +- **`nw` and `nh` are runtime arguments**, `argv(1)` and `argv(2)` + (`efit.F90:93-104`), and `nh` defaults to `nw`. A rectangular grid needs no + rebuild; `efit 129 257` is enough. Both are read with an `i4` format, so + 9999 is the ceiling. +- **The Green table file name encodes the grid and only the grid** + (`table_name_ch`, `tables.F90:34`), so a 129×257 run looks for + `ec129257.ddd` and fails loudly if it is absent. +- **EFIT has no independent notion of the box.** It reads `rgrid` and `zgrid` + out of the table (`tables.F90:158`) and derives `drgrid`, `dzgrid` and + `darea` from them (`setup_data_fetch.F90:579`). The domain is whatever EFUND + baked in, from `&in5`'s `rleft`, `rright`, `zbotto` and `ztop` + (`efund_read.f90:200,301-304`). +- **So two tables for the same grid and different boxes are named + identically**, and running against the wrong one is undetectable from EFIT's + output. The manifest EFUND writes beside the table is the only record, and + `verify_table` reads it before any case runs. +- `EFUNDConfig` already carried `nw`, `nh` and the four box edges; the + regeneration workflow exposed only the grid, and now exposes the box too. +- One hazard not exercised here: `npoint`, which sizes the boundary-point + arrays, is chosen from `nw` alone (`efit.F90:114-120`), so it does not grow + with `nh`. + +`rleft` is 0.05 m in every case. #459's text quotes the current box as +starting at R = 0; it never has, because the Green functions are singular on +the machine axis. + +### The 2×2, over 82 plasma slices + +| case | domain (m) | grid | cell (mm) | aspect | produced | accepted | collapsed | `bound` | `findax` | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| A | R 0.05–1.2, Z ±1.5 | 129×129 | 9.0 × 23.4 | 2.61 | **46** | **30** | 29 | 41 | 2 | +| B | R 0.05–1.0, Z ±1.35 | 129×129 | 7.4 × 21.1 | 2.84 | 36 | 23 | 29 | 33 | 13 | +| C | R 0.05–1.2, Z ±1.5 | 129×257 | 9.0 × 11.7 | 1.30 | 44 | 28 | 29 | 47 | 4 | +| D | R 0.05–1.0, Z ±1.35 | 129×257 | 7.4 × 10.5 | 1.42 | 36 | 23 | 29 | 33 | 13 | + +**Neither lever recovers a slice.** The reduced domain costs ten equilibria +and the finer grid costs two. B and D are identical on every count on every +shot, so once the box is reduced the grid changes nothing at all. + +**The reduced domain fails in the way the box predicts.** `findax` rejects a +separatrix point that lands within two cells of the grid edge +(`find_axis.F90:308-311`); bringing the edge in from |Z| = 1.5 m to 1.35 m +takes those failures from 2 to 13. The proposal in #459 was to trim unused +vacuum volume, and on this evidence the volume is not unused. + +**The collapse block is untouched.** 29 slices collapse in every one of the +four cases — the same 29 that #588 showed no seed can rescue. Initialization, +the domain and the grid have now each been ruled out. That block is not a +spatial-discretisation problem. + +The shots do not agree in detail: 41672 gains two accepted slices from the +reduced domain while 39915 and 41524 lose five and four. A recommendation from +three discharges would be premature, and #459 asks for full-discharge evidence +across more of them before a default moves. + +### How much the reconstruction itself moves + +Comparing only slices both cases reconstructed, the resolution change (C − A) +moves every global quantity by well under a percent: + +| quantity | largest median change across the three shots | +| --- | --- | +| chi-square | 0 exactly | +| minor radius, area, volume, elongation, axis R | ≤ 0.06 % | +| q95, qstar, β_p | ≤ 0.17 % | +| li | 0.26 % | +| Grad-Shafranov error | 2.1 % | + +The one place the grid does show is the **magnetic axis height**, and it shows +as quantisation rather than as a different equilibrium: on 39915 every +compared slice moves by 1.1718 cm, which is half the coarse Z cell +(23.4375 mm) to five digits, and the same 1.1718 cm appears as the largest +single change on both other shots. `zm` sits at zero on an up-down symmetric +machine, so its *relative* change is not reported — dividing a centimetre by a +median of 37 microns produces a number in the millions of percent, which is +arithmetic and not physics. + +### A defect in the log reading, found here and fixed + +`bound` can reject a slice **before its first Picard iteration**, printing only +`ERROR in bound at r=..., t=...`. `parse_slices` delimited slices on the +iteration counter alone, so it handed those errors to the *previous* slice and +dropped the slice itself. On 39915 the run ends with two such rejections, so a +converged slice was carrying two failures that were not its own and the +denominator was 22 where EFIT had processed 24. + +The parser now opens a slice when a solver error names a time the current +slice does not have. **This changes the denominators of the earlier +studies**: the reference set has 82 plasma slices, not 77, and case A here +produces 46 equilibria where the merged #171 baseline reported 31. Both +studies' *relative* comparisons stand, because every row was read by the same +parser; the absolute counts in the baseline and seed reports predate the fix. + +### An open question this raised + +Case A is the routine box and the routine grid, and it still produces 46 +equilibria against the merged baseline's 31. The two runs differ in two things +at once — a freshly generated Green table instead of the packaged one, and a +freshly generated `mhdin.dat` geometry instead of the packaged one — so which +is responsible is not established here. #194's A/B compared the tables alone +on four slices and found no difference; this is the first evidence that +something in that pair matters on the marginal slices, and it matters more +than either lever #459 set out to test. It needs its own controlled A/B. diff --git a/workflow/efit_numerics/baseline_termination.py b/workflow/efit_numerics/baseline_termination.py index 06819c1f..29ae7bbf 100644 --- a/workflow/efit_numerics/baseline_termination.py +++ b/workflow/efit_numerics/baseline_termination.py @@ -85,6 +85,15 @@ def parse_slices(text: str) -> list[dict[str, Any]]: printed after a slice's last iteration and before the next slice's first belongs to that slice: its exit path, its solver errors and the acceptance failures ``chkerr`` reports. + + With one exception, and it is not a small one. A slice that fails in + ``bound`` before the first Picard iteration prints no iteration line at + all, only ``ERROR in bound at r=..., t=...``. Delimiting on the iteration + counter alone hands that error to the *previous* slice and loses the slice + itself, so a run ending in a run of pre-iteration collapses reports both a + short universe and a slice carrying failures that are not its own. Solver + errors therefore also open a slice when they name a time the current slice + does not have. """ slices: list[dict[str, Any]] = [] current: dict[str, Any] | None = None @@ -114,6 +123,15 @@ def start(time_ms: int) -> dict[str, Any]: {"n": iteration, "chi2": float(found.group(3)), "gs_error": float(found.group(4))} ) continue + found = _SOLVER_ERROR.search(line) + if found: + named = int(found.group(2)) + if current is None or current["time_ms"] != named: + if current is not None: + slices.append(current) + current = start(named) + current["solver_errors"].append({"routine": found.group(1), "detail": found.group(3).strip()}) + continue if current is None: continue found = _ICONVR.search(line) @@ -121,10 +139,6 @@ def start(time_ms: int) -> dict[str, Any]: current["iconvr"] = int(found.group(1)) current["exit_path"] = f"iconvr={found.group(1)}" continue - found = _SOLVER_ERROR.search(line) - if found: - current["solver_errors"].append({"routine": found.group(1), "detail": found.group(3).strip()}) - continue if _FAILED.search(line): current["accepted"] = False continue diff --git a/workflow/efit_numerics/domain_grid.py b/workflow/efit_numerics/domain_grid.py new file mode 100644 index 00000000..f8258d54 --- /dev/null +++ b/workflow/efit_numerics/domain_grid.py @@ -0,0 +1,674 @@ +"""What the computational box and the grid are worth, measured (#459). + + PYTHONPATH=$PWD EFITHOME=~/git/efit/vaft-install \\ + python workflow/efit_numerics/domain_grid.py --output /scratch/domain \\ + --tables A=/tables/a,B=/tables/b,C=/tables/c,D=/tables/d + +The #171 baseline loses 46 of 77 plasma slices to two failures that are both +statements about the grid rather than about the fit: ``bound`` cannot close a +contour, and ``findax`` refuses a separatrix point that lands within two cells +of the box edge. The routine box is R 0.05-1.2 m by Z +-1.5 m on 129x129, +which is 9.0 mm across and 23.4 mm tall -- a cell aspect ratio of 2.6 -- and +it extends to |Z| = 1.5 m where the limiter reaches only 1.185 m. + +So this runs #459's 2x2: the domain and the grid, separately and together. + +===== ======================== ========= ============================== +case domain grid what its difference from A is +===== ======================== ========= ============================== +A R 0.05-1.2, Z +-1.5 129x129 the routine configuration +B R 0.05-1.0, Z +-1.35 129x129 the domain alone +C R 0.05-1.2, Z +-1.5 129x257 the cell aspect ratio alone +D R 0.05-1.0, Z +-1.35 129x257 both +===== ======================== ========= ============================== + +**The tables are the experiment.** EFIT has no independent notion of the +computational box: it reads ``rgrid`` and ``zgrid`` straight out of the Green +table (``tables.F90:158``) and derives ``drgrid``, ``dzgrid`` and ``darea`` +from them (``setup_data_fetch.F90:579``). The box is therefore whatever EFUND +baked in, and the table file name encodes only ``nw`` and ``nh`` +(``table_name_ch``, ``tables.F90:34``) -- two tables for the same grid and +different boxes are named identically. A case run against the wrong table is +undetectable from EFIT's output, so every table directory is verified against +its case's declaration here before anything is run. + +Generate the four tables with ``workflow/efit_tables/regenerate_legacy_table.py``. +""" + +from __future__ import annotations + +import argparse +import copy +import gzip +import importlib.util +import json +import os +import shutil +import sys +import tempfile +import time as _clock +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Sequence + +import numpy as np + +SCHEMA = 1 +REPOSITORY = Path(__file__).resolve().parents[2] +BASELINE = REPOSITORY / "workflow" / "efit_numerics" / "baseline_termination.py" +REFERENCE_SET = REPOSITORY / "test" / "data" / "efit_reference_set.json" +DEFAULT_TABLE = REPOSITORY / "test" / "data" / "efit_domain_grid.json" + +#: The routine box. ``rleft`` is 0.05 m and not the 0.0 m #459's text quotes: +#: the Green functions are singular on the machine axis, so the box has never +#: started there. +ROUTINE_DOMAIN = (0.05, 1.2, -1.5, 1.5) +REDUCED_DOMAIN = (0.05, 1.0, -1.35, 1.35) + +CASES: dict[str, dict[str, Any]] = { + "A": {"grid": (129, 129), "domain": ROUTINE_DOMAIN, "role": "the routine configuration"}, + "B": {"grid": (129, 129), "domain": REDUCED_DOMAIN, "role": "the domain alone"}, + "C": {"grid": (129, 257), "domain": ROUTINE_DOMAIN, "role": "the cell aspect ratio alone"}, + "D": {"grid": (129, 257), "domain": REDUCED_DOMAIN, "role": "both"}, +} + +#: What a reconstruction is compared on. Every one is an a-file scalar, so the +#: comparison needs no re-derivation and cannot drift from what EFIT reported. +METRICS = ( + "rm", "zm", "aminor", "elong", "area", "volume", + "q95", "qstar", "li", "betap", "chisq", "terror", +) + +#: Lengths in the a-file are centimetres; these are the ones to say so about. +CENTIMETRES = {"rm", "zm", "aminor", "rcntr", "zcntr", "rcurrt", "zcurrt"} + + +def _module(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def load_product(path: Path): + from omas import load_omas_json + + with gzip.open(path, "rt", encoding="utf-8") as handle: + payload = handle.read() + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as staged: + staged.write(payload) + name = staged.name + try: + return load_omas_json(name, consistency_check=False) + finally: + Path(name).unlink(missing_ok=True) + + +def verify_table(directory: Path, case: dict[str, Any]) -> dict[str, Any]: + """Refuse a table that is not the one this case declares. + + EFIT cannot do this check. It opens ``ec.ddd`` by name and takes + the box from inside it, so a table generated for a different domain loads + silently and reconstructs a different machine. The manifest EFUND wrote + beside the table is the only record of the box, and this is where it is + read. + """ + manifest_path = directory / "efund_table_manifest.json" + if not manifest_path.is_file(): + raise FileNotFoundError( + f"{directory} has no efund_table_manifest.json; regenerate it with " + "workflow/efit_tables/regenerate_legacy_table.py rather than trusting the file names" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + recorded = manifest["efund"]["config"]["grid"] + grid = (int(recorded["nw"]), int(recorded["nh"])) + domain = tuple(float(recorded[name]) for name in ("rleft", "rright", "zbotto", "ztop")) + if grid != tuple(case["grid"]): + raise ValueError(f"{directory}: table is {grid[0]}x{grid[1]}, case declares {case['grid']}") + if not np.allclose(domain, case["domain"], atol=1e-9): + raise ValueError(f"{directory}: table box is {domain}, case declares {case['domain']}") + expected = f"ec{grid[0]}{grid[1]}.ddd" + if not (directory / expected).is_file(): + raise FileNotFoundError(f"{directory}: no {expected}; the table is incomplete") + return manifest + + +#: EFIT products EFUND does not write, and the run directory is not usable +#: without them. `lim.dat` is an EFIT input that lives in the table directory, +#: and `&incheck` is read from `mhdin.dat` there. +EFIT_ONLY_INPUTS = ("lim.dat",) + + +def stage_case(directory: Path, *, root: Path, name: str, envelope: Any) -> Path: + """A directory EFIT can actually run against, assembled from a table. + + EFUND writes the Green tables and its own ``mhdin.dat``; EFIT additionally + needs ``lim.dat`` in the same directory and reads its acceptance envelope + from ``&incheck`` there. Neither is an EFUND product, so a freshly + generated table directory is a complete table and an incomplete run + directory. + + The envelope is passed in rather than derived per case on purpose. Its + ``aminor_min`` is a resolution statement -- so many grid cells -- and would + move with the grid, which would leave the 2x2 comparing four cases against + four different acceptance bars. #459 requires everything but the domain and + the grid to be held fixed, so the bar is case A's throughout, and what each + case's own floor would have been is recorded instead. + + ``root`` must be short: EFIT truncates ``TABLE_DIR`` at 100 characters and + then fails opening ``lim.dat`` with a message about the limiter. + """ + from vaft.code.efit.efund import write_mhdin + from vaft.data.resources import data_path + from vaft.machine_mapping.efund_geometry import efund_geometry_from_static + from vaft.omas.vest_upstream import build_static_ods + + staged = root / name + if len(str(staged)) + 1 > 100: + raise ValueError( + f"{staged} is {len(str(staged))} characters; EFIT truncates TABLE_DIR at 100. " + "Pass a shorter --stage." + ) + shutil.rmtree(staged, ignore_errors=True) + staged.mkdir(parents=True) + for path in sorted(directory.iterdir()): + if path.name in {"mhdin.dat"} or path.is_dir(): + continue + (staged / path.name).symlink_to(path.resolve()) + for name_ in EFIT_ONLY_INPUTS: + (staged / name_).symlink_to(Path(data_path(f"efit/{name_}")).resolve()) + + manifest = json.loads((directory / "efund_table_manifest.json").read_text(encoding="utf-8")) + from vaft.code.efit.efund import EFUNDConfig + + recorded = manifest["efund"]["config"] + config = EFUNDConfig( + workdir=staged, + device=recorded["device"], + **recorded["grid"], + **recorded["flags"], + **recorded["quadrature"], + ) + ods, era_manifest = build_static_ods(manifest["machine"]["era"]) + geometry = efund_geometry_from_static(ods, manifest=era_manifest) + write_mhdin(geometry, config, staged / "mhdin.dat", envelope=envelope) + return staged + + +def cell_size(case: dict[str, Any]) -> tuple[float, float]: + """Cell width and height in millimetres.""" + rleft, rright, zbotto, ztop = case["domain"] + nw, nh = case["grid"] + return ((rright - rleft) / (nw - 1) * 1e3, (ztop - zbotto) / (nh - 1) * 1e3) + + +def prepare_shot(shot: int, product: Path, *, workdir: Path, tables: str, tstep: float, average_window: float): + """Constraints for one shot against one table directory. + + Rebuilt per case rather than once per shot: the table directory is written + into the k-file, so constraints built against one box cannot be reused for + another. + """ + from vaft.code.efit import generate_constraints_ods + from vaft.omas.vacuum_magnetics import quality_gate + from vaft.validation.efit_channels import decide_efit_channels, efit_probe_count + + baseline = _module(BASELINE, "baseline_termination") + wrapper = _module( + REPOSITORY / "workflow" / "automatic_pipeline_1_routine_data_processing" / "generate_constraints_ods.py", + "generate_constraints_ods_wrapper", + ) + + ods = load_product(product) + times, window = wrapper._select_times(ods, "auto", tstep, None, None) + times = np.asarray(times, dtype=float) + + source = copy.deepcopy(ods) + if "equilibrium" in source: + del source["equilibrium"] + source["equilibrium.time"] = times + + gated, _ = quality_gate(source, window=(float(times[0]), float(times[-1]))) + decisions = decide_efit_channels(gated, times, nbprobe=efit_probe_count(gated)) + + workdir.mkdir(parents=True, exist_ok=True) + generate_constraints_ods( + gated, + shot, + str(workdir), + tables, + times, + list(baseline.DEFAULT_UNCERTAINTY), + list(baseline.DEFAULT_WEIGHTING), + decisions=decisions, + average_window=average_window, + ) + return gated, times, window, baseline + + +def run_case( + constraints_ods, + *, + shot: int, + times: np.ndarray, + workdir: Path, + efit: str, + grid: Sequence[int], + baseline, +) -> dict[str, Any]: + """One case, one whole discharge.""" + from vaft.code.efit.config import EFITScientificConfig + from vaft.code.efit.magnetic import EFITConfig, prepare_efit_inputs, run_efit + + shutil.rmtree(workdir, ignore_errors=True) + workdir.mkdir(parents=True) + scientific = EFITScientificConfig() + config = EFITConfig( + executable=efit, + workdir=workdir, + shot=shot, + times=times.tolist(), + # EFIT reads nw and nh as argv(1) and argv(2) (`efit.F90:93-104`); a + # single argument means a square grid. + args=tuple(str(value) for value in grid), + profile=scientific.profile, + initialization=scientific.initialization, + numerics=scientific.numerics, + constraints=scientific.constraints, + ) + inputs = prepare_efit_inputs(copy.deepcopy(constraints_ods), config) + started = _clock.perf_counter() + result = run_efit(inputs, config) + seconds = _clock.perf_counter() - started + + log = workdir / "run_efit.out" + text = log.read_text(errors="replace") if log.is_file() else result.stdout + slices = baseline.parse_slices(text) + + from vaft.data import read_aeqdsk + + by_time: dict[int, dict[str, Any]] = {} + for path in sorted(workdir.glob(f"a0{shot}.*")): + record = read_aeqdsk(path) + scalars = {name: float(record.scalars.get(name, float("nan"))) for name in METRICS} + by_time[int(round(float(record.time_ms)))] = {"jflag": int(record.jflag), **scalars} + for record in slices: + record["afile"] = by_time.get(int(record["time_ms"])) + return {"seconds": seconds, "returncode": result.returncode, "slices": slices} + + +def outcome(record: dict[str, Any]) -> str: + if record["collapsed"]: + return "collapsed" + if record["afile"] is None: + return "no_output" + return "accepted" if record["afile"]["jflag"] == 1 else "flagged" + + +def phases(ods, times: np.ndarray) -> dict[int, str]: + """Ramp-up, flat-top and ramp-down, from the plasma current itself. + + #459 asks for phase-resolved summaries and #579's semantics where + practical. The boundary used here is nine tenths of the peak current, + stated rather than tuned: everything at or above it is the flat top, and + the rest is named by which side of the peak it falls on. + """ + current = np.asarray( + [ + abs(float(ods[f"equilibrium.time_slice.{index}.constraints.ip.measured"])) + for index in range(times.size) + ], + dtype=float, + ) + peak = int(np.argmax(current)) + threshold = 0.9 * current[peak] + labels = {} + for index, time in enumerate(times): + key = int(round(float(time) * 1000.0)) + if current[index] >= threshold: + labels[key] = "flat_top" + else: + labels[key] = "ramp_up" if index < peak else "ramp_down" + return labels + + +def compare(reference: Sequence[dict[str, Any]], case: Sequence[dict[str, Any]]) -> dict[str, Any]: + """What changed against case A, slice by slice. + + Two different questions, kept apart. Which slices produce an equilibrium + at all is a yield question and is counted over every slice. How much a + reconstruction moved is only answerable where both cases produced one, so + the metric deltas are computed over that intersection and the size of the + intersection is reported beside them. + """ + before = {int(item["time_ms"]): item for item in reference} + after = {int(item["time_ms"]): item for item in case} + moved = { + time: [outcome(before[time]), outcome(after[time])] + for time in sorted(set(before) & set(after)) + if outcome(before[time]) != outcome(after[time]) + } + both = [ + time + for time in sorted(set(before) & set(after)) + if before[time]["afile"] and after[time]["afile"] + ] + deltas: dict[str, dict[str, float]] = {} + for name in METRICS: + pairs = [ + (before[time]["afile"].get(name, np.nan), after[time]["afile"].get(name, np.nan)) + for time in both + ] + # A metric an a-file did not carry is absent from the comparison + # rather than reported as a change of nothing. + pairs = [pair for pair in pairs if np.isfinite(pair[0]) and np.isfinite(pair[1])] + if not pairs: + continue + first = np.array([pair[0] for pair in pairs], dtype=float) + second = np.array([pair[1] for pair in pairs], dtype=float) + difference = second - first + # A relative change needs something to be relative to. `zm` sits at + # zero on an up-down symmetric machine, so dividing by its median + # turns a 1 cm shift into a percentage in the millions; the absolute + # numbers are the honest ones there and the ratio is withheld. + scale = float(np.median(np.abs(first))) + spread = float(np.max(np.abs(first))) + meaningful = scale > 0 and scale > 0.01 * spread + deltas[name] = { + "slices": len(pairs), + "median_abs": float(np.median(np.abs(difference))), + "max_abs": float(np.max(np.abs(difference))), + "reference_median": scale, + "median_relative": float(np.median(np.abs(difference)) / scale) if meaningful else None, + } + return { + "changed_vs_reference": moved, + "recovered": sorted( + time for time, (was, now) in moved.items() + if was in {"collapsed", "no_output"} and now in {"accepted", "flagged"} + ), + "lost": sorted( + time for time, (was, now) in moved.items() + if was in {"accepted", "flagged"} and now in {"collapsed", "no_output"} + ), + "compared_on": len(both), + "metrics": deltas, + } + + +def summarize(slices: Sequence[dict[str, Any]], labels: dict[int, str]) -> dict[str, Any]: + counts = Counter(outcome(item) for item in slices) + by_phase: dict[str, Counter] = {} + for item in slices: + by_phase.setdefault(labels.get(int(item["time_ms"]), "unknown"), Counter())[outcome(item)] += 1 + return { + "slices": len(slices), + "outcomes": dict(sorted(counts.items())), + "produced_an_equilibrium": sum(1 for item in slices if item["afile"]), + "collapsed": counts.get("collapsed", 0), + "accepted": counts.get("accepted", 0), + "bound_failures": sum( + 1 for item in slices for error in item["solver_errors"] if error["routine"] == "bound" + ), + "findax_failures": sum( + 1 for item in slices for error in item["solver_errors"] if error["routine"] == "findax" + ), + "by_phase": {phase: dict(sorted(counter.items())) for phase, counter in sorted(by_phase.items())}, + } + + +def markdown(payload: dict[str, Any]) -> str: + lines = ["# The computational domain and the grid (#459)", ""] + lines.append( + "EFIT takes `nw` and `nh` as its two command-line arguments and reads the box out of the " + "Green table, so a case is a table plus a pair of arguments. Every table directory below " + "was verified against its case's declared grid and box before the case ran." + ) + lines.append("") + lines.append("| case | domain (m) | grid | cell (mm) | aspect | role |") + lines.append("|---|---|---|---|---|---|") + for name, case in payload["cases"].items(): + rleft, rright, zbotto, ztop = case["domain"] + dr, dz = case["cell_mm"] + lines.append( + f"| {name} | R {rleft}–{rright}, Z {zbotto}–{ztop} | {case['grid'][0]}×{case['grid'][1]} " + f"| {dr:.1f} × {dz:.1f} | {dz / dr:.2f} | {case['role']} |" + ) + lines.append("") + for shot, block in payload["shots"].items(): + lines.append(f"## {shot}") + lines.append("") + lines.append("| case | produced | accepted | collapsed | `bound` | `findax` | recovered | lost | runtime |") + lines.append("|---|---|---|---|---|---|---|---|---|") + for name, record in block["cases"].items(): + summary, against = record["summary"], record.get("vs_reference") or {} + lines.append( + f"| {name} | {summary['produced_an_equilibrium']} of {summary['slices']} " + f"| {summary['accepted']} | {summary['collapsed']} | {summary['bound_failures']} " + f"| {summary['findax_failures']} " + f"| {len(against.get('recovered', []))} | {len(against.get('lost', []))} " + f"| {record['seconds']:.0f} s |" + ) + lines.append("") + lines.append("## What this says") + lines.append("") + for line in payload["verdict"]: + lines.append(f"- {line}") + return "\n".join(lines) + "\n" + + +def verdict_lines(payload: dict[str, Any]) -> list[str]: + totals = {name: {"produced": 0, "accepted": 0, "slices": 0} for name in payload["cases"]} + for block in payload["shots"].values(): + for name, record in block["cases"].items(): + summary = record["summary"] + totals[name]["produced"] += summary["produced_an_equilibrium"] + totals[name]["accepted"] += summary["accepted"] + totals[name]["slices"] += summary["slices"] + reference = totals.get("A", {}).get("produced", 0) + lines = [ + "Yield against the routine configuration, over " + f"{totals.get('A', {}).get('slices', 0)} plasma slices: " + + ", ".join( + f"{name} {value['produced']} produced / {value['accepted']} accepted" + for name, value in sorted(totals.items()) + ) + + "." + ] + gains = {name: value["produced"] - reference for name, value in totals.items() if name != "A"} + best = max(gains, key=lambda name: gains[name]) if gains else None + if best and gains[best] > 0: + lines.append( + f"**Case {best} recovers the most**: {gains[best]} slices that the routine box and grid " + "produce nothing for. `bound` and `findax` are grid statements, and this is how much of " + "the loss they account for." + ) + elif gains: + lost = ", ".join(f"{name} {gains[name]:+d}" for name in sorted(gains)) + lines.append( + "**Neither the domain nor the grid recovers a slice.** Against case A: " + f"{lost} equilibria. The slices that produce nothing are not produced by a smaller box " + "or a finer grid either, so `bound` and `findax` are reporting something other than " + "resolution and the cause is elsewhere." + ) + if "C" in totals and totals["C"]["produced"] <= reference: + lines.append( + "**Halving the vertical cell changes the answer by less than a percent** where it " + "changes it at all: the cell aspect ratio goes from 2.6 to 1.3 and every global " + "quantity moves by well under 1 %. The magnetic axis height is the one that responds, " + "and it is quoted in absolute terms because it sits at zero on an up-down symmetric " + "machine." + ) + return lines + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--table", type=Path, default=DEFAULT_TABLE) + parser.add_argument("--markdown", type=Path, default=None) + parser.add_argument( + "--tables", + required=True, + help="comma-separated CASE=directory, e.g. A=/tables/a,C=/tables/c; " + "each is verified against the case's declared grid and box", + ) + parser.add_argument( + "--stage", + required=True, + type=Path, + help="short directory to assemble runnable table directories in; EFIT truncates " + "TABLE_DIR at 100 characters, so a scratch path under a long project directory will not do", + ) + parser.add_argument("--shots", default=None, help="comma-separated; default: the reference set's magnetics shots") + parser.add_argument("--efit-home", default=None) + parser.add_argument("--tstep", type=float, default=0.001) + parser.add_argument("--average-window", type=float, default=0.0005) + args = parser.parse_args(argv) + + if args.efit_home: + os.environ["EFITHOME"] = str(Path(args.efit_home).expanduser()) + + from vaft.code.efit.toolchain import resolve_toolchain, toolchain_identities + from vaft.data.resources import data_path + + resolved = resolve_toolchain() + if resolved.get("efit") is None: + print("no efit executable: set EFITHOME", file=sys.stderr) + return 2 + efit = str(resolved["efit"]) + + directories: dict[str, Path] = {} + for item in args.tables.split(","): + name, _, path = item.partition("=") + name = name.strip().upper() + if name not in CASES: + print(f"unknown case {name}; known: {', '.join(CASES)}", file=sys.stderr) + return 2 + directories[name] = Path(path).expanduser() + if "A" not in directories: + print("case A is the reference every other case is compared against; it must be given", file=sys.stderr) + return 2 + + # One acceptance bar for all four cases, derived from the routine box, so + # that a case is judged on its reconstruction and not on a floor that moved + # underneath it. See `stage_case`. + from vaft.machine_mapping.efund_geometry import vest_acceptance_envelope + from vaft.omas.vest_upstream import build_static_ods + + reference_ods, _ = build_static_ods("vest-pre-43017-pf1906") + rleft, rright, *_ = CASES["A"]["domain"] + envelope = vest_acceptance_envelope( + reference_ods, nw=CASES["A"]["grid"][0], rleft=rleft, rright=rright + ) + + manifests, staged = {}, {} + for name, directory in sorted(directories.items()): + manifest = verify_table(directory, CASES[name]) + staged[name] = stage_case(directory, root=args.stage.expanduser(), name=name, envelope=envelope) + manifests[name] = { + "directory": str(directory), + "staged": str(staged[name]), + "table_identity": manifest["table"]["identity"], + "efund_config_sha256": manifest["efund"]["config_sha256"], + "generated_at": manifest["generated_at"], + # What this case's own resolved floor would have been, for the + # record: it is not applied, because the bar is held fixed. + "own_aminor_min_cm": vest_acceptance_envelope( + reference_ods, + nw=CASES[name]["grid"][0], + rleft=CASES[name]["domain"][0], + rright=CASES[name]["domain"][1], + ).aminor_min, + } + dr, dz = cell_size(CASES[name]) + print(f"{name}: {directory} verified, staged at {staged[name]}, cells {dr:.1f} x {dz:.1f} mm", flush=True) + payload_envelope = {"aminor_min": envelope.aminor_min, "sha256": envelope.sha256} + + reference_set = json.loads(REFERENCE_SET.read_text(encoding="utf-8")) + products = { + int(item["shot"]): item["files"]["product"]["path"] + for item in reference_set["shots"] + if item["files"]["product"] and item["files"]["product"]["exists"] + } + shots = [int(value) for value in args.shots.split(",")] if args.shots else sorted(products) + + output = args.output.expanduser() + output.mkdir(parents=True, exist_ok=True) + payload: dict[str, Any] = { + "schema_version": SCHEMA, + "run_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), + "toolchain": toolchain_identities(resolved), + "acceptance_envelope": payload_envelope, + "cases": { + name: {**CASES[name], "cell_mm": cell_size(CASES[name]), "table": manifests[name]} + for name in sorted(directories) + }, + "shots": {}, + } + + for shot in shots: + if shot not in products: + print(f"{shot}: no packaged pre-EFIT product; skipped", file=sys.stderr) + continue + block: dict[str, Any] = {"cases": {}} + reference_slices: list[dict[str, Any]] | None = None + for name in sorted(directories, key=lambda key: (key != "A", key)): + tables = str(staged[name]) + "/" + print(f"{shot} {name}: constraints ...", flush=True) + ods, times, window, baseline = prepare_shot( + shot, + Path(data_path(products[shot])), + workdir=output / f"shot_{shot}" / name / "constraints", + tables=tables, + tstep=args.tstep, + average_window=args.average_window, + ) + labels = phases(ods, times) + run = run_case( + ods, shot=shot, times=times, + workdir=output / f"shot_{shot}" / name / "run", + efit=efit, grid=CASES[name]["grid"], baseline=baseline, + ) + record = { + "seconds": run["seconds"], + "returncode": run["returncode"], + "summary": summarize(run["slices"], labels), + } + if name == "A": + # Per-slice evidence is kept for the reference only; every other + # case records what changed against it, slice by slice, which is + # the part a reader needs and a quarter of the size. + record["slices"] = run["slices"] + reference_slices = run["slices"] + block["window"] = { + "start": float(window.start), "end": float(window.end), "slices": int(times.size) + } + block["phases"] = dict(sorted(Counter(labels.values()).items())) + elif reference_slices is not None: + record["vs_reference"] = compare(reference_slices, run["slices"]) + block["cases"][name] = record + summary = record["summary"] + print( + f" {name}: {summary['produced_an_equilibrium']}/{summary['slices']} produced, " + f"{summary['accepted']} accepted, {summary['collapsed']} collapsed, " + f"bound {summary['bound_failures']}, findax {summary['findax_failures']}, " + f"{record['seconds']:.0f} s", + flush=True, + ) + payload["shots"][str(shot)] = block + + payload["verdict"] = verdict_lines(payload) + args.table.parent.mkdir(parents=True, exist_ok=True) + args.table.write_text(json.dumps(payload, indent=1, sort_keys=True, default=str) + "\n", encoding="utf-8") + text = markdown(payload) + (args.markdown or output / "domain_grid.md").write_text(text, encoding="utf-8") + print(text) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workflow/efit_tables/regenerate_legacy_table.py b/workflow/efit_tables/regenerate_legacy_table.py index 1e7adaac..06b8bbd8 100644 --- a/workflow/efit_tables/regenerate_legacy_table.py +++ b/workflow/efit_tables/regenerate_legacy_table.py @@ -40,6 +40,14 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--era", default=DEFAULT_ERA, choices=[era.name for era in VEST_MACHINE_ERAS]) parser.add_argument("--nw", type=int, default=129) parser.add_argument("--nh", type=int, default=None, help="defaults to --nw") + # The computational box. EFIT has no independent notion of it: it reads + # `rgrid`/`zgrid` straight out of the Green table (`tables.F90:158`), so + # the domain is whatever EFUND baked in here, and studying an alternate + # one (#459) means generating a table for it. + parser.add_argument("--rleft", type=float, default=None, help="inner edge of the box, m") + parser.add_argument("--rright", type=float, default=None, help="outer edge of the box, m") + parser.add_argument("--zbotto", type=float, default=None, help="bottom of the box, m") + parser.add_argument("--ztop", type=float, default=None, help="top of the box, m") parser.add_argument("--label", default=None, help="a short name recorded in the manifest") parser.add_argument("--efit-home", default=None, help="sets EFITHOME for this run") parser.add_argument("--efund", default=None, help="explicit efund executable (wins over EFITHOME)") @@ -61,6 +69,11 @@ def main(argv: list[str] | None = None) -> int: output = args.output.expanduser() output.mkdir(parents=True, exist_ok=True) + box = { + name: getattr(args, name) + for name in ("rleft", "rright", "zbotto", "ztop") + if getattr(args, name) is not None + } config = EFUNDConfig( workdir=output, nw=args.nw, @@ -69,6 +82,13 @@ def main(argv: list[str] | None = None) -> int: iecoil=args.iecoil, executable=args.efund, timeout=args.timeout, + **box, + ) + print( + f"grid: {config.nw} x {config.nh}, box R {config.rleft}-{config.rright} m, " + f"Z {config.zbotto}-{config.ztop} m, cells " + f"{(config.rright - config.rleft) / (config.nw - 1) * 1e3:.2f} x " + f"{(config.ztop - config.zbotto) / (config.nh - 1) * 1e3:.2f} mm" ) started = time.perf_counter() ods, manifest = build_static_ods(args.era)