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)