From dc35d0bdfdb9a5a64daa0c2d6baf496abf001cd0 Mon Sep 17 00:00:00 2001 From: Evan Bursch Date: Wed, 23 Sep 2026 15:06:28 -0400 Subject: [PATCH 1/2] Regenerate the golden MANTA values for the beta_N fix #34 corrected beta_N to use the pressure (2/3 of the stored energy density) rather than the stored energy density itself, but left the golden file from before the fix, so test_field_matches_golden[betaN] fails on main. Regenerated from main: beta_N moves by exactly 2/3 and every other field is unchanged to 1e-14. Co-Authored-By: Claude Opus 5.5 --- tests/data/golden_manta.json | 1378 +++++++++++++++++----------------- 1 file changed, 689 insertions(+), 689 deletions(-) diff --git a/tests/data/golden_manta.json b/tests/data/golden_manta.json index 024273b..eb899c6 100644 --- a/tests/data/golden_manta.json +++ b/tests/data/golden_manta.json @@ -5,830 +5,830 @@ "NTi": 8, "nr": 60 }, - "openpopcon": "2.0.0", - "numpy": "2.4.6", - "numba": "0.66.0", + "openpopcon": "2.1.0", + "numpy": "2.2.6", + "numba": "0.61.2", "note": "regenerate with `uv run python tests/generate_golden.py`" }, "fields": { "Paux": [ [ 99999.0, - 8.250340310142658, - 12.036902178481995, - 16.99112564660364, - 23.292135569953032, - 31.112807197168763, - 40.62068858150792, - 51.97983321690651 + 8.250340310142683, + 12.036902178482002, + 16.99112564660365, + 23.29213556995305, + 31.112807197168802, + 40.62068858150793, + 51.97983321690655 ], [ 99999.0, - 12.373525265339534, - 18.15399870773918, - 25.895644856981853, - 35.9029722040009, - 48.470994144101326, - 63.88621740614417, - 82.42959085905598 + 12.373525265339518, + 18.15399870773925, + 25.895644856981882, + 35.90297220400089, + 48.47099414410136, + 63.88621740614435, + 82.42959085905615 ], [ 99999.0, - 17.13979944593114, - 25.31614142671623, - 36.44033687684737, - 50.974923707785166, - 69.36870210185828, - 92.05781800153107, - 119.47015296293341 + 17.139799445931203, + 25.31614142671619, + 36.44033687684736, + 50.97492370778521, + 69.3687021018581, + 92.05781800153113, + 119.47015296293348 ], [ 99999.0, - 22.522763142025397, - 33.483018960999004, - 48.56715531093918, - 68.42785479761672, - 93.6988141382342, - 124.99595127010132, - 162.92356118070933 + 22.52276314202544, + 33.483018960998926, + 48.56715531093913, + 68.42785479761653, + 93.69881413823416, + 124.99595127010137, + 162.9235611807093 ], [ 99999.0, - 28.49649020970836, - 42.61656291720154, - 62.22268794185975, - 88.18938305195432, - 121.36591906156724, - 162.5776790473372, - 212.63440527218296 + 28.49649020970837, + 42.61656291720161, + 62.22268794185988, + 88.1893830519543, + 121.36591906156764, + 162.57767904733754, + 212.63440527218305 ], [ 99999.0, - 35.03697353596017, - 52.682144964849904, - 77.35895047956525, - 110.19508001613569, - 152.28569614540288, - 204.69497397365106, - 268.4667644997999 + 35.0369735359601, + 52.6821449648501, + 77.35895047956546, + 110.19508001613552, + 152.285696145403, + 204.69497397365157, + 268.46676449980055 ], [ 99999.0, - 42.12230746265217, - 63.648483058539085, - 93.93290696954898, - 134.38747620503557, - 186.38325499761243, - 251.2522241146071, - 330.3006862136747 + 42.122307462652195, + 63.64848305853912, + 93.9329069695493, + 134.38747620503602, + 186.3832549976124, + 251.25222411460825, + 330.3006862136741 ], [ 99999.0, - 49.73253751600005, - 75.48726197003927, - 111.90578386617278, - 160.7149788036354, - 223.5915541777453, - 302.1640368049505, - 398.0292460579749 + 49.73253751600021, + 75.48726197003907, + 111.90578386617267, + 160.7149788036353, + 223.59155417774446, + 302.1640368049502, + 398.0292460579747 ] ], "Q": [ [ - 0.00039506633216692457, - 5.439069305523813, - 4.763484332575676, - 4.1085613814042, - 3.5274723915790074, - 3.0315746015045555, - 2.615168883686167, - 2.267185481687183 - ], - [ - 0.0007023402977556026, - 6.693845717934161, - 5.748931515280585, - 4.866898894937335, - 4.1108407829220495, - 3.484423079599898, - 2.971265126764852, - 2.55113669652357 - ], - [ - 0.0010974068940524811, - 7.7115274550222885, - 6.5264086726462915, - 5.449785393685631, - 4.549430275981532, - 3.818850929132651, - 3.230548268597098, - 2.75561206146972 - ], - [ - 0.0015802661336279421, - 8.562106184569853, - 7.163288528307027, - 5.918452385841479, - 4.896714457084417, - 4.080505231128474, - 3.4315643278225068, - 2.9130496991378383 + 0.0003950663321669246, + 5.439069305523801, + 4.763484332575681, + 4.108561381404198, + 3.5274723915790087, + 3.0315746015045537, + 2.6151688836861675, + 2.267185481687182 + ], + [ + 0.0007023402977556028, + 6.69384571793417, + 5.748931515280566, + 4.866898894937331, + 4.110840782922049, + 3.4844230795998983, + 2.9712651267648442, + 2.551136696523567 + ], + [ + 0.0010974068940524824, + 7.711527455022267, + 6.526408672646305, + 5.449785393685637, + 4.549430275981531, + 3.818850929132662, + 3.2305482685970963, + 2.755612061469721 + ], + [ + 0.001580266133627942, + 8.562106184569837, + 7.163288528307047, + 5.918452385841485, + 4.896714457084432, + 4.080505231128478, + 3.431564327822506, + 2.913049699137839 ], [ 0.0021509180266336236, - 9.291810145106133, - 7.701339871081812, - 6.308876323865703, - 5.182774360705064, - 4.29418739309426, - 3.594683486831609, - 3.040214479821141 - ], - [ - 0.0028093625815826845, - 9.93146472889283, - 8.16726522742193, - 6.643240556288051, - 5.425624701600335, + 9.29181014510613, + 7.701339871081807, + 6.30887632386569, + 5.182774360705066, + 4.294187393094246, + 3.5946834868316, + 3.0402144798211395 + ], + [ + 0.0028093625815826854, + 9.931464728892854, + 8.167265227421902, + 6.643240556288036, + 5.4256247016003405, 4.474411275921838, - 3.731611514679319, - 3.1466027927569367 - ], - [ - 0.0035555998058043716, - 10.50216108842501, - 8.57879024688433, - 6.935893306770727, - 5.636677228628491, - 4.630222324825901, - 3.849551951000918, - 3.238001233086139 - ], - [ - 0.004389629705732339, - 11.018745601987401, - 8.94811168284835, - 7.196516940258292, - 5.823514325642593, - 4.767559057808529, - 3.9531908170482035, - 3.318147686990535 + 3.7316115146793107, + 3.1466027927569318 + ], + [ + 0.003555599805804373, + 10.502161088424998, + 8.578790246884337, + 6.935893306770707, + 5.636677228628476, + 4.630222324825903, + 3.8495519510009015, + 3.2380012330861443 + ], + [ + 0.004389629705732341, + 11.018745601987371, + 8.948111682848374, + 7.196516940258304, + 5.8235143256426, + 4.767559057808549, + 3.953190817048208, + 3.318147686990537 ] ], "Pheat": [ [ 100002.13273934061, - 12.324670356818935, - 17.074162534083715, - 22.95592504702467, - 30.104565557074487, - 38.65737331922167, + 12.32467035681896, + 17.074162534083726, + 22.95592504702469, + 30.104565557074523, + 38.65737331922171, 48.754152616116656, - 60.53707889347153 + 60.53707889347159 ], [ 100005.59146655504, - 21.30894458247948, - 29.52065837848044, - 39.69002987540002, + 21.30894458247947, + 29.52065837848052, + 39.69002987540003, 52.049791236842, - 66.83731101235945, - 84.29430615057197, - 104.66659326206288 + 66.83731101235948, + 84.29430615057214, + 104.66659326206309 ], [ 100010.2438911523, - 32.58393851652954, + 32.58393851652962, 45.140636311137115, - 60.69082812494577, - 79.59038942050294, - 102.20228525196525, - 128.89612989852577, - 160.04780651547566 + 60.69082812494582, + 79.59038942050306, + 102.2022852519651, + 128.89612989852588, + 160.04780651547574 ], [ 100016.07143308986, - 46.100259330853184, - 63.86566925442358, - 85.86632073789688, - 112.60571188063902, - 144.59737124569202, + 46.10025933085321, + 63.865669254423516, + 85.86632073789686, + 112.60571188063888, + 144.59737124569205, 182.36423482234875, - 226.43810790260113 + 226.4381079026011 ], [ 100023.0632165984, - 61.818466302549204, - 85.64111742538572, - 115.1430454428491, - 150.9994196650653, - 193.8988598228253, - 244.5426006011775, - 303.6437700388713 + 61.81846630254923, + 85.64111742538579, + 115.14304544284919, + 150.99941966506532, + 193.8988598228257, + 244.5426006011779, + 303.6437700388714 ], [ 100031.21222977848, - 79.70581304639705, - 110.42161513979258, - 148.46000883852508, - 194.69152558774877, - 250.00403917025608, - 315.3016884955202, - 391.50394720199404 + 79.705813046397, + 110.4216151397928, + 148.4600088385253, + 194.6915255877486, + 250.00403917025625, + 315.30168849552064, + 391.5039472019949 ], [ 100040.51364095078, - 99.73435813049673, - 138.168453325849, - 185.76516722737907, - 243.61377916863225, + 99.73435813049679, + 138.16845332584907, + 185.76516722737938, + 243.61377916863282, 312.8252686182739, - 394.5309672866323, - 489.88139493687515 + 394.53096728663354, + 489.8813949368747 ], [ 100050.96395440031, - 121.8797722083898, - 168.8479269672136, - 227.01320478046256, - 297.70675290302836, - 382.2862371093469, - 482.13419450831213, - 598.6566108533225 + 121.87977220838991, + 168.8479269672135, + 227.01320478046262, + 297.70675290302853, + 382.28623710934625, + 482.13419450831196, + 598.6566108533223 ] ], "Pconf": [ [ - 5486.814034835568, - 12.324670356818933, - 17.074162534083715, - 22.955925047024667, - 30.104565557074483, - 38.65737331922167, + 5486.814034835573, + 12.32467035681896, + 17.074162534083726, + 22.955925047024685, + 30.104565557074523, + 38.65737331922171, 48.754152616116656, - 60.53707889347154 + 60.53707889347159 ], [ - 6501.962488165836, - 21.308944582479487, - 29.520658378480448, - 39.69002987540002, - 52.049791236842005, - 66.83731101235945, - 84.29430615057198, - 104.66659326206289 + 6501.962488165838, + 21.308944582479477, + 29.52065837848052, + 39.690029875400036, + 52.049791236842, + 66.83731101235949, + 84.29430615057214, + 104.66659326206309 ], [ - 7417.120039909661, - 32.58393851652955, + 7417.120039909663, + 32.58393851652962, 45.14063631113712, - 60.69082812494577, - 79.59038942050294, - 102.20228525196526, - 128.89612989852577, - 160.04780651547568 - ], - [ - 8259.80327208364, - 46.100259330853184, - 63.86566925442357, - 85.86632073789688, - 112.60571188063899, + 60.69082812494581, + 79.59038942050304, + 102.20228525196511, + 128.89612989852588, + 160.04780651547577 + ], + [ + 8259.803272083644, + 46.100259330853206, + 63.865669254423516, + 85.86632073789684, + 112.60571188063889, 144.59737124569202, - 182.36423482234878, - 226.4381079026011 + 182.36423482234875, + 226.43810790260108 ], [ - 9046.680594363157, - 61.8184663025492, - 85.64111742538572, - 115.1430454428491, - 150.99941966506532, - 193.8988598228253, - 244.5426006011775, - 303.6437700388713 + 9046.680594363159, + 61.81846630254923, + 85.64111742538579, + 115.14304544284917, + 150.9994196650653, + 193.8988598228257, + 244.54260060117795, + 303.6437700388715 ], [ - 9788.787909728917, - 79.70581304639707, - 110.42161513979259, - 148.46000883852514, - 194.69152558774877, - 250.0040391702561, - 315.3016884955202, - 391.5039472019941 + 9788.787909728922, + 79.70581304639701, + 110.42161513979283, + 148.46000883852534, + 194.69152558774863, + 250.00403917025625, + 315.30168849552064, + 391.50394720199495 ], [ 10493.896344582878, - 99.73435813049673, - 138.16845332584901, - 185.76516722737907, - 243.61377916863228, + 99.73435813049682, + 138.1684533258491, + 185.7651672273794, + 243.61377916863282, 312.825268618274, - 394.53096728663235, - 489.8813949368752 - ], - [ - 11167.732195040138, - 121.8797722083898, - 168.8479269672136, - 227.01320478046256, - 297.70675290302836, - 382.286237109347, - 482.13419450831213, - 598.6566108533225 + 394.5309672866336, + 489.8813949368747 + ], + [ + 11167.732195040142, + 121.87977220838992, + 168.8479269672135, + 227.01320478046264, + 297.7067529030286, + 382.28623710934625, + 482.1341945083121, + 598.6566108533223 ] ], "Psol": [ [ - 5476.275971200018, - 2.2432956032068105, + 5476.2759712000225, + 2.2432956032068283, 7.300568888516448, - 13.36878776954433, - 20.618816112547417, - 29.22157799702677, - 39.33624321025367, - 51.11653057449365 - ], - [ - 6483.2281528137455, - 3.386500576057923, - 12.14538078636085, - 22.64623027099052, - 35.186236668793875, - 50.062563772901854, - 67.55135609570445, - 87.9189518061022 - ], - [ - 7387.847640922019, - 4.580119756495868, - 17.991765073450267, - 34.05989124305593, - 53.241085407927756, - 75.99174269031276, - 102.73527043779524, - 133.87961674053713 - ], - [ - 8217.651017541435, - 5.774760316404674, - 24.771294672154482, - 47.517771627975506, - 74.6627141025307, - 106.85418995691242, - 144.6925971988968, - 188.75591462668956 - ], - [ - 8989.306692347378, + 13.36878776954434, + 20.618816112547442, + 29.221577997026806, + 39.33624321025366, + 51.1165305744937 + ], + [ + 6483.228152813748, + 3.386500576057905, + 12.14538078636091, + 22.646230270990536, + 35.18623666879385, + 50.06256377290189, + 67.55135609570459, + 87.9189518061024 + ], + [ + 7387.847640922022, + 4.580119756495925, + 17.99176507345024, + 34.05989124305595, + 53.24108540792784, + 75.9917426903126, + 102.73527043779532, + 133.8796167405372 + ], + [ + 8217.651017541439, + 5.774760316404681, + 24.77129467215439, + 47.51777162797545, + 74.66271410253059, + 106.85418995691239, + 144.69259719889678, + 188.7559146266895 + ], + [ + 8989.30669234738, 6.930981532883209, - 32.42932979951945, - 62.946409154345005, - 99.35478380041792, - 142.5261964019864, - 193.26731605814564, - 252.35411807999174 + 32.42932979951951, + 62.94640915434509, + 99.35478380041788, + 142.5261964019868, + 193.2673160581461, + 252.3541180799919 ], [ - 9713.850568320553, - 8.016037020710826, - 40.92050477131423, - 80.28481042088715, - 127.23730731555627, - 182.9050502124257, - 248.32988827605004, - 324.5133813781514 + 9713.850568320559, + 8.016037020710726, + 40.9205047713144, + 80.28481042088731, + 127.23730731555607, + 182.90505021242583, + 248.32988827605047, + 324.5133813781522 ], [ 10399.053771862918, - 9.001985347987599, - 50.206110515743575, - 99.48093173005603, - 158.24203416788865, - 227.9031107185199, - 309.7697826338654, - 405.0964600660743 - ], - [ - 11050.64259908957, - 9.864497168255042, - 60.25244201646615, - 120.48945725290324, - 192.30953685272763, - 277.444066862737, - 377.4907566653901, - 493.9838517535683 + 9.00198534798767, + 50.20611051574362, + 99.48093173005628, + 158.24203416788916, + 227.90311071851983, + 309.7697826338666, + 405.09646006607375 + ], + [ + 11050.642599089573, + 9.864497168255127, + 60.25244201646598, + 120.48945725290321, + 192.30953685272775, + 277.44406686273624, + 377.4907566653899, + 493.98385175356805 ] ], "f_rad": [ [ - 0.002999816724208166, - 0.8838583580111653, - 0.7106342195722215, + 0.0029998167242081654, + 0.8838583580111645, + 0.7106342195722216, 0.587322114779269, - 0.49650297720968756, - 0.4274704107174021, - 0.37366115072986783, - 0.3308404255372669 + 0.49650297720968745, + 0.42747041071740194, + 0.37366115072986794, + 0.3308404255372667 ], [ - 0.004202690829827829, - 0.891670714728414, - 0.7032108598820128, + 0.004202690829827828, + 0.8916707147284145, + 0.7032108598820119, 0.5700693916020937, - 0.47289941162590726, - 0.3997758397570372, - 0.34337786434124506, - 0.29898430178697616 - ], - [ - 0.005540798976629866, - 0.9006382117166095, - 0.702150502785747, - 0.5624994914560186, - 0.46109526834102715, - 0.385219205862216, + 0.4728994116259075, + 0.39977583975703707, + 0.3433778643412446, + 0.29898430178697577 + ], + [ + 0.005540798976629864, + 0.9006382117166084, + 0.7021505027857474, + 0.5624994914560187, + 0.46109526834102693, + 0.38521920586221653, 0.32705493088312887, - 0.2815623138439706 + 0.28156231384397057 ], [ - 0.006996001977548645, - 0.909471744744634, - 0.7038220353371295, - 0.5594761617884025, - 0.4549839548641911, - 0.3770718614086247, - 0.31757494988442847, - 0.2712277614104787 + 0.006996001977548642, + 0.9094717447446339, + 0.7038220353371304, + 0.5594761617884029, + 0.4549839548641916, + 0.3770718614086248, + 0.3175749498844285, + 0.2712277614104788 ], [ - 0.008555434580248808, + 0.008555434580248806, 0.9177984650897333, - 0.7067251589443498, - 0.5587856253288414, - 0.45190101165074786, - 0.3723879388333371, - 0.311822435693209, - 0.26477068710961554 - ], - [ - 0.010209353630576356, - 0.9255381034053445, - 0.7101754993413167, - 0.5593635664573834, - 0.4505441443579928, - 0.3697181356587112, - 0.30826073165471796, - 0.26060684316695637 - ], - [ - 0.01195004394053916, - 0.9327080912932818, - 0.7138390189473033, - 0.5606551949410172, - 0.45022058441410207, - 0.3682846404252438, - 0.30606158496615704, - 0.25788050827838116 - ], - [ - 0.013771194760452975, - 0.9393564539307392, - 0.7175456165728821, - 0.5623532235867361, - 0.4505371757223881, - 0.36764124028583295, - 0.30474715390180723, - 0.2560964193558574 + 0.7067251589443495, + 0.558785625328841, + 0.45190101165074803, + 0.3723879388333365, + 0.31182243569320844, + 0.2647706871096154 + ], + [ + 0.010209353630576349, + 0.9255381034053454, + 0.710175499341316, + 0.559363566457383, + 0.45054414435799334, + 0.36971813565871103, + 0.3082607316547176, + 0.2606068431669559 + ], + [ + 0.011950043940539168, + 0.9327080912932812, + 0.7138390189473032, + 0.5606551949410168, + 0.4502205844141014, + 0.368284640425244, + 0.3060615849661563, + 0.25788050827838144 + ], + [ + 0.013771194760452974, + 0.9393564539307387, + 0.7175456165728827, + 0.5623532235867365, + 0.45053717572238816, + 0.3676412402858336, + 0.3047471539018075, + 0.25609641935585764 ] ], "tauE": [ [ 0.006508328403622307, - 3.24220808489872, - 2.5891764967461754, - 2.1108551829157753, - 1.750731389846737, - 1.4732808176187524, + 3.2422080848987154, + 2.589176496746174, + 2.110855182915774, + 1.7507313898467352, + 1.4732808176187513, 1.2553008914370838, - 1.0811377746156352 + 1.0811377746156345 ], [ 0.007322914723733394, - 2.50030502261133, - 1.9967043538613312, - 1.6278356224055697, + 2.500305022611331, + 1.9967043538613276, + 1.6278356224055694, 1.3501176891346418, - 1.1361551546202402, - 0.9680548075761988, - 0.8337448236579653 + 1.1361551546202397, + 0.9680548075761974, + 0.8337448236579642 ], [ 0.008024225808257872, - 2.043908118224107, + 2.043908118224104, 1.6322329482377669, - 1.3306962205332236, - 1.103671944192718, - 0.9287653798595973, - 0.7913494802423707, - 0.6815559694481083 + 1.3306962205332231, + 1.1036719441927172, + 0.9287653798595983, + 0.7913494802423701, + 0.6815559694481079 ], [ 0.00864669204628782, - 1.7335757522668334, - 1.384406390818857, - 1.1286528396168227, - 0.9360983029766032, + 1.733575752266833, + 1.384406390818858, + 1.1286528396168232, + 0.9360983029766039, 0.7877482983278379, 0.6711966444504903, - 0.5780733937661308 + 0.5780733937661309 ], [ 0.00921037319677044, - 1.5082549376813812, - 1.2044687242422911, - 0.9819566384995023, + 1.5082549376813805, + 1.2044687242422905, + 0.9819566384995019, 0.8144293006945028, - 0.6853610285270021, - 0.5839581292158076, - 0.5029385357691601 + 0.6853610285270011, + 0.5839581292158068, + 0.5029385357691599 ], [ 0.009728133304518674, - 1.336887715707915, - 1.0676175500338838, - 0.8703871836056575, - 0.7218942237211347, - 0.6074906283888785, - 0.517609079163054, - 0.44579489410427137 + 1.3368877157079158, + 1.0676175500338823, + 0.8703871836056565, + 0.721894223721135, + 0.6074906283888782, + 0.5176090791630535, + 0.4457948941042707 ], [ 0.010208787981810385, - 1.2019673046566068, - 0.9598722270694282, - 0.7825466004317133, - 0.6490397391929075, + 1.2019673046566062, + 0.9598722270694279, + 0.7825466004317123, + 0.6490397391929064, 0.5461819004164291, - 0.46537131161981027, - 0.40080470558625236 + 0.46537131161980927, + 0.40080470558625264 ], [ - 0.010658680743369283, - 1.0928569236867263, - 0.8727383890921339, + 0.010658680743369285, + 1.0928569236867258, + 0.8727383890921343, 0.7115097616017387, - 0.5901221855010765, - 0.49660142097874954, - 0.42312653432297853, - 0.36442105858386475 + 0.5901221855010763, + 0.49660142097875026, + 0.42312653432297864, + 0.3644210585838649 ] ], "betaN": [ [ - 0.5944884683038691, - 0.6652270976112213, - 0.7359610025400138, - 0.8066905552486976, - 0.8774161375699068, - 0.9481381262884191, - 1.0188568853114066, - 1.0895727617354494 + 0.3963256455359129, + 0.44348473174081454, + 0.4906406683600095, + 0.5377937034991317, + 0.5849440917132712, + 0.6320920841922796, + 0.6792379235409376, + 0.7263818411569666 + ], + [ + 0.528434194047884, + 0.5913129756544198, + 0.6541875578133458, + 0.7170582713321754, + 0.7799254556176953, + 0.8427894455897061, + 0.9056505647212502, + 0.9685091215426217 + ], + [ + 0.6605427425598549, + 0.739141219568024, + 0.8177344472666823, + 0.8963228391652197, + 0.974906819522119, + 1.0534868069871328, + 1.1320632059015627, + 1.2106364019282767 ], [ 0.7926512910718257, - 0.8869694634816294, - 0.9812813367200185, + 0.8869694634816291, + 0.981281336720019, 1.0755874069982634, - 1.169888183426543, + 1.1698881834265424, 1.2641841683845592, 1.3584758470818752, - 1.4527636823139325 + 1.4527636823139332 + ], + [ + 0.9247598395837966, + 1.0347977073952335, + 1.1448282261733551, + 1.254851974831307, + 1.3648695473309662, + 1.474881529781985, + 1.5848884882621879, + 1.694890962699587 ], [ - 0.990814113839782, - 1.1087118293520362, - 1.2266016709000234, - 1.3444842587478292, - 1.4623602292831777, - 1.5802302104806991, - 1.6980948088523444, - 1.815954602892416 + 1.056868388095768, + 1.1826259513088395, + 1.3083751156266916, + 1.4341165426643507, + 1.5598509112353907, + 1.6855788911794123, + 1.8113011294425003, + 1.9370182430852434 ], [ - 1.188976936607739, + 1.1889769366077392, 1.3304541952224438, - 1.4719220050800281, + 1.4719220050800277, 1.6133811104973952, - 1.7548322751398144, + 1.7548322751398135, 1.8962762525768388, - 2.0377137706228132, - 2.179145523470899 - ], - [ - 1.3871397593756958, - 1.5521965610928505, - 1.7172423392600327, - 1.8822779622469608, - 2.0473043209964503, - 2.212322294672977, - 2.377332732393282, - 2.542336444049381 - ], - [ - 1.5853025821436515, - 1.7739389269632588, - 1.962562673440037, - 2.1511748139965268, - 2.339776366853086, - 2.5283683367691183, - 2.7169516941637504, - 2.905527364627865 - ], - [ - 1.7834654049116085, - 1.9956812928336658, - 2.207883007620042, - 2.420071665746091, - 2.6322484127097194, - 2.8444143788652583, - 3.0565706559342187, - 3.2687182852063477 - ], - [ - 1.981628227679564, - 2.2174236587040723, - 2.4532033418000467, - 2.6889685174956584, - 2.9247204585663553, - 3.1604604209613982, - 3.396189617704689, - 3.631909205784832 + 2.0377137706228123, + 2.1791455234708996 + ], + [ + 1.3210854851197098, + 1.478282439136048, + 1.6354688945333646, + 1.7926456783304394, + 1.949813639044238, + 2.1069736139742656, + 2.2641264118031255, + 2.4212728038565534 ] ], "Pfusion": [ [ - 39.50668877044258, - 50.21932534566566, - 61.430983914657915, - 72.93433643950269, - 84.5619847633087, - 96.18127631603791, - 107.68874446533471, - 119.00493525632237 - ], - [ - 70.2341133696757, - 89.27880061451674, - 109.21063807050307, - 129.6610425591159, - 150.33241735699346, - 170.9889356729563, - 191.44665682726176, - 211.56432934457305 - ], - [ - 109.7408021401183, - 139.49812596018236, - 170.64162198516104, - 202.59537899861851, - 234.89440212030206, - 267.17021198899425, + 39.50668877044259, + 50.21932534566568, + 61.43098391465801, + 72.9343364395027, + 84.5619847633088, + 96.18127631603797, + 107.68874446533475, + 119.00493525632241 + ], + [ + 70.23411336967573, + 89.27880061451677, + 109.2106380705031, + 129.66104255911597, + 150.33241735699337, + 170.98893567295644, + 191.44665682726182, + 211.56432934457322 + ], + [ + 109.74080214011843, + 139.49812596018248, + 170.64162198516112, + 202.5953789986187, + 234.89440212030223, + 267.17021198899437, 299.1354012925965, - 330.5692646008955 + 330.5692646008958 ], [ - 158.02675508177037, - 200.87730138266275, - 245.72393565863192, + 158.02675508177035, + 200.87730138266272, + 245.72393565863203, 291.7373457580108, - 338.2479390532351, - 384.72510526415175, - 430.75497786133894, - 476.0197410252896 + 338.2479390532352, + 384.72510526415186, + 430.754977861339, + 476.01974102528965 ], [ 215.0919721946319, 273.41632688195745, - 334.45757909091543, + 334.4575790909158, 397.08694283729255, - 460.39302815579236, + 460.3930281557924, 523.6536154984286, - 586.3053865334892, + 586.305386533489, 647.9157586177553 ], [ - 280.9364534787028, - 357.11520245806696, - 436.84255228201226, - 518.6441702364637, - 601.3296694279738, - 683.9557426918252, - 765.786627309047, - 846.2573173782922 + 280.9364534787029, + 357.1152024580671, + 436.8425522820124, + 518.6441702364639, + 601.3296694279735, + 683.9557426918258, + 765.7866273090473, + 846.2573173782929 ], [ - 355.5601989339832, - 451.9739281109912, - 552.8788552319213, - 656.4090279555244, - 761.0578628697784, - 865.6314868443411, - 969.1987001880123, + 355.5601989339833, + 451.97392811099104, + 552.8788552319221, + 656.4090279555246, + 761.0578628697789, + 865.6314868443413, + 969.1987001880126, 1071.0444173069018 ], [ - 438.96320856047345, - 557.9925038407297, - 682.5664879406444, - 810.3815159944743, - 939.5776084812085, - 1068.680847955977, + 438.96320856047373, + 557.9925038407299, + 682.5664879406445, + 810.3815159944749, + 939.5776084812089, + 1068.6808479559775, 1196.541605170386, - 1322.277058403583 + 1322.2770584035832 ] ], "Pohmic": [ [ 1.1406187913241388, - 0.9827329464646599, - 0.8593266375874834, - 0.7606686358455705, - 0.6802660185555319, + 0.9827329464646596, + 0.8593266375874833, + 0.7606686358455703, + 0.6802660185555318, 0.6137009574725956, - 0.5578162321253268, - 0.5103296822321376 + 0.557816232125327, + 0.5103296822321374 ], [ - 1.119022239951111, - 0.9639199607015823, + 1.1190222399511107, + 0.963919960701582, 0.8426857683626133, - 0.7457638425069418, + 0.7457638425069416, 0.6667772208685406, - 0.6013864959070209, + 0.6013864959070208, 0.5464900970435782, - 0.49984590255363126 + 0.49984590255363115 ], [ 1.1027284144050988, - 0.9497589163789628, - 0.8301865990416535, - 0.7345910080845293, - 0.6566846191770491, - 0.5921884767312182, + 0.9497589163789619, + 0.8301865990416529, + 0.734591008084529, + 0.6566846191770488, + 0.5921884767312181, 0.538043883702226, - 0.49203954706185843 + 0.4920395470618582 ], [ 1.0896804488395895, - 0.9384386992543278, - 0.8202112785214967, - 0.7256879424634423, - 0.6486539548730802, - 0.5848795400006047, - 0.5313408656908135, - 0.48585169891812985 - ], - [ - 1.0788180988091114, - 0.9290278123061142, - 0.8119293568232385, - 0.7183054225467596, + 0.9384386992543277, + 0.8202112785214966, + 0.725687942463442, + 0.64865395487308, + 0.5848795400006044, + 0.5313408656908136, + 0.4858516989181298 + ], + [ + 1.0788180988091112, + 0.9290278123061139, + 0.8119293568232384, + 0.7183054225467592, 0.642002633368191, - 0.5788326603877345, + 0.5788326603877344, 0.5258010452216132, - 0.4807426616802027 + 0.48074266168020263 ], [ - 1.0695248223398302, - 0.920985557338499, + 1.06952482233983, + 0.9209855573384991, 0.8048595731312218, - 0.712009896173286, - 0.6363361801608188, - 0.5736859083654351, + 0.7120098961732858, + 0.6363361801608187, + 0.573685908365435, 0.5210899967314281, - 0.47640154780690314 + 0.476401547806903 ], [ - 1.0614111705394416, - 0.9139709095331248, - 0.798698807542495, - 0.7065286334679571, - 0.631406729114277, - 0.5692121016622088, + 1.0614111705394411, + 0.9139709095331247, + 0.7986988075424949, + 0.7065286334679567, + 0.6314067291142771, + 0.5692121016622087, 0.5169979986343829, - 0.4726335623926815 + 0.4726335623926814 ], [ - 1.0542157893761246, - 0.9077553082654609, - 0.7932441448967971, - 0.7016792610367477, - 0.6270486975215053, + 1.054215789376124, + 0.9077553082654601, + 0.7932441448967968, + 0.7016792610367474, + 0.627048697521505, 0.5652596126663817, - 0.5133851930668099, - 0.4693089002133981 + 0.5133851930668096, + 0.46930890021339794 ] ], "n_i_20_avg": [ @@ -839,63 +839,63 @@ 0.6070494769504278, 0.6069962592186215, 0.6069461684993146, - 0.6068990071511782, - 0.6068546092655243 + 0.6068990071511783, + 0.6068546092655241 ], [ 0.8096413100942697, 0.8095551953075399, 0.809474764154598, - 0.8093993026005705, - 0.8093283456248289, + 0.8093993026005704, + 0.8093283456248288, 0.8092615579990864, 0.809198676201571, 0.8091394790206989 ], [ - 1.0120516376178368, + 1.012051637617837, 1.0119439941344248, - 1.0118434551932474, - 1.011749128250713, - 1.011660432031036, - 1.0115769474988576, + 1.0118434551932476, + 1.0117491282507132, + 1.0116604320310363, + 1.0115769474988578, 1.0114983452519637, 1.0114243487758738 ], [ - 1.2144619651414046, - 1.21433279296131, - 1.214212146231897, - 1.2140989539008558, - 1.2139925184372433, - 1.2138923369986294, - 1.2137980143023568, - 1.2137092185310485 + 1.2144619651414044, + 1.2143327929613097, + 1.2142121462318969, + 1.2140989539008555, + 1.213992518437243, + 1.2138923369986292, + 1.2137980143023566, + 1.2137092185310483 ], [ - 1.4168722926649717, - 1.4167215917881948, - 1.4165808372705464, - 1.4164487795509983, - 1.4163246048434506, - 1.416207726498401, - 1.4160976833527494, - 1.4159940882862234 + 1.4168722926649715, + 1.4167215917881946, + 1.4165808372705462, + 1.4164487795509981, + 1.4163246048434504, + 1.4162077264984008, + 1.4160976833527492, + 1.4159940882862232 ], [ 1.6192826201885393, 1.6191103906150799, 1.618949528309196, - 1.618798605201141, - 1.6186566912496578, + 1.6187986052011407, + 1.6186566912496576, 1.6185231159981728, 1.618397352403142, 1.6182789580413979 ], [ - 1.8216929477121064, - 1.8214991894419648, - 1.8213182193478452, + 1.8216929477121062, + 1.8214991894419643, + 1.821318219347845, 1.8211484308512835, 1.8209887776558649, 1.820838505497944, @@ -909,90 +909,90 @@ 2.0234982565014263, 2.0233208640620726, 2.0231538949977157, - 2.022996690503928, + 2.0229966905039274, 2.0228486975517477 ] ], "vloop": [ [ 0.11406187913241388, - 0.09827329464646599, + 0.09827329464646596, 0.08593266375874833, - 0.07606686358455705, - 0.06802660185555319, + 0.07606686358455703, + 0.06802660185555318, 0.06137009574725956, - 0.05578162321253268, - 0.05103296822321376 + 0.0557816232125327, + 0.05103296822321374 ], [ - 0.1119022239951111, - 0.09639199607015822, + 0.11190222399511107, + 0.0963919960701582, 0.08426857683626134, - 0.07457638425069418, + 0.07457638425069416, 0.06667772208685406, - 0.06013864959070209, + 0.06013864959070207, 0.05464900970435782, - 0.04998459025536313 + 0.049984590255363115 ], [ 0.11027284144050989, - 0.09497589163789628, - 0.08301865990416535, - 0.07345910080845293, - 0.06566846191770491, - 0.05921884767312182, + 0.09497589163789619, + 0.08301865990416529, + 0.0734591008084529, + 0.06566846191770488, + 0.05921884767312181, 0.0538043883702226, - 0.04920395470618584 + 0.04920395470618582 ], [ 0.10896804488395895, - 0.09384386992543278, - 0.08202112785214967, - 0.07256879424634423, - 0.06486539548730802, - 0.058487954000060474, - 0.05313408656908135, + 0.09384386992543277, + 0.08202112785214966, + 0.0725687942463442, + 0.06486539548730799, + 0.05848795400006044, + 0.05313408656908136, 0.04858516989181298 ], [ - 0.10788180988091114, - 0.09290278123061142, - 0.08119293568232386, - 0.07183054225467596, + 0.10788180988091112, + 0.09290278123061138, + 0.08119293568232384, + 0.07183054225467592, 0.0642002633368191, - 0.05788326603877345, + 0.05788326603877344, 0.05258010452216132, - 0.04807426616802027 + 0.04807426616802026 ], [ - 0.10695248223398302, - 0.0920985557338499, + 0.10695248223398299, + 0.09209855573384991, 0.08048595731312218, - 0.0712009896173286, - 0.06363361801608189, - 0.05736859083654351, + 0.07120098961732858, + 0.06363361801608187, + 0.0573685908365435, 0.05210899967314281, - 0.047640154780690315 + 0.0476401547806903 ], [ - 0.10614111705394416, + 0.1061411170539441, 0.09139709095331247, - 0.0798698807542495, - 0.07065286334679571, - 0.06314067291142769, - 0.056921210166220884, + 0.07986988075424949, + 0.07065286334679567, + 0.0631406729114277, + 0.05692121016622087, 0.05169979986343829, - 0.04726335623926815 + 0.04726335623926814 ], [ - 0.10542157893761246, - 0.09077553082654609, - 0.07932441448967971, - 0.07016792610367477, - 0.06270486975215053, + 0.10542157893761239, + 0.09077553082654602, + 0.07932441448967968, + 0.07016792610367474, + 0.0627048697521505, 0.05652596126663817, - 0.051338519306680985, - 0.04693089002133981 + 0.051338519306680964, + 0.046930890021339794 ] ] } From 1dcb4f6f67f355b88f96ee23789c41663ce77384 Mon Sep 17 00:00:00 2001 From: Evan Bursch Date: Wed, 23 Sep 2026 15:06:28 -0400 Subject: [PATCH 2/2] Sensitivity scans on every example: R_0, scale/hold axes, gfile_rescale Scanning - R_0 is accepted as an alias for R (matching B_0) and is scannable; giving both is an error, and a scan of either drops the other. - A scan can have a single axis, so one parameter can be varied on its own. - An axis can give 'scale' (values relative to the settings file) instead of 'values', and 'hold' to keep aspect_ratio, a, I_P, qstar or B_0 fixed while it varies. Conflicting holds, and holds that overlap the other axis, are refused. - Scanning Zeff_target without 'impurity' is refused, since every cell would silently come out the same. - POPCON_scan.operating_point() tabulates the same (T_i, n/n_G or n_e) point across every cell. gEQDSK - gfile_rescale: True uses the equilibrium for its shape and profiles and maps every flux surface onto the settings R and a, with the settings current. Volumes and areas are exact for the mapped surfaces; q and the trapped fraction are rescaled to leading order in a/R. This makes R, a, I_P and qstar scannable with a gEQDSK. Off by default; the golden test pins the plain path. Examples - SPARC, MANTA and radiative_ARC now follow the ITER/CENTAUR layout: a documented settings file with a scan: block, a plotsettings file that lists only what differs from the defaults (loads identically), and a _ex.ipynb notebook. Settings values are unchanged apart from MANTA's verbosity (2 -> 1) and gfile_rescale: True. - The NSF examples are untouched. Tests - test_sensitivity.py runs +-25% on B_0, H_fac, I_P and Zeff_target, and R at fixed aspect ratio and current, fixed aspect ratio and q*, and fixed minor radius and q*, on every example other than the NSF pair. Co-Authored-By: Claude Opus 5.5 --- README.MD | 53 +- openpopcon/core.py | 533 +++++++++++++++--- .../resources/examples/MANTA/MANTA_ex.ipynb | 315 +++++++++++ .../examples/MANTA/POPCON_input_example.yaml | 143 ++--- .../resources/examples/MANTA/manta_ex.ipynb | 183 ------ .../resources/examples/MANTA/plotsettings.yml | 373 ++---------- .../examples/SPARC/POPCON_input_example.yaml | 209 +++---- .../resources/examples/SPARC/SPARC_EX.ipynb | 195 ------- .../resources/examples/SPARC/SPARC_ex.ipynb | 244 ++++++++ .../resources/examples/SPARC/plotsettings.yml | 361 ++---------- .../radiative_ARC/POPCON_input_example.yaml | 115 ++-- .../examples/radiative_ARC/plotsettings.yml | 312 +--------- .../radiative_ARC/radiative_ARC_ex.ipynb | 226 ++++++++ tests/helpers.py | 7 +- tests/test_scan.py | 153 ++++- tests/test_sensitivity.py | 146 +++++ 16 files changed, 1914 insertions(+), 1654 deletions(-) create mode 100644 openpopcon/resources/examples/MANTA/MANTA_ex.ipynb delete mode 100644 openpopcon/resources/examples/MANTA/manta_ex.ipynb delete mode 100644 openpopcon/resources/examples/SPARC/SPARC_EX.ipynb create mode 100644 openpopcon/resources/examples/SPARC/SPARC_ex.ipynb create mode 100644 openpopcon/resources/examples/radiative_ARC/radiative_ARC_ex.ipynb create mode 100644 tests/test_sensitivity.py diff --git a/README.MD b/README.MD index e760a04..8cf5a84 100644 --- a/README.MD +++ b/README.MD @@ -37,17 +37,17 @@ openpopcon examples ./my_scans cd my_scans/MANTA ``` -Each example directory has its own settings file, a notebook, and where applicable a gEQDSK and a profiles file. +Apart from the two NSF examples, which keep their original files, each example directory has the same layout: a `POPCON_input_example.yaml` settings file with a `scan:` block, a `plotsettings.yml` that lists only what differs from the defaults, a `_ex.ipynb` notebook that runs a POPCON and then the scan, and where applicable a gEQDSK and a profiles file. | Example | Machine | Notes | | --- | --- | --- | -| `MANTA` | 4.55 m, 11 T, 10 MA, negative triangularity | gEQDSK + profiles file | -| `SPARC` | 1.85 m, 12.2 T, 8.7 MA | parabolic profiles | -| `ITER` | 6.2 m, 5.3 T, 15 MA | 500 MW at Q = 10, parabolic profiles | -| `CENTAUR` | 2.0 m, 10.9 T, 9.6 MA, negative triangularity | 40 MW breakeven, `H_NT23` scaling | -| `radiative_ARC` | 4.2 m, 11.5 T, 14 MA | radiative L-mode, `H89` | -| `NSF` | 0.8 m, 3 T, 1 MA, negative triangularity | gEQDSK + profiles file | -| `NSF_NT_CMOD` | 0.8 m, 3 T, 1 MA | parabolic profiles | +| `MANTA` | 4.55 m, 11 T, 10 MA, negative triangularity | gEQDSK + profiles file; scans `B_0` x `H_fac` | +| `SPARC` | 1.85 m, 12.2 T, 8.7 MA | parabolic profiles; scans `R_0` x `B_0` | +| `ITER` | 6.2 m, 5.3 T, 15 MA | 500 MW at Q = 10, parabolic profiles; scans `I_P` x `H_fac` | +| `CENTAUR` | 2.0 m, 10.9 T, 9.6 MA, negative triangularity | 40 MW breakeven, `H_NT23` scaling; scans `I_P` x `B_0` | +| `radiative_ARC` | 4.2 m, 11.5 T, 14 MA | radiative L-mode, `H89`; scans `Zeff_target` x `H_fac` | +| `NSF` | 0.8 m, 3 T, 1 MA, negative triangularity | gEQDSK + profiles file; older layout, no scan block | +| `NSF_NT_CMOD` | 0.8 m, 3 T, 1 MA | parabolic profiles; older layout, no scan block or notebook | `ITER` and `CENTAUR` deliberately use parabolic profiles rather than a gEQDSK, which keeps their geometry free for `POPCON_scan` to vary. Each of the two carries a note at the bottom of its settings file on how closely the 0-D result tracks the published design point, and where it does not. @@ -113,7 +113,7 @@ sc.plot_metric("Q", "max") # one number per cell, as a heatmap sc.output # every cell in one xarray Dataset ``` -The same scan can live in the settings file instead, which is what the `ITER` and `CENTAUR` examples do: +The same scan can live in the settings file instead, which is what every example except the two NSF ones does: ```yaml scan: @@ -123,9 +123,42 @@ scan: Each axis takes either an explicit list of `values` or a `min`/`max`/`N` range, and a `scan=` argument overrides the settings file. Parameters are named as they appear in the settings file, and the whole settings derivation is re-run for each cell rather than a solved field being patched, so scanning `R` on a file that specifies `qstar` correctly re-derives `Ip`. `openpopcon.SCANNABLE_SETTINGS_KEYS` lists what can be varied. +The major radius can be given as either `R` or `R_0` (not both), matching `B_0`, and either name can be scanned whichever one the settings file uses. + Every panel is drawn on the same contour levels so the cells can be compared directly, and the y axis is shared only when it is the Greenwald fraction, since an absolute density axis moves with `n_G = I_p / (pi a^2)`. -Scanning `R`, `a`, `kappa`, `delta` or `I_P` on an example that supplies a gEQDSK is refused rather than silently ignored: `__get_geometry` takes those from the equilibrium, and always takes the ohmic current from it, so some cells would come out identical and others not. Scan `B_0` or `H_fac` on those examples, or clear `gfilename` to use parabolic profiles. +#### One parameter at a time, relative changes, and what stays fixed + +Give only `rows` (or only `cols`) to vary one parameter on its own. `scale` gives the values relative to the settings file instead of absolutely, and `hold` names what stays fixed while the parameter varies: + +```python +# the field on its own, +-25% +sc = op.POPCON_scan(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile, + scan={"rows": {"parameter": "B_0", "scale": [0.75, 1.0, 1.25]}}) + +# the major radius +-25%, three ways +{"rows": {"parameter": "R", "scale": [0.75, 1.0, 1.25], "hold": ["aspect_ratio", "I_P"]}} +{"rows": {"parameter": "R", "scale": [0.75, 1.0, 1.25], "hold": ["aspect_ratio", "qstar"]}} +{"rows": {"parameter": "R", "scale": [0.75, 1.0, 1.25], "hold": ["a", "qstar"]}} +``` + +| `hold` | effect in every cell | +| --- | --- | +| `aspect_ratio` | moves `a` with `R` (or `R` with `a`) so `R/a` stays at the base value | +| `a` | the minor radius stays put, which is what happens anyway | +| `I_P` | the current stays at the base value | +| `qstar` | `q*` stays at the base value, re-deriving `I_P` from each cell's `R`, `a`, `B_0` and `kappa` | +| `B_0` | the on-axis field stays at the base value, even if the file derives it from `B_coil` | + +The same keys work in a settings file's `scan:` block. `hold` counts as setting those keys, so the other axis cannot also scan them. Scanning `Zeff_target` is refused unless the settings file also gives `impurity`: without it `Zeff_target` has no effect and every cell would come out the same. + +`sc.operating_point(T_i_avg=8.0, n_G_frac=0.5)` returns one row per cell at the same point on the grid: the cell's `R`, `a`, `B_0`, `I_P` and `q*`, plus the fusion power, auxiliary power, Q and the rest. Give `n_e_20_avg` instead of `n_G_frac` to hold the density itself rather than the Greenwald fraction, which moves with `I_P / a^2`. At a fixed density and temperature the fusion power does not depend on the field or the confinement factor; those move the power needed to get there, so compare `Paux` and `Q` as well as `Pfusion`. + +#### gEQDSK examples + +Without further settings, a gEQDSK's own geometry and current win: `__get_geometry` takes `R`, `a`, `kappa`, `delta` and `I_P` from the equilibrium when they differ from the settings by more than 10%, and always takes the ohmic current from it. Scanning any of them is then refused rather than silently giving some identical cells. + +`gfile_rescale: True`, which the `MANTA` example sets, uses the equilibrium for its shape and profiles only. Every flux surface is mapped onto the settings `R` and `a`, as `r -> R + (a/a_eq)(r - R_eq)` with `z` scaled the same way about the plasma mid-height, and the current profile carries the settings `I_P`. That makes `R`, `R_0`, `a`, `I_P` and `qstar` scannable; `kappa` and `delta` stay the equilibrium's. The flux surface volumes and areas are exact for the mapped surfaces. The q profile and trapped particle fraction are rescaled to leading order in the inverse aspect ratio, `q ~ a^2 B_0 / (R I_p)` and `f_t ~ sqrt(a/R)`; that is not a new equilibrium, so keep geometry scans near the file's own values. `sc.write_output()` saves the base settings, the scan specification, the combined arrays and the grid plot; `op.POPCON_scan.read_output(name)` reads it back. diff --git a/openpopcon/core.py b/openpopcon/core.py index 46344ce..448487d 100644 --- a/openpopcon/core.py +++ b/openpopcon/core.py @@ -68,6 +68,7 @@ KNOWN_SETTINGS_KEYS = { "name", "R", + "R_0", "a", "kappa", "delta", @@ -86,6 +87,7 @@ "nr", "gfilename", "profsfilename", + "gfile_rescale", "j_alpha1", "j_alpha2", "j_offset", @@ -127,6 +129,7 @@ # raw file contents rather than patching derived fields SCANNABLE_SETTINGS_KEYS = { "R", + "R_0", "a", "kappa", "delta", @@ -174,11 +177,16 @@ "parallel": "it only controls how the solve is threaded", "impurityfractions": "it is an array; scan Zeff_target instead", "impurity": "it selects which species Zeff_target applies to", + "gfile_rescale": "it decides how the gEQDSK is used; set it in the settings file", } # read() prefers the first key of each pair, so overriding the second without -# removing the first would silently do nothing and produce identical cells +# removing the first would silently do nothing and produce identical cells. +# R and R_0 are aliases that read() refuses to see together, so each drops the +# other SCAN_SHADOWS = { + "R": ("R_0",), + "R_0": ("R",), "qstar": ("I_P",), "B_coil": ("B_0",), "wall_thickness": ("B_0",), @@ -186,7 +194,12 @@ # geometry that __get_geometry takes from the gEQDSK when one is supplied, # overriding whatever the settings file says -GEQDSK_OWNED_KEYS = {"R", "a", "kappa", "delta", "I_P", "qstar"} +GEQDSK_OWNED_KEYS = {"R", "R_0", "a", "kappa", "delta", "I_P", "qstar"} + +# ...except these, when gfile_rescale is set: the gEQDSK is then moved and +# resized to the settings R and a, and carries the settings current, instead +# of those being taken from the file +GEQDSK_RESCALABLE_KEYS = {"R", "R_0", "a", "I_P", "qstar"} # Everything __get_geometry takes from a gEQDSK depends only on the file and @@ -221,6 +234,19 @@ def _compute_gfile_geometry(gfilename: str, nr: int) -> dict: psin_ft, ftrapped_profile = get_trapped_particle_fraction(gfile) ftrapped_profile = np.interp(sqrtpsin, np.sqrt(psin_ft), ftrapped_profile) + # what gfile_rescale needs to move and resize the flux surfaces exactly. By + # Pappus, dV/dR = 2 pi (cross-sectional area) and dS/dR = 2 pi (perimeter) + # for a translation; see __get_geometry for the resize. Each is on the same + # grid as the quantity it adjusts + dVdR = np.empty(len(fs)) + dSdR = np.empty(len(fs)) + for i, contour in enumerate(fs): + r, z = contour[:, 0], contour[:, 1] + dVdR[i] = 2 * np.pi * np.abs(np.sum(r[:-1] * z[1:] - r[1:] * z[:-1])) / 2 + # summed the same way get_fluxvolumes sums the area it is shifting + dSdR[i] = np.trapezoid(2 * np.pi * np.hypot(np.diff(r), np.diff(z))) + dVdR = np.interp(sqrtpsin, np.sqrt(psin), dVdR) + return { "sqrtpsin": sqrtpsin, "volgrid": volgrid, @@ -228,6 +254,10 @@ def _compute_gfile_geometry(gfilename: str, nr: int) -> dict: "qr": qr, "Jrms_norm": Jrms_norm, "ftrapped_profile": ftrapped_profile, + "dVdR": dVdR, + "dSdR": dSdR, + # vacuum field at the LCFS geometric centre, which q was solved for + "geq_B0": float(gfile["bcentr"]) * float(gfile["rcentr"]) / geq_R, "Ipint": Ipint, "geq_a": geq_a, "geq_R": geq_R, @@ -642,14 +672,16 @@ def BetaN(s, T_i_keV, n_e_20) -> float: beta = 2 mu0

/ (B^2)

= int P dV / V (average pressure) - Since Pressure per m^3 is n_eT_e+n_iT_i, + Since Pressure per m^3 is n_eT_e+n_iT_i, and W_tot per m^3 is 3/2 (n_eT_e+n_iT_i), P = 2/3 W_tot beta_N = beta a B0 / (Ip) """ P_avg = ( 1e6 - * volume_integral(s, s.sqrtpsin, (2/3) * _W_tot_prof(s, s.sqrtpsin, T_i_keV, n_e_20)) + * volume_integral( + s, s.sqrtpsin, (2 / 3) * _W_tot_prof(s, s.sqrtpsin, T_i_keV, n_e_20) + ) / s.V ) beta = 2 * (4e-7 * np.pi) * P_avg / (s.B0**2) @@ -1165,7 +1197,9 @@ def __init__(self) -> None: self.resistivity_alg: int = ( 0 # Resistivity algorithm. 0 = Jardin, 1 = Paz-Soldan, 2 = local maximum ) - self.verbosity: int = 0 # Verbosity level. 0 = silent, 1 = normal, 2 = debug, 3 = print all matrices + self.verbosity: int = ( + 0 # Verbosity level. 0 = silent, 1 = normal, 2 = debug, 3 = print all matrices + ) self.state = None # Set by build_state after setup @@ -1622,7 +1656,12 @@ def _load(self, data: dict, settingsfile: str) -> None: # Params # ----------------------------------------------------------- self.name = str(data["name"]) - self.R = float(data["R"]) + # R_0 is an alias for R, named to match B_0 + if "R" in data and "R_0" in data: + raise ValueError( + "R and R_0 are both the major radius; give only one of them." + ) + self.R = float(data["R_0"] if "R_0" in data else data["R"]) self.a = float(data["a"]) self.kappa = float(data["kappa"]) self.delta = float(data["delta"]) @@ -1704,6 +1743,9 @@ def _load(self, data: dict, settingsfile: str) -> None: self.nr = int(data["nr"]) self.gfilename = self._resolve(str(safe_get(data, "gfilename", ""))) + # move and resize the gEQDSK to R, a and Ip rather than taking + # those from it + self.gfile_rescale = bool(safe_get(data, "gfile_rescale", False)) self.profsfilename = self._resolve(str(safe_get(data, "profsfilename", ""))) self.j_alpha1 = float(safe_get(data, "j_alpha1", 1)) @@ -2808,6 +2850,7 @@ def __get_geometry(self) -> None: print(f"Triangularity: {geq_delta}") print(f"z0: {geq_z0}") print("gEQDSK Ip:", Ipint) + print("gEQDSK B0:", geo["geq_B0"]) if self.settings.verbosity > 1: print("Len of sqrtpsin:", len(sqrtpsin)) @@ -2817,18 +2860,59 @@ def __get_geometry(self) -> None: print("Len of agrid:", len(agrid)) print("Len of ftrapped_profile:", len(ftrapped_profile)) - if np.abs(geq_a / self.settings.a - 1) > 0.1: - print( - f"Warning: gEQDSK minor radius ({geq_a} m) differs significantly from settings a ({self.settings.a} m). Defaulting to gEQDSK value." + if self.settings.gfile_rescale: + # keep the settings R, a and Ip, and map every flux surface + # onto them: r -> R + s (r - geq_R), z -> z0 + s (z - z0) with + # s = a / geq_a. kappa and delta are unchanged by construction. + # Volumes and areas follow exactly. q and the trapped fraction + # are rescaled to leading order in the inverse aspect ratio, + # q ~ a^2 B_0 / (R Ip) and f_t ~ sqrt(a / R) + R, a, Ip = self.settings.R, self.settings.a, self.settings.Ip + sc = a / geq_a + offset = R - sc * geq_R + volgrid = sc**3 * volgrid + sc**2 * offset * geo["dVdR"] + agrid = sc**2 * agrid + sc * offset * geo["dSdR"] + # normalized so that its integral over the cross-section is 1 + Jrms_norm = Jrms_norm / sc**2 + qr = ( + qr + * sc**2 + * (geq_R / R) + * (Ipint / Ip) + * (self.settings.B0 / geo["geq_B0"]) ) - self.settings.a = geq_a - self.algorithms.a = geq_a - if np.abs(geq_R / self.settings.R - 1) > 0.1: - print( - f"Warning: gEQDSK major radius ({geq_R} m) differs significantly from settings R ({self.settings.R} m). Defaulting to gEQDSK value." + ftrapped_profile = np.minimum( + ftrapped_profile * np.sqrt(sc * geq_R / R), 1.0 ) - self.settings.R = geq_R - self.algorithms.R = geq_R + Itot = Ip + if self.settings.verbosity > 0: + print( + f"gEQDSK rescaled from R = {geq_R:.4g} m, a = {geq_a:.4g} m, " + f"Ip = {Ipint:.4g} MA to R = {R:.4g} m, a = {a:.4g} m, " + f"Ip = {Ip:.4g} MA (gfile_rescale)." + ) + else: + if np.abs(geq_a / self.settings.a - 1) > 0.1: + print( + f"Warning: gEQDSK minor radius ({geq_a} m) differs significantly from settings a ({self.settings.a} m). Defaulting to gEQDSK value." + ) + self.settings.a = geq_a + self.algorithms.a = geq_a + if np.abs(geq_R / self.settings.R - 1) > 0.1: + print( + f"Warning: gEQDSK major radius ({geq_R} m) differs significantly from settings R ({self.settings.R} m). Defaulting to gEQDSK value." + ) + self.settings.R = geq_R + self.algorithms.R = geq_R + if np.abs(Ipint / self.settings.Ip - 1) > 0.1: + print( + f"Warning: gEQDSK Ip ({Ipint}) differs significantly from settings Ip ({self.settings.Ip}). Defaulting to gEQDSK value." + ) + self.settings.Ip = Ipint + self.algorithms.Ip = Ipint + # the ohmic current always comes from the equilibrium + Itot = Ipint + if np.abs(geq_kappa / self.settings.kappa - 1) > 0.1: print( f"Warning: gEQDSK elongation ({geq_kappa}) differs significantly from settings kappa ({self.settings.kappa}). Defaulting to gEQDSK value." @@ -2841,17 +2925,11 @@ def __get_geometry(self) -> None: ) self.settings.delta = geq_delta self.algorithms.delta = geq_delta - if np.abs(Ipint / self.settings.Ip - 1) > 0.1: - print( - f"Warning: gEQDSK Ip ({Ipint}) differs significantly from settings Ip ({self.settings.Ip}). Defaulting to gEQDSK value." - ) - self.settings.Ip = Ipint - self.algorithms.Ip = Ipint self.algorithms._addextprof(sqrtpsin, -2) self.algorithms._addextprof(volgrid, -1) self.algorithms._addextprof(Jrms_norm, 0) - self.algorithms.Itot = Ipint + self.algorithms.Itot = Itot self.algorithms._addextprof(qr, 5) self.algorithms._addextprof(agrid, -3) self.algorithms._addextprof(ftrapped_profile, 6) @@ -3263,33 +3341,100 @@ def populate_outputs(state, n_e_20_max, T_i_max, T_e_max, Paux, Nn, NTi): ) +# what an axis can hold fixed while its own parameter varies, and what holding +# it means. _resolve_axes turns each into the settings-file keys every cell sets +SCAN_HOLDS = { + "aspect_ratio": "R/a, by moving a with R (or R with a)", + "a": "the minor radius, which is what happens anyway", + "I_P": "the plasma current", + "qstar": "q*, by re-deriving I_P in every cell", + "B_0": "the on-axis field", +} + +# the settings attributes a relative ('scale') axis multiplies, for keys whose +# value can be derived rather than read directly from the file +_SCALE_BASES = { + "R": "R", + "R_0": "R", + "a": "a", + "kappa": "kappa", + "delta": "delta", + "B_0": "B0", + "I_P": "Ip", + "H_fac": "H_fac", + "tipeak_over_tepeak": "tipeak_over_tepeak", +} + + +def _qstar(settings) -> float: + """q* of a settings object; get_Ip is linear in 1/qstar.""" + return ( + phys.get_Ip(1.0, settings.R, settings.a, settings.B0, settings.kappa) + / settings.Ip + ) + + class ScanAxis: """ One scanned parameter and the values it takes. + + scale, if given, is the same values relative to the base settings, kept + for labels. hold names quantities kept fixed while this parameter varies + (see SCAN_HOLDS). An implicit axis stands in for the one a + one-dimensional scan leaves out: it has a single cell and overrides + nothing. """ - def __init__(self, parameter: str, values) -> None: + def __init__( + self, parameter: str, values, scale=None, hold=(), implicit: bool = False + ) -> None: self.parameter = str(parameter) self.values = list(values) + self.scale = None if scale is None else [float(f) for f in scale] + self.hold = [str(h) for h in hold] + self.implicit = implicit # never a bare 'n' or 'T': Dataset.T is transpose - self.dim = "scan_" + re.sub(r"\W", "_", self.parameter) + self.dim = ( + "scan_fixed" if implicit else "scan_" + re.sub(r"\W", "_", self.parameter) + ) + # filled in by POPCON_scan once the base settings are known: the extra + # settings-file keys each cell sets because of 'hold' + self.extras = [{} for _ in self.values] @property def label(self) -> str: - return self.parameter + if not self.hold: + return self.parameter + return f"{self.parameter} ({', '.join(self.hold)} fixed)" + + def value_label(self, k: int) -> str: + text = f"{self.parameter} = {_fmt_value(self.values[k])}" + if self.scale is not None: + text += f" (x{_fmt_value(self.scale[k])})" + return text + + def overrides(self, k: int) -> dict: + """The settings-file keys cell k along this axis replaces.""" + if self.implicit: + return {} + return {self.parameter: self.values[k], **self.extras[k]} def __len__(self) -> int: return len(self.values) def __repr__(self) -> str: - return f"ScanAxis({self.parameter!r}, {self.values!r})" + extra = f", hold={self.hold!r}" if self.hold else "" + return f"ScanAxis({self.parameter!r}, {self.values!r}{extra})" def _parse_axis_spec(spec, which): """ Accepts the several ways a scan axis can be written and returns a - ScanAxis. Handles ('I_P', [...]), {'parameter':..., 'values': [...]} and - {'parameter':..., 'min':..., 'max':..., 'N':...}. + ScanAxis. Handles ('I_P', [...]), {'parameter':..., 'values': [...]}, + {'parameter':..., 'min':..., 'max':..., 'N':...} and + {'parameter':..., 'scale': [...]}, any of the mappings optionally with + 'hold': [...]. A 'scale' axis has no values yet; POPCON_scan fills them + in from the base settings. """ if isinstance(spec, (tuple, list)) and len(spec) == 2: parameter, rest = spec @@ -3308,22 +3453,43 @@ def _parse_axis_spec(spec, which): parameter = spec.pop("parameter", None) if parameter is None: raise ValueError(f"{which}: the scan specification needs a 'parameter'.") + hold = spec.pop("hold", []) + if isinstance(hold, str): + hold = [hold] + unknown = set(spec) - {"values", "scale", "min", "max", "N"} + if unknown: + raise ValueError( + f"{which}: unknown key(s) {', '.join(sorted(unknown))} in the " + f"specification for '{parameter}'. Expected 'values', 'scale', " + f"'min'/'max'/'N' and optionally 'hold'." + ) + given = [k for k in ("values", "scale", "min") if k in spec] + if len(given) > 1: + raise ValueError( + f"{which}: give only one of 'values', 'scale' or 'min'/'max'/'N' " + f"for '{parameter}', got {', '.join(given)}." + ) if "values" in spec: - return ScanAxis(parameter, list(spec["values"])) + return ScanAxis(parameter, list(spec["values"]), hold=hold) + if "scale" in spec: + scale = np.asarray(spec["scale"], dtype=float).tolist() + return ScanAxis(parameter, [None] * len(scale), scale=scale, hold=hold) missing = [k for k in ("min", "max", "N") if k not in spec] if missing: raise ValueError( - f"{which}: scanning '{parameter}' needs either 'values', or all of " - f"'min', 'max' and 'N'. Missing: {', '.join(missing)}." + f"{which}: scanning '{parameter}' needs one of 'values', 'scale', or " + f"all of 'min', 'max' and 'N'. Missing: {', '.join(missing)}." ) # N is validated in _check_scan; guard only against linspace throwing here N = int(spec["N"]) if N < 1: - return ScanAxis(parameter, []) + return ScanAxis(parameter, [], hold=hold) return ScanAxis( - parameter, np.linspace(float(spec["min"]), float(spec["max"]), N).tolist() + parameter, + np.linspace(float(spec["min"]), float(spec["max"]), N).tolist(), + hold=hold, ) @@ -3348,6 +3514,13 @@ class POPCON_scan: sc.plot() sc.plot_metric('Q') + Give only one of rows and cols to vary one parameter on its own. An axis + can give 'scale' (values relative to the settings file) instead of + 'values', and 'hold' to keep other quantities fixed as it varies: + + scan={'rows': {'parameter': 'R', 'scale': [0.75, 1.0, 1.25], + 'hold': ['aspect_ratio', 'qstar']}} + The scan can equally be written as a `scan:` block in the settings file. Parameters are named as they appear in the settings file, and the whole settings derivation is re-run for each cell, so scanning R on a file that @@ -3387,7 +3560,7 @@ def __init__( def _read_spec(self, spec): if not isinstance(spec, dict): raise ValueError( - "The scan specification must be a mapping with 'rows' and " + "The scan specification must be a mapping with 'rows' and/or " f"'cols' keys, got {type(spec).__name__}." ) unknown = set(spec) - {"rows", "cols"} @@ -3396,10 +3569,98 @@ def _read_spec(self, spec): f"Unknown key(s) in the scan specification: " f"{', '.join(sorted(unknown))}. Expected 'rows' and 'cols'." ) + if not spec: + raise ValueError("The scan specification needs 'rows' or 'cols'.") row = _parse_axis_spec(spec["rows"], "rows") if "rows" in spec else None col = _parse_axis_spec(spec["cols"], "cols") if "cols" in spec else None + # a one-dimensional scan: the missing axis is a single cell that only + # labels itself, so every other part of the scan stays two-dimensional + if row is None: + row = ScanAxis("", [0.0], implicit=True) + if col is None: + col = ScanAxis("", [0.0], implicit=True) return row, col + def _base_value(self, parameter): + """The base settings' value of a settings-file key, for 'scale'.""" + s = self.base_settings + if parameter == "qstar": + return _qstar(s) + if parameter in _SCALE_BASES: + return float(getattr(s, _SCALE_BASES[parameter])) + value = s.rawdata.get(parameter) + if isinstance(value, (int, float)) and not isinstance(value, bool): + return float(value) + return None + + def _resolve_axes(self, bad) -> None: + """ + Turns 'scale' into absolute values and 'hold' into the extra keys + each cell sets, now that the base settings are known. + """ + s = self.base_settings + for axis, which in ((self.row, "rows"), (self.col, "cols")): + if axis.implicit: + continue + p = axis.parameter + if axis.scale is not None: + base = self._base_value(p) + if base is None: + bad.append( + f"{which}: '{p}' has no numeric value in the settings to " + f"scale; give 'values' instead." + ) + continue + axis.values = [f * base for f in axis.scale] + axis.extras = [{} for _ in axis.values] + + unknown = [h for h in axis.hold if h not in SCAN_HOLDS] + if unknown: + bad.append( + f"{which}: cannot hold {', '.join(unknown)} fixed. Available: " + f"{', '.join(SCAN_HOLDS)}." + ) + continue + holds = set(axis.hold) + if {"aspect_ratio", "a"} <= holds: + bad.append(f"{which}: cannot hold both aspect_ratio and a fixed.") + if {"I_P", "qstar"} <= holds: + bad.append( + f"{which}: cannot hold both I_P and qstar fixed; at fixed " + f"field one determines the other." + ) + if "aspect_ratio" in holds and p not in ("R", "R_0", "a"): + bad.append( + f"{which}: holding aspect_ratio fixed needs the axis to scan " + f"R, R_0 or a, not '{p}'." + ) + if p in holds: + bad.append(f"{which}: cannot scan '{p}' while holding it fixed.") + + qstar0 = _qstar(s) + for k, v in enumerate(axis.values): + extra = {} + if "aspect_ratio" in holds and v is not None: + if p in ("R", "R_0"): + extra["a"] = v * s.a / s.R + elif p == "a": + extra["R"] = v * s.R / s.a + if "qstar" in holds: + extra["qstar"] = qstar0 + if "I_P" in holds: + extra["I_P"] = s.Ip + if "B_0" in holds: + extra["B_0"] = s.B0 + axis.extras[k] = extra + + @staticmethod + def _axis_keys(axis): + """Every settings-file key an axis sets, with R_0 counted as R.""" + if axis.implicit: + return set() + keys = {axis.parameter} | {k for e in axis.extras for k in e} + return {"R" if k == "R_0" else k for k in keys} + def _check_scan(self) -> None: """ Reports everything wrong with the scan specification at once, in the @@ -3408,20 +3669,19 @@ def _check_scan(self) -> None: bad, warn = [], [] s = self.base_settings - if self.row is None or self.col is None: + self._resolve_axes(bad) + + if self.row.implicit and self.col.implicit: + bad.append("the scan needs at least one of 'rows' and 'cols'.") + clash = self._axis_keys(self.row) & self._axis_keys(self.col) + if clash: bad.append( - "a scan needs two parameters, given as 'rows' and 'cols'. " - "To vary just one, give the other a single value." + f"rows and cols both set {', '.join(sorted(clash))}; each " + f"setting may come from only one axis (hold counts too)." ) - if self.row is not None and self.col is not None: - if self.row.parameter == self.col.parameter: - bad.append( - f"rows and cols both scan '{self.row.parameter}'; they must " - f"be different parameters." - ) for axis, which in ((self.row, "rows"), (self.col, "cols")): - if axis is None: + if axis.implicit: continue p = axis.parameter if p in UNSCANNABLE_REASONS: @@ -3439,17 +3699,40 @@ def _check_scan(self) -> None: warn.append( f"{which}: '{p}' has repeated values, so some cells will be identical." ) + # Zeff_target only sets an impurity fraction when 'impurity' says + # which one; without it every cell would come out the same + if p == "Zeff_target" and "impurity" not in s.rawkeys: + bad.append( + f"{which}: scanning Zeff_target needs 'impurity' in the " + f"settings file, to say which impurity carries it. Without " + f"it Zeff_target has no effect and every cell is identical." + ) # the gEQDSK trap: __get_geometry overrides these from the # equilibrium, and always takes the ohmic current from it - if s.gfilename != "" and p in GEQDSK_OWNED_KEYS: + if s.gfilename == "": + continue + keys = {axis.parameter} | {k for e in axis.extras for k in e} + owned = keys & GEQDSK_OWNED_KEYS + if s.gfile_rescale: + owned -= GEQDSK_RESCALABLE_KEYS + if owned: + rescalable = owned <= GEQDSK_RESCALABLE_KEYS bad.append( - f"{which}: '{p}' cannot be scanned while gfilename is set " - f"({os.path.basename(s.gfilename)}). The geometry is read from " - f"the gEQDSK, which overrides R, a, kappa, delta and Ip whenever " - f"they differ from the settings by more than 10%, and always " + f"{which}: {', '.join(sorted(owned))} cannot be scanned while " + f"gfilename is set ({os.path.basename(s.gfilename)}). The " + f"geometry is read from the gEQDSK, which overrides R, a, " + f"kappa, delta and Ip whenever they differ from the settings " + f"by more than 10%, and always " f"takes the ohmic current from the equilibrium. Some cells would " f"silently be identical and others not. Scan B_0 or H_fac " f"instead, or clear gfilename to use parabolic profiles." + + ( + " To scan R, a or the current with this equilibrium, set " + "gfile_rescale: True, which moves and resizes it to each " + "cell's R and a and gives it that cell's current." + if rescalable + else "" + ) ) for w in warn: @@ -3460,6 +3743,14 @@ def _check_scan(self) -> None: f"{self.settingsfile}:\n - " + "\n - ".join(bad) ) + def _cell_label(self, i, j) -> str: + parts = [ + f"{a.parameter}={_fmt_value(a.values[k])}" + for a, k in ((self.row, i), (self.col, j)) + if not a.implicit + ] + return ", ".join(parts) + def _build_cells(self) -> None: """ Builds every cell's POPCON up front, so that a settings problem at one @@ -3469,9 +3760,9 @@ def _build_cells(self) -> None: self.cells = [[None] * len(self.col) for _ in range(len(self.row))] self.cell_overrides = [[None] * len(self.col) for _ in range(len(self.row))] - for i, vi in enumerate(self.row.values): - for j, vj in enumerate(self.col.values): - overrides = {self.row.parameter: vi, self.col.parameter: vj} + for i in range(len(self.row)): + for j in range(len(self.col)): + overrides = {**self.row.overrides(i), **self.col.overrides(j)} self.cell_overrides[i][j] = overrides try: with contextlib.redirect_stdout(io.StringIO()): @@ -3484,8 +3775,7 @@ def _build_cells(self) -> None: self.cells[i][j] = cell except (ValueError, KeyError) as e: problems.append( - f"cell ({i}, {j}) with {self.row.parameter}=" - f"{_fmt_value(vi)}, {self.col.parameter}={_fmt_value(vj)}: {e}" + f"cell ({i}, {j}) with {self._cell_label(i, j)}: {e}" ) if problems: @@ -3526,10 +3816,7 @@ def run_scan(self, progress: bool = True, quiet: bool = None) -> None: for i in range(nrow): for j in range(ncol): cell = self.cells[i][j] - label = ( - f"{self.row.parameter}={_fmt_value(self.row.values[i])}, " - f"{self.col.parameter}={_fmt_value(self.col.values[j])}" - ) + label = self._cell_label(i, j) start = datetime.datetime.now() if quiet: # read() and __report_invalid print unconditionally; across @@ -3571,8 +3858,9 @@ def algorithms_list(self): @property def scanvariables(self): return { - self.row.parameter: np.asarray(self.row.values), - self.col.parameter: np.asarray(self.col.values), + a.parameter: np.asarray(a.values) + for a in (self.row, self.col) + if not a.implicit } @property @@ -3624,7 +3912,7 @@ def _combine(self): ds[self.row.dim].attrs["parameter"] = self.row.parameter ds[self.col.dim].attrs["parameter"] = self.col.parameter ds.attrs["scan_parameters"] = json.dumps( - [self.row.parameter, self.col.parameter] + [a.parameter for a in (self.row, self.col) if not a.implicit] ) return ds @@ -3640,6 +3928,71 @@ def metric(self, name: str = "Q", reduce: str = "max"): # the tick labels plot_metric puts on the axes return reduced.transpose(self.row.dim, self.col.dim) + def operating_point( + self, + T_i_avg: float, + n_G_frac: float = None, + n_e_20_avg: float = None, + fields=( + "Pfusion", + "Paux", + "Q", + "Pohmic", + "Psol", + "f_rad", + "tauE", + "betaN", + "H98", + "H89", + ), + ): + """ + The same operating point in every cell, as one table: the grid point + nearest the given volume-averaged ion temperature and either + Greenwald fraction or volume-averaged density (10^20 m^-3). Fixing the + Greenwald fraction follows n_G = Ip/(pi a^2) as the scan changes the + current or minor radius; fixing n_e_20_avg holds the density itself. + Points with no physical solution come back as NaN. + """ + import pandas as pd + + if (n_G_frac is None) == (n_e_20_avg is None): + raise ValueError("Give exactly one of n_G_frac and n_e_20_avg.") + rows = [] + for i, j in np.ndindex(*self.shape): + cell = self.cells[i][j] + if cell is None or not hasattr(cell, "output"): + raise RuntimeError("Call run_scan() first.") + out = cell.output + if n_G_frac is not None: + ni = int(np.abs(out.n_G_frac.values - n_G_frac).argmin()) + else: + ni = int(np.abs(out.n_e_20_avg.values - n_e_20_avg).argmin()) + tj = int(np.abs(out.T_i_avg.values - T_i_avg).argmin()) + pt = out.isel({DIM_N: ni, DIM_T: tj}) + valid = float(pt.Paux) < UNPHYSICAL_PLOT_CUTOFF + + record = { + a.parameter: a.values[k] + for a, k in ((self.row, i), (self.col, j)) + if not a.implicit + } + st = cell.settings + record.update( + R=st.R, + a=st.a, + B_0=st.B0, + I_P=st.Ip, + qstar=_qstar(st), + n_G_frac=float(pt.n_G_frac), + n_e_20_avg=float(pt.n_e_20_avg), + T_i_avg=float(pt.T_i_avg), + ) + for name in fields: + record[name] = float(pt[name]) if valid else np.nan + rows.append(record) + return pd.DataFrame(rows) + # ------------------------------------------------------------------- # Plotting # ------------------------------------------------------------------- @@ -3650,7 +4003,7 @@ def _shared_axes(self): only right for the Greenwald fraction: an absolute density axis moves with n_G = Ip/(pi a^2) and would be misleading if shared. """ - scanned = {self.row.parameter, self.col.parameter} + scanned = self._axis_keys(self.row) | self._axis_keys(self.col) yax = self.cells[0][0].plotsettings.yax sharey = yax == "nG" and not (scanned & {"nmin_frac", "nmax_frac"}) sharex = not (scanned & {"Tmin_keV", "Tmax_keV", "tipeak_over_tepeak"}) @@ -3749,18 +4102,12 @@ def plot( ) if self.base_settings.verbosity > 0: print(f" cell ({i}, {j}) not plotted: {e}") - if i == 0: - ax.set_title( - f"{self.col.label} = {_fmt_value(self.col.values[j])}", - fontsize=11, - ) - if j == ncol - 1: + if i == 0 and not self.col.implicit: + ax.set_title(self.col.value_label(j), fontsize=11) + if j == ncol - 1 and not self.row.implicit: twin = ax.twinx() twin.set_yticks([]) - twin.set_ylabel( - f"{self.row.label} = {_fmt_value(self.row.values[i])}", - fontsize=11, - ) + twin.set_ylabel(self.row.value_label(i), fontsize=11) if sharex and i < nrow - 1: ax.set_xlabel("") if sharey and j > 0: @@ -3776,7 +4123,8 @@ def plot( fig.legend(handles, labels, loc="center left", bbox_to_anchor=(1.0, 0.5)) fig.suptitle( - f"{self.base_settings.name}: {self.row.parameter} vs {self.col.parameter}", + f"{self.base_settings.name}: " + + " vs ".join(a.label for a in (self.row, self.col) if not a.implicit), fontsize=13, ) fig.tight_layout() @@ -3814,8 +4162,12 @@ def plot_metric( ax.set_xticklabels([_fmt_value(v) for v in self.col.values]) ax.set_yticks(range(self.shape[0])) ax.set_yticklabels([_fmt_value(v) for v in self.row.values]) - ax.set_xlabel(self.col.parameter) - ax.set_ylabel(self.row.parameter) + ax.set_xlabel(self.col.label) + ax.set_ylabel(self.row.label) + if self.col.implicit: + ax.set_xticks([]) + if self.row.implicit: + ax.set_yticks([]) ax.set_title(f"{reduce} {name} over the n,T grid") finite = z[np.isfinite(z)] @@ -3873,13 +4225,15 @@ def write_output( with open(savedir.joinpath("scan.json"), "w") as f: json.dump( { - "rows": { - "parameter": self.row.parameter, - "values": self.row.values, - }, - "cols": { - "parameter": self.col.parameter, - "values": self.col.values, + **{ + which: { + "parameter": axis.parameter, + "values": axis.values, + "scale": axis.scale, + "hold": axis.hold, + } + for axis, which in ((self.row, "rows"), (self.col, "cols")) + if not axis.implicit }, "overrides": self.cell_overrides, }, @@ -3939,10 +4293,19 @@ def read_output(cls, name: str, directory: str = None): plotsettingsfile=str(readdir.joinpath("plotsettings.yaml")), scalinglawfile=str(readdir.joinpath("scalinglaws.yaml")), scan={ - "rows": (spec["rows"]["parameter"], spec["rows"]["values"]), - "cols": (spec["cols"]["parameter"], spec["cols"]["values"]), + which: { + "parameter": spec[which]["parameter"], + "values": spec[which]["values"], + "hold": spec[which].get("hold", []), + } + for which in ("rows", "cols") + if which in spec }, ) + # values were saved absolute; the relative factors only label them + for axis, which in ((sc.row, "rows"), (sc.col, "cols")): + if which in spec and spec[which].get("scale") is not None: + axis.scale = spec[which]["scale"] ds = xr.load_dataset(readdir.joinpath("arrays.nc")) sc._output = ds diff --git a/openpopcon/resources/examples/MANTA/MANTA_ex.ipynb b/openpopcon/resources/examples/MANTA/MANTA_ex.ipynb new file mode 100644 index 0000000..dd5b15d --- /dev/null +++ b/openpopcon/resources/examples/MANTA/MANTA_ex.ipynb @@ -0,0 +1,315 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# OpenPOPCON MANTA example\n", + "\n", + "MANTA is a negative triangularity fusion pilot plant design: 4.55 m, 11 T,\n", + "10 MA, delta = -0.448. See Rutherford et al., \"MANTA: a\n", + "negative-triangularity NASEM-compliant fusion pilot plant\",\n", + "[Plasma Phys. Control. Fusion 66 (2024) 105006](https://iopscience.iop.org/article/10.1088/1361-6587/ad6708).\n", + "\n", + "Unlike most of the examples, this one reads its geometry and current profile\n", + "from a gEQDSK (`gMANTA`) and its density and temperature profiles from a file\n", + "(`profsMANTA.csv`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import openpopcon as op" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup and run\n", + "\n", + "`settingsfile` holds the machine and algorithm settings, `plotsettingsfile`\n", + "the contour levels and axes. Both are read from this directory, and the\n", + "gEQDSK and profiles file are found relative to the settings file.\n", + "\n", + "The numerics are compiled with numba the first time they run, so the first\n", + "solve in a fresh kernel takes a few seconds longer than the rest." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "settingsfile = \"./POPCON_input_example.yaml\"\n", + "plotsettingsfile = \"./plotsettings.yml\"\n", + "\n", + "pc = op.POPCON(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)\n", + "pc.run_POPCON()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting\n", + "\n", + "This example's plotsettings draw the volume-averaged density on the y axis\n", + "rather than the Greenwald fraction, and H89 rather than H98." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = pc.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding a custom contour\n", + "\n", + "`custom_plot` draws any (Nn, NTi) array on top of an existing POPCON. Here\n", + "it is the bremsstrahlung power, computed point by point from the solver's\n", + "own radial model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "P_brems = np.empty((pc.settings.Nn, pc.settings.NTi))\n", + "rho = pc.algorithms.sqrtpsin\n", + "for i in range(pc.settings.Nn):\n", + " for j in range(pc.settings.NTi):\n", + " P_brems[i, j] = pc.algorithms.volume_integral(\n", + " rho,\n", + " pc.algorithms._P_brem_rad(\n", + " rho, float(pc.output.T_e_max[j]), float(pc.output.n_e_20_max[i])\n", + " ),\n", + " )\n", + "\n", + "fig, ax = pc.plot(show=False)\n", + "pc.custom_plot(\n", + " fig,\n", + " ax,\n", + " P_brems,\n", + " np.linspace(np.min(P_brems), np.max(P_brems), 5),\n", + " \"teal\",\n", + " 1.0,\n", + " r\"$P_{brems}$\",\n", + ")\n", + "ax.legend(bbox_to_anchor=(1, 1), loc=\"upper left\")\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### A point on the plot\n", + "\n", + "The numbers behind one grid point, picked by Greenwald fraction and\n", + "volume-averaged ion temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "i = int(np.abs(pc.output.n_G_frac.values - 0.88).argmin())\n", + "j = int(np.abs(pc.output.T_i_avg.values - 7.3).argmin())\n", + "point = pc.output.isel(n_index=i, T_index=j)\n", + "\n", + "print(f\"n/n_G = {float(point.n_G_frac):.2f}\")\n", + "print(f\" = {float(point.T_i_avg):.1f} keV\")\n", + "print(f\"P_fus = {float(point.Pfusion):.1f} MW\")\n", + "print(f\"P_aux = {float(point.Paux):.1f} MW\")\n", + "print(f\"Q = {float(point.Q):.2f}\")\n", + "print(f\"beta_N = {float(point.betaN):.2f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scoping a single operating point\n", + "\n", + "`single_point` solves one density/temperature pair and shows the profiles\n", + "behind it, which is the quickest way to see why a point on the POPCON sits\n", + "where it does." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pc.single_point(n_G_frac=0.88, Ti_av=7.3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scanning the field and the confinement factor\n", + "\n", + "Only parameters that are re-derived on every run can be scanned; the list is\n", + "in `openpopcon.SCANNABLE_SETTINGS_KEYS`.\n", + "\n", + "This example supplies a gEQDSK and sets `gfile_rescale: True`, so the\n", + "equilibrium supplies the shape (`kappa`, `delta`) and the profiles, and is\n", + "moved, resized and given the current of each cell's `R`, `a` and `I_P`.\n", + "That keeps `R`, `a`, `I_P` and `qstar` scannable. Flux surface volumes\n", + "and areas follow exactly; q and the trapped fraction are rescaled to leading\n", + "order in the inverse aspect ratio, so keep geometry scans near the file's own\n", + "values. With `gfile_rescale: False` the equilibrium's own geometry and\n", + "current are used, and scanning any of those is refused.\n", + "\n", + "The `scan:` block in the settings file varies `B_0` and `H_fac`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc = op.POPCON_scan(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)\n", + "sc.run_scan()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = sc.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`plot_metric` reduces each cell to one number so the trend across the scan\n", + "reads at a glance. Anything in the output works, with any reduction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = sc.plot_metric(\"Q\", reduce=\"max\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Scanning the size of the machine\n", + "\n", + "Because `gfile_rescale` is on, the major radius can be scanned with the\n", + "equilibrium too. `scale` gives the values relative to the settings file, and\n", + "`hold` says what else stays fixed; here the minor radius (the default) and\n", + "the current." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scR = op.POPCON_scan(\n", + " settingsfile=settingsfile,\n", + " plotsettingsfile=plotsettingsfile,\n", + " scan={\"rows\": {\"parameter\": \"R\", \"scale\": [0.9, 1.0, 1.1], \"hold\": [\"I_P\"]}},\n", + ")\n", + "scR.run_scan()\n", + "fig, axs = scR.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scR.operating_point(T_i_avg=7.3, n_G_frac=0.88)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Saving and reloading\n", + "\n", + "`write_output` saves a single POPCON or a whole scan, as a directory or a\n", + "zip archive, and `read_output` brings it back without re-solving." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pc.write_output(name=\"manta\", archive=False, overwrite=True)\n", + "\n", + "pcread = op.POPCON()\n", + "pcread.read_output(\"manta\")\n", + "fig, ax = pcread.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc.write_output(name=\"manta_scan\", archive=False, overwrite=True)\n", + "\n", + "back = op.POPCON_scan.read_output(\"manta_scan\")\n", + "print(back.shape, back.row.parameter, back.col.parameter)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openPOPCON", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/openpopcon/resources/examples/MANTA/POPCON_input_example.yaml b/openpopcon/resources/examples/MANTA/POPCON_input_example.yaml index 761c15e..4cc983d 100644 --- a/openpopcon/resources/examples/MANTA/POPCON_input_example.yaml +++ b/openpopcon/resources/examples/MANTA/POPCON_input_example.yaml @@ -1,97 +1,106 @@ -# OpenPOPCON input file #----------------------------------------------------------------------- -# Machine Paramters +# OpenPOPCON input file: MANTA +# +# MANTA, a negative triangularity fusion pilot plant design. From +# Rutherford et al., "MANTA: a negative-triangularity NASEM-compliant +# fusion pilot plant", Plasma Phys. Control. Fusion 66 (2024) 105006, +# doi:10.1088/1361-6587/ad6708. +# +# Unlike most of the examples, this one takes its geometry and current +# profile from a gEQDSK (gMANTA) and its kinetic profiles from a file +# (profsMANTA.csv). gfile_rescale is on, so the equilibrium is used for its +# shape and profiles and is mapped onto the R, a and I_P given here. That +# keeps R, a, I_P and qstar available to POPCON_scan; kappa and delta stay +# the equilibrium's. #----------------------------------------------------------------------- name: "MANTA" -R: 4.55 # major radius in meters -a: 1.2 # minor radius in meters -kappa: 1.4 # elongation -delta: -0.448 - #---------------------- -# I_P and q* can be calculated from each other. Pick one to specify: -I_P: 10. # plasma current in MA -# or -# qstar: 2.5 +# Machine parameters #---------------------- -#---------------------- -# B_0 and B_coil can be calculated from each other. Pick one to specify: -B_0: 11.0 # on-axis magnetic field in Tesla -# or -# B_coil: 24.0 # magnetic field at the coil in Tesla -# wall_thickness: 1.1 # wall thickness between magnet and separatrix in meters -#---------------------- +# with gfile_rescale (below) the equilibrium is mapped onto R, a and I_P; +# without it, it overrides them when they differ by more than 10% +R: 4.55 # major radius, m (R_0 is accepted as an alias) +a: 1.2 # minor radius, m +kappa: 1.4 # elongation +delta: -0.448 # triangularity -- negative, this is an NT device + +I_P: 10. # plasma current, MA (or give qstar instead) +B_0: 11.0 # on-axis toroidal field, T (or B_coil and wall_thickness) + +tipeak_over_tepeak: 0.9894 #---------------------- -tipeak_over_tepeak: 0.9894 # ratio of ion to electron temperature at peak -fuel: 2 # 1 for D-D, 2 for D-T, 3 for D-He3 +# Plasma composition #---------------------- -#---------------------- -# Impurity Fractions -# In order, the fractions of: -# 0 He ash -# 1 Ne -# 2 Ar -# 3 Kr -# 4 Xe -# 5 W -# Impfracs and Z_eff can be calculated from each other. If you specify -# Z_eff, the impurity fraction for the specified impurity will be overwritten. +fuel: 2 # 1 D-D, 2 D-T, 3 D-He3 + +# He, Ne, Ar, Kr, Xe, W impurityfractions: [0.025, 0., 0., 0.00075, 0., 0.] -Zeff_target: 2.26 # average effective charge of plasma -impurity: 3 # 0 for He, 1 for Ne, 2 for Ar, 3 for Kr, 4 for Xe, 5 for W -#---------------------- + +# krypton is the Zeff carrier; the fraction above is overwritten from this +Zeff_target: 2.26 +impurity: 3 # 0 He, 1 Ne, 2 Ar, 3 Kr, 4 Xe, 5 W #---------------------- -scalinglaw: "H98y2" # Name of tau_E scaling law as defined in scaling_laws.yaml -H_fac: 0.79 # Factor by which to multiply the scaling law +# Confinement #---------------------- +scalinglaw: "H98y2" +H_fac: 0.79 + +nr: 200 # number of radial points + #---------------------- # Profiles -# If you specify a filename, the profiles will be read from that file and normalized. -# Otherwise, the normalized profiles will be parabolic of the form -# f(r) = (1 - offset)*(1 - r**alpha1)**alpha2 + offset -# j profile comes from the equilibrium, the others come from the profs file. +#---------------------- + +# j comes from the equilibrium, n and T from the profiles file. Clear both +# to fall back to parabolic profiles, set with _alpha1, _alpha2 +# and _offset as in the other examples. gfilename: "gMANTA" profsfilename: "profsMANTA.csv" -# j_alpha1: 2. -# j_alpha2: 3. -# j_offset: 0. +# True keeps R, a and I_P from above and moves, resizes and re-currents the +# equilibrium to match, keeping its shape (kappa, delta) and profiles. That +# is what lets POPCON_scan vary R, a, I_P and qstar with a gEQDSK. False +# takes R, a and I_P from the equilibrium whenever they differ from the +# settings by more than 10%, and always takes the ohmic current from it +gfile_rescale: True + +#---------------------- +# Algorithm settings +#---------------------- -# ne_alpha1: 2. -# ne_alpha2: 1.5 -# ne_offset: 0. +Nn: 50 # number of density points +NTi: 50 # number of temperature points -# ni_alpha1: 2. -# ni_alpha2: 1.5 -# ni_offset: 0. +nmax_frac: 1.0 +nmin_frac: 0.3 -# Ti_alpha1: 2. -# Ti_alpha2: 1.5 -# Ti_offset: 0. +Tmax_keV: 11. +Tmin_keV: 6. -# Te_alpha1: 2. -# Te_alpha2: 1.5 -# Te_offset: 0. -#---------------------- +resistivity_model: "Jardin" # Jardin, Paz-Soldan, or maximum +verbosity: 1 +parallel: True #---------------------- -# Algorithm settings +# Scan (used by POPCON_scan; ignored by a plain POPCON) #---------------------- -Nn: 50 # resolution of densities -NTi: 50 # resolution of ion temperatures -nr: 200 # number of radial grid points -nmax_frac: 1.0 # maximum electron density as a fraction of the Greenwald density -nmin_frac: 0.3 # minimum electron density as a fraction of the Greenwald density -Tmax_keV: 11. # maximum volume-averaged temperature in keV -Tmin_keV: 6. # minimum volume-averaged temperature in keV -resistivity_model: "Jardin" # Jardin, Paz-Soldan or maximum (suggested: Jardin) -verbosity: 2 # 0 for no output, 1 for minimal output, 2 for verbose output -parallel: True \ No newline at end of file + +# field and confinement quality, the two machine knobs the gEQDSK leaves +# free +scan: + rows: + parameter: B_0 + min: 10. + max: 12. + N: 3 + cols: + parameter: H_fac + values: [0.7, 0.79, 0.9] diff --git a/openpopcon/resources/examples/MANTA/manta_ex.ipynb b/openpopcon/resources/examples/MANTA/manta_ex.ipynb deleted file mode 100644 index ca05611..0000000 --- a/openpopcon/resources/examples/MANTA/manta_ex.ipynb +++ /dev/null @@ -1,183 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# OpenPOPCON MANTA example\n", - "Welcome! This is an example usage of the OpenPOPCON scoping tool for the [MANTA](https://iopscience.iop.org/article/10.1088/1361-6587/ad6708) tokamak." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import openpopcon as op\n", - "\n", - "%matplotlib qt\n", - "%reload_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup\n", - "The POPCON object requires three settings files in the yaml format.\n", - "The first, 'settingsfile', contains the physical parameters for the tokamak, as well as algorithm information. \n", - "The second, 'plotsettingsfile', contains the settings for the plots. This can be updated after plotting to change the appearance of the plots, without restarting the jupyter notebook.\n", - "The third, 'scalinglawfile', you will not necessarily need to change, but it contains the confinement time $\\tau_E$ scaling laws for the simulation.\n", - "\n", - "When you run this on your own, it is recommended that you copy the first two settings files and modify them to suit your needs; you can leave scalinglawfile as is." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "settingsfile = \"./POPCON_input_example.yaml\"\n", - "plotsettingsfile = \"./plotsettings.yml\"\n", - "\n", - "pc = op.POPCON(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running the code\n", - "\n", - "To run the code, simply run the cell below. This will run the simulation.\n", - "\n", - "OpenPOPCON uses numba 'just in time' (jit) compilation to speed up the simulation. This means that the first time you run the simulation, it will take a little longer to compile the code. Subsequent runs will be faster, until you restart the jupyter notebook kernel, at which the compilation will be required again." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This runs the POPCON\n", - "pc.run_POPCON()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting\n", - "The POPCON object has a method, 'plot', which will plot the results of the simulation. This method takes the settings from the 'plotsettingsfile' and plots the results. You can modify the 'plotsettingsfile' and rerun both 'update_plotsettings' and the plot method to see the changes.\n", - "\n", - "There is also the method 'custom_plot' which allows you to plot any array on top of the simulation results. This can be useful if you have a specific quantity that is not calculated by default." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pc.update_plotsettings()\n", - "fig, ax = pc.plot(show=True)\n", - "\n", - "# Example of how to plot a custom quantity\n", - "P_synchotron = np.empty((pc.settings.Nn, pc.settings.NTi))\n", - "rho = pc.algorithms.sqrtpsin\n", - "for i in range(pc.settings.Nn):\n", - " for j in range(pc.settings.NTi):\n", - " P_synchotron[i, j] = pc.algorithms.volume_integral(\n", - " rho,\n", - " pc.algorithms._P_brem_rad(\n", - " rho, float(pc.output.T_e_max[j]), float(pc.output.n_e_20_max[i])\n", - " ),\n", - " )\n", - "pc.custom_plot(\n", - " fig,\n", - " ax,\n", - " P_synchotron,\n", - " np.linspace(np.min(P_synchotron), np.max(P_synchotron), 5),\n", - " \"teal\",\n", - " 1.0,\n", - " r\"$P_{synch}$\",\n", - ")\n", - "ax.legend(bbox_to_anchor=(1, 1), loc=\"upper left\")\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Scoping operation points\n", - "\n", - "POPCONs, or 'Plasma OPerating CONtours', show a wide range of operating points for a given tokamak. The 'single_point' method lets you explore a single pair of density/temperatures, and look at the resulting profiles. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pc.single_point(n_G_frac=0.88, Ti_av=7.3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Reading and writing outputs\n", - "\n", - "Running this simulation large quantities of times can be time consuming. Saving and loading of POPCON outputs is enabled so that you can save the results of a simulation and load them later to edit the plots. Outputs are saved to the 'outputs' directory as either a folder or a zip archive. See below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pc.write_output(name=\"test\", archive=False, overwrite=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pcread = op.POPCON()\n", - "pcread.read_output(\"test\")\n", - "pcread.plot()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "openPOPCON", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/openpopcon/resources/examples/MANTA/plotsettings.yml b/openpopcon/resources/examples/MANTA/plotsettings.yml index ee603a1..90b093a 100644 --- a/openpopcon/resources/examples/MANTA/plotsettings.yml +++ b/openpopcon/resources/examples/MANTA/plotsettings.yml @@ -1,332 +1,49 @@ #----------------------------------------------------------------------- -# OpenPOPCON Plotting Settings +# OpenPOPCON Plotting Settings: MANTA +# +# Only the entries that differ from resources/default_plotsettings.yml are +# listed; anything left out falls back to the default. +# +# Densities are drawn as volume averages rather than Greenwald fractions, +# and H89 contours are drawn in place of H98. #----------------------------------------------------------------------- -yax: "n20_av" # y-axis variable; nG for greenwald fraction, n20 for density -xax: "T_i_av" # x-axis variable; T_i_av for average ion temperature, - # T_i_ax for maximum ion temperature, - # T_e_av for average electron temperature, - # T_e_ax for maximum electron temperature -figsize: [8, 6] # figure size in inches -fill_invalid: True # fill invalid values with gray - -#---------------------- -# Options per output -#---------------------- - -plotoptions: - Paux: - plot: True # do not plot this output - color: r # line color - linewidth: 3 # line width - label: $P_{aux}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.1,1,10,25,50,100,500,2000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pfusion: - plot: True # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{fus}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [100,200,500,1000,2000,4000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pfusionheating: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{fus}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [100,200,500,1000,2000,4000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Psol: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{SOL}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [10,20,50,80,100] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pohmic: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{OH}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.01,0.1,0.2,0.5,1.] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pbrems: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{brems}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pimprad: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{imprad}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Prad: - plot: True # do not plot this output - color: purple # line color - linewidth: 2 # line width - label: $P_{rad}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [10,50,100,200] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 3 # number of contour levels - Pheat: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{heat}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Ploss: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{loss}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Palpha: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{\alpha}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pdd: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{DD}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pdt: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{DT}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - tauE: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\tau_E$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Q: - plot: True # do not plot this output - color: orange # line color - linewidth: 2 # line width - label: $Q$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.5,1,2,5,10,25,50,100] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - min : -1 # only used if scale is specified - max : 2 # only used if scale is specified - levels: 4 # number of contour levels - H89: - plot: True # do not plot this output - color: blue # line color - linewidth: 2 # line width - label: $H_{89}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - H98: - plot: False # do not plot this output - color: blue # line color - linewidth: 2 # line width - label: $H_{98y2}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.1f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 5 # number of contour levels - vloop: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $V_{loop}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - betaN: - plot: True # do not plot this output - color: forestgreen # line color - linewidth: 2 # line width - label: $\beta_N$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.d' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [1,2,3] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 5 # number of contour levels - n_G_frac: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $n_G$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - n20_max: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $n_{i20}(0)$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - n20_avg: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\langle n_{i20}\rangle$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - T_i_max: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $T_i(0)$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - T_i_avg: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\langle T_i \rangle$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - - +yax: "n20_av" # nG, n20_av or n20_ax +xax: "T_i_av" # T_i_av, T_i_ax, T_e_av or T_e_ax; either axis takes either family +figsize: [8,6] +fill_invalid: True +plotoptions: + Paux: + plot: True + color: r + linewidth: 3 + label: $P_{aux}$ + fontsize: 12 + fmt: '%.2d' + spacing: manual + manuallevels: [0.1,1,10,25,50,100,500,2000] + scale: minmax + levels: 10 + H89: + plot: True + color: blue + linewidth: 2 + label: $H_{89}$ + fontsize: 12 + fmt: '%.2f' + spacing: lin + manuallevels: [] + scale: minmax + levels: 10 + H98: + plot: False + color: blue + linewidth: 2 + label: $H_{98y2}$ + fontsize: 12 + fmt: '%.1f' + spacing: lin + manuallevels: [] + scale: minmax + levels: 5 diff --git a/openpopcon/resources/examples/SPARC/POPCON_input_example.yaml b/openpopcon/resources/examples/SPARC/POPCON_input_example.yaml index f56b475..4553552 100644 --- a/openpopcon/resources/examples/SPARC/POPCON_input_example.yaml +++ b/openpopcon/resources/examples/SPARC/POPCON_input_example.yaml @@ -1,97 +1,112 @@ -# OpenPOPCON input file -#----------------------------------------------------------------------- -# Machine Paramters -#----------------------------------------------------------------------- - -name: "SPARC" - -R: 1.85 # major radius in meters -a: 0.57 # minor radius in meters -kappa: 1.97 # elongation -delta: 0.54 # triangularity - -#---------------------- -# I_P and q* can be calculated from each other. Pick one to specify: -I_P: 8.7 # plasma current in MA -# or -# qstar: 2.5 -#---------------------- - -#---------------------- -# B_0 and B_coil can be calculated from each other. Pick one to specify: -B_0: 12.2 # on-axis magnetic field in Tesla -# or -# B_coil: 24.0 # magnetic field at the coil in Tesla -# wall_thickness: 1.1 # wall thickness between magnet and separatrix in meters -#---------------------- - -#---------------------- -tipeak_over_tepeak: 0.9894 # ratio of ion to electron temperature at peak -fuel: 2 # 1 for D-D, 2 for D-T, 3 for D-He3 -#---------------------- - -#---------------------- -# Impurity Fractions -# In order, the fractions of: -# 0 He ash -# 1 Ne -# 2 Ar -# 3 Kr -# 4 Xe -# 5 W -# Impfracs and Z_eff can be calculated from each other. If you specify -# Z_eff, the impurity fraction for the specified impurity will be overwritten. -impurityfractions: [0.025, 0., 0., 0., 0., 0.] -Zeff_target: 1.5 # average effective charge of plasma -impurity: 2 # 0 for He, 1 for Ne, 2 for Ar, 3 for Kr, 4 for Xe, 5 for W -#---------------------- - -#---------------------- -scalinglaw: "H98y2" # Name of tau_E scaling law as defined in scaling_laws.yaml -H_fac: 1. # Factor by which to multiply the scaling law -#---------------------- - -#---------------------- -# Profiles -# If you specify a filename, the profiles will be read from that file and normalized. -# Otherwise, the normalized profiles will be parabolic of the form -# f(r) = (1 - offset)*(1 - r**alpha1)**alpha2 + offset -# j profile comes from the equilibrium, the others come from the profs file. -gfilename: "" -profsfilename: "" - -j_alpha1: 2. -j_alpha2: 3. -j_offset: 0.01 - -ne_alpha1: 2.5 -ne_alpha2: 1. -ne_offset: 0.4 - -ni_alpha1: 2.5 -ni_alpha2: 1. -ni_offset: 0.4 - -Ti_alpha1: 2. -Ti_alpha2: 3.5 -Ti_offset: 0.18 - -Te_alpha1: 2. -Te_alpha2: 3.5 -Te_offset: 0.18 -#---------------------- - - -#---------------------- -# Algorithm settings -#---------------------- -Nn: 50 # resolution of densities -NTi: 50 # resolution of ion temperatures -nr: 100 # number of radial grid points -nmax_frac: 1. # maximum electron density as a fraction of the Greenwald density -nmin_frac: 0.05 # minimum electron density as a fraction of the Greenwald density -Tmax_keV: 16. # maximum volume-averaged temperature in keV -Tmin_keV: 3. # minimum volume-averaged temperature in keV -resistivity_model: "Jardin" # Jardin, Paz-Soldan or maximum (suggested: Jardin) -verbosity: 1 # 0 for no output, 1 for minimal output, 2 for verbose output -parallel: True \ No newline at end of file +#----------------------------------------------------------------------- +# OpenPOPCON input file: SPARC +# +# SPARC, the compact high-field D-T tokamak being built by Commonwealth +# Fusion Systems and MIT. Machine parameters from Creely et al., "Overview +# of the SPARC tokamak", J. Plasma Phys. 86 (2020) 865860502. +# +# Parabolic profiles rather than a gEQDSK, deliberately: it keeps R, a, +# kappa, delta and I_P available to POPCON_scan, which the gEQDSK path +# overrides. +#----------------------------------------------------------------------- + +name: "SPARC" + +#---------------------- +# Machine parameters +#---------------------- + +R: 1.85 # major radius, m (R_0 is accepted as an alias) +a: 0.57 # minor radius, m +kappa: 1.97 # elongation +delta: 0.54 # triangularity + +I_P: 8.7 # plasma current, MA (or give qstar instead) +B_0: 12.2 # on-axis toroidal field, T (or B_coil and wall_thickness) + +tipeak_over_tepeak: 0.9894 + +#---------------------- +# Plasma composition +#---------------------- + +fuel: 2 # 1 D-D, 2 D-T, 3 D-He3 + +# He, Ne, Ar, Kr, Xe, W +impurityfractions: [0.025, 0., 0., 0., 0., 0.] + +# argon is the Zeff carrier; the fraction above is overwritten from this +Zeff_target: 1.5 +impurity: 2 # 0 He, 1 Ne, 2 Ar, 3 Kr, 4 Xe, 5 W + +#---------------------- +# Confinement +#---------------------- + +scalinglaw: "H98y2" +H_fac: 1. + +nr: 100 # number of radial points + +#---------------------- +# Profiles +#---------------------- + +gfilename: "" +profsfilename: "" + +# f(rho) = (1 - offset) * (1 - rho^alpha1)^alpha2 + offset +j_alpha1: 2. +j_alpha2: 3. +j_offset: 0.01 + +ne_alpha1: 2.5 +ne_alpha2: 1. +ne_offset: 0.4 + +ni_alpha1: 2.5 +ni_alpha2: 1. +ni_offset: 0.4 + +Ti_alpha1: 2. +Ti_alpha2: 3.5 +Ti_offset: 0.18 + +Te_alpha1: 2. +Te_alpha2: 3.5 +Te_offset: 0.18 + +#---------------------- +# Algorithm settings +#---------------------- + +Nn: 50 # number of density points +NTi: 50 # number of temperature points + +# n_G = Ip / (pi a^2) = 8.52e20 m^-3 +nmax_frac: 1. +nmin_frac: 0.05 + +Tmax_keV: 16. +Tmin_keV: 3. + +resistivity_model: "Jardin" # Jardin, Paz-Soldan, or maximum + +verbosity: 1 +parallel: True + +#---------------------- +# Scan (used by POPCON_scan; ignored by a plain POPCON) +#---------------------- + +# The major radius against the field, at fixed current. R_0 is scanned +# here to show the alias; it replaces the R given above in every cell. +scan: + rows: + parameter: R_0 + values: [1.65, 1.85, 2.05] + cols: + parameter: B_0 + min: 10. + max: 14. + N: 3 diff --git a/openpopcon/resources/examples/SPARC/SPARC_EX.ipynb b/openpopcon/resources/examples/SPARC/SPARC_EX.ipynb deleted file mode 100644 index 25528a4..0000000 --- a/openpopcon/resources/examples/SPARC/SPARC_EX.ipynb +++ /dev/null @@ -1,195 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# OpenPOPCON MANTA example\n", - "Welcome! This is an example usage of the OpenPOPCON scoping tool for the [MANTA](https://iopscience.iop.org/article/10.1088/1361-6587/ad6708) tokamak." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import openpopcon as op" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup\n", - "The POPCON object requires three settings files in the yaml format.\n", - "The first, 'settingsfile', contains the physical parameters for the tokamak, as well as algorithm information. \n", - "The second, 'plotsettingsfile', contains the settings for the plots. This can be updated after plotting to change the appearance of the plots, without restarting the jupyter notebook.\n", - "The third, 'scalinglawfile', you will not necessarily need to change, but it contains the confinement time $\\tau_E$ scaling laws for the simulation.\n", - "\n", - "When you run this on your own, it is recommended that you copy the first two settings files and modify them to suit your needs; you can leave scalinglawfile as is." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "settingsfile = \"./POPCON_input_example.yaml\"\n", - "plotsettingsfile = \"./plotsettings.yml\"\n", - "\n", - "pc = op.POPCON(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running the code\n", - "\n", - "To run the code, simply run the cell below. This will run the simulation.\n", - "\n", - "OpenPOPCON uses numba 'just in time' (jit) compilation to speed up the simulation. This means that the first time you run the simulation, it will take a little longer to compile the code. Subsequent runs will be faster, until you restart the jupyter notebook kernel, at which the compilation will be required again." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# # This runs the POPCON\n", - "\n", - "# pc.settings.verbosity = 0\n", - "\n", - "# pc.settings.parallel = True\n", - "# %timeit pc.run_POPCON()\n", - "\n", - "# pc.settings.parallel = False\n", - "# %timeit pc.run_POPCON()\n", - "\n", - "pc.run_POPCON()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting\n", - "The POPCON object has a method, 'plot', which will plot the results of the simulation. This method takes the settings from the 'plotsettingsfile' and plots the results. You can modify the 'plotsettingsfile' and rerun both 'update_plotsettings' and the plot method to see the changes.\n", - "\n", - "There is also the method 'custom_plot' which allows you to plot any array on top of the simulation results. This can be useful if you have a specific quantity that is not calculated by default." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pc.update_plotsettings()\n", - "fig, ax = pc.plot(show=False)\n", - "\n", - "# Example of how to plot a custom quantity\n", - "P_synchotron = np.empty((pc.settings.Nn, pc.settings.NTi))\n", - "rho = pc.algorithms.sqrtpsin\n", - "for i in range(pc.settings.Nn):\n", - " for j in range(pc.settings.NTi):\n", - " P_synchotron[i, j] = pc.algorithms.volume_integral(\n", - " rho,\n", - " pc.algorithms._P_brem_rad(\n", - " rho, float(pc.output.T_e_max[j]), float(pc.output.n_e_20_max[i])\n", - " ),\n", - " )\n", - "pc.custom_plot(\n", - " fig,\n", - " ax,\n", - " P_synchotron,\n", - " np.linspace(np.min(P_synchotron), np.max(P_synchotron), 5),\n", - " \"teal\",\n", - " 1.0,\n", - " r\"$P_{synch}$\",\n", - ")\n", - "ax.legend(bbox_to_anchor=(1, 1), loc=\"upper left\")\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Scoping operation points\n", - "\n", - "POPCONs, or 'Plasma OPerating CONtours', show a wide range of operating points for a given tokamak. The 'single_point' method lets you explore a single pair of density/temperatures, and look at the resulting profiles. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pc.single_point(n_G_frac=0.37, Ti_av=7.3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Reading and writing outputs\n", - "\n", - "Running this simulation large quantities of times can be time consuming. Saving and loading of POPCON outputs is enabled so that you can save the results of a simulation and load them later to edit the plots. Outputs are saved to the 'outputs' directory as either a folder or a zip archive. See below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pc.write_output(name=\"sparc_test\", archive=False, overwrite=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pcread = op.POPCON()\n", - "pcread.read_output(\"sparc_test\")\n", - "pcread.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "openPOPCON", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/openpopcon/resources/examples/SPARC/SPARC_ex.ipynb b/openpopcon/resources/examples/SPARC/SPARC_ex.ipynb new file mode 100644 index 0000000..463b50a --- /dev/null +++ b/openpopcon/resources/examples/SPARC/SPARC_ex.ipynb @@ -0,0 +1,244 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# OpenPOPCON SPARC example\n", + "\n", + "SPARC is the compact, high-field D-T tokamak being built by Commonwealth\n", + "Fusion Systems and MIT: 1.85 m major radius, 12.2 T, 8.7 MA. Machine\n", + "parameters are from Creely et al., \"Overview of the SPARC tokamak\",\n", + "J. Plasma Phys. 86 (2020) 865860502.\n", + "\n", + "This example uses parabolic profiles rather than a gEQDSK, which keeps `R`,\n", + "`a`, `kappa`, `delta` and `I_P` free for `POPCON_scan` to vary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import openpopcon as op" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup and run\n", + "\n", + "`settingsfile` holds the machine and algorithm settings, `plotsettingsfile`\n", + "the contour levels and axes. Both are read from this directory.\n", + "\n", + "The numerics are compiled with numba the first time they run, so the first\n", + "solve in a fresh kernel takes a few seconds longer than the rest." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "settingsfile = \"./POPCON_input_example.yaml\"\n", + "plotsettingsfile = \"./plotsettings.yml\"\n", + "\n", + "pc = op.POPCON(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)\n", + "pc.run_POPCON()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting\n", + "\n", + "This example's plotsettings draw the volume-averaged density on the y axis\n", + "rather than the Greenwald fraction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = pc.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### A point on the plot\n", + "\n", + "The numbers behind one grid point, picked by Greenwald fraction and\n", + "volume-averaged ion temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "i = int(np.abs(pc.output.n_G_frac.values - 0.37).argmin())\n", + "j = int(np.abs(pc.output.T_i_avg.values - 7.3).argmin())\n", + "point = pc.output.isel(n_index=i, T_index=j)\n", + "\n", + "print(f\"n/n_G = {float(point.n_G_frac):.2f}\")\n", + "print(f\" = {float(point.T_i_avg):.1f} keV\")\n", + "print(f\"P_fus = {float(point.Pfusion):.1f} MW\")\n", + "print(f\"P_aux = {float(point.Paux):.1f} MW\")\n", + "print(f\"Q = {float(point.Q):.2f}\")\n", + "print(f\"beta_N = {float(point.betaN):.2f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scoping a single operating point\n", + "\n", + "`single_point` solves one density/temperature pair and shows the profiles\n", + "behind it, which is the quickest way to see why a point on the POPCON sits\n", + "where it does." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pc.single_point(n_G_frac=0.37, Ti_av=7.3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scanning the major radius against the field\n", + "\n", + "`POPCON_scan` runs a full POPCON at every combination of two machine\n", + "parameters. The ranges come from the `scan:` block at the bottom of the\n", + "settings file.\n", + "\n", + "The rows scan `R_0`, which is an alias for `R`: the settings file gives `R`,\n", + "and each cell replaces it with the scanned `R_0`. The current is held fixed,\n", + "so a larger machine here also has a higher q and a lower Greenwald density." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc = op.POPCON_scan(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)\n", + "sc.run_scan()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = sc.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`plot_metric` reduces each cell to one number so the trend across the scan\n", + "reads at a glance. Anything in the output works, with any reduction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = sc.plot_metric(\"Pfusion\", reduce=\"max\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Scanning from Python instead of the settings file\n", + "\n", + "Passing `scan=` overrides the block in the settings file. Each axis takes\n", + "either an explicit list of values or a `min`/`max`/`N` range." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc2 = op.POPCON_scan(\n", + " settingsfile=settingsfile,\n", + " plotsettingsfile=plotsettingsfile,\n", + " scan={\n", + " \"rows\": (\"I_P\", {\"min\": 7.5, \"max\": 9.9, \"N\": 3}),\n", + " \"cols\": (\"H_fac\", [0.8, 1.0, 1.2]),\n", + " },\n", + ")\n", + "sc2.run_scan()\n", + "fig, axs = sc2.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Saving and reloading a scan\n", + "\n", + "`write_output` saves the whole scan: the base settings, the scan\n", + "specification, the combined arrays and the grid plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc.write_output(name=\"sparc_scan\", archive=False, overwrite=True)\n", + "\n", + "back = op.POPCON_scan.read_output(\"sparc_scan\")\n", + "print(back.shape, back.row.parameter, back.col.parameter)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openPOPCON", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/openpopcon/resources/examples/SPARC/plotsettings.yml b/openpopcon/resources/examples/SPARC/plotsettings.yml index 3d7824a..081942f 100644 --- a/openpopcon/resources/examples/SPARC/plotsettings.yml +++ b/openpopcon/resources/examples/SPARC/plotsettings.yml @@ -1,332 +1,37 @@ #----------------------------------------------------------------------- -# OpenPOPCON Plotting Settings +# OpenPOPCON Plotting Settings: SPARC +# +# Only the entries that differ from resources/default_plotsettings.yml are +# listed; anything left out falls back to the default. +# +# Densities are drawn as volume averages rather than Greenwald fractions. #----------------------------------------------------------------------- -yax: "n20_av" # y-axis variable; nG for greenwald fraction, n20 for density -xax: "T_i_av" # x-axis variable; T_i_av for average ion temperature, - # T_i_ax for maximum ion temperature, - # T_e_av for average electron temperature, - # T_e_ax for maximum electron temperature -figsize: [8, 6] # figure size in inches -fill_invalid: True # fill invalid values with gray - -#---------------------- -# Options per output -#---------------------- - -plotoptions: - Paux: - plot: True # do not plot this output - color: r # line color - linewidth: 3 # line width - label: $P_{aux}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.1,1,10,25,50,100,500,2000] # only used if manual spacing - scale: specified # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pfusion: - plot: True # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{fus}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [100,200,500,1000,2000,4000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pfusionheating: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{fus}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [100,200,500,1000,2000,4000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Psol: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{SOL}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [10,20,50,80,100] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pohmic: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{OH}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.01,0.1,0.2,0.5,1.] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pbrems: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{brems}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pimprad: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{imprad}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Prad: - plot: True # do not plot this output - color: purple # line color - linewidth: 2 # line width - label: $P_{rad}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [10,50,100,200] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 3 # number of contour levels - Pheat: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{heat}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Ploss: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{loss}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Palpha: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{\alpha}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pdd: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{DD}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pdt: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{DT}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - tauE: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\tau_E$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Q: - plot: True # do not plot this output - color: orange # line color - linewidth: 2 # line width - label: $Q$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.5,1,2,5,10,25,50,100] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - min : -1 # only used if scale is specified - max : 2 # only used if scale is specified - levels: 4 # number of contour levels - H89: - plot: False # do not plot this output - color: blue # line color - linewidth: 2 # line width - label: $H_{89}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - H98: - plot: True # do not plot this output - color: blue # line color - linewidth: 2 # line width - label: $H_{98y2}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.1f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 5 # number of contour levels - vloop: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $V_{loop}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - betaN: - plot: True # do not plot this output - color: forestgreen # line color - linewidth: 2 # line width - label: $\beta_N$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [1,2,3] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 5 # number of contour levels - n_G_frac: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $n_G$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - n20_max: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $n_{i20}(0)$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - n20_avg: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\langle n_{i20}\rangle$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - T_i_max: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $T_i(0)$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - T_i_avg: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\langle T_i \rangle$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - - +yax: "n20_av" # nG, n20_av or n20_ax +xax: "T_i_av" # T_i_av, T_i_ax, T_e_av or T_e_ax; either axis takes either family +figsize: [8,6] +fill_invalid: True +plotoptions: + Paux: + plot: True + color: r + linewidth: 3 + label: $P_{aux}$ + fontsize: 12 + fmt: '%.2d' + spacing: manual + manuallevels: [0.1,1,10,25,50,100,500,2000] + scale: specified + levels: 10 + betaN: + plot: True + color: forestgreen + linewidth: 2 + label: $\beta_N$ + fontsize: 12 + fmt: '%.2f' + spacing: lin + manuallevels: [1,2,3] + scale: minmax + levels: 5 diff --git a/openpopcon/resources/examples/radiative_ARC/POPCON_input_example.yaml b/openpopcon/resources/examples/radiative_ARC/POPCON_input_example.yaml index 53ef360..c9ded17 100644 --- a/openpopcon/resources/examples/radiative_ARC/POPCON_input_example.yaml +++ b/openpopcon/resources/examples/radiative_ARC/POPCON_input_example.yaml @@ -1,66 +1,61 @@ -# OpenPOPCON input file #----------------------------------------------------------------------- -# Machine Paramters +# OpenPOPCON input file: radiative ARC +# +# An ARC-class, high-field D-T reactor run in a radiative L-mode, with +# krypton seeded to Zeff = 2.1 and the H89 L-mode scaling. +# +# Parabolic profiles rather than a gEQDSK, deliberately: it keeps R, a, +# kappa, delta and I_P available to POPCON_scan, which the gEQDSK path +# overrides. #----------------------------------------------------------------------- name: "ARC Rad-Lmode" -R: 4.2 # major radius in meters -a: 1.2 # minor radius in meters -kappa: 1.6 # elongation -delta: 0.3 # triangularity - #---------------------- -# I_P and q* can be calculated from each other. Pick one to specify: -I_P: 14. # plasma current in MA -# or -# qstar: 2.5 +# Machine parameters #---------------------- -#---------------------- -# B_0 and B_coil can be calculated from each other. Pick one to specify: -B_0: 11.5 # on-axis magnetic field in Tesla -# or -# B_coil: 24.0 # magnetic field at the coil in Tesla -# wall_thickness: 1.1 # wall thickness between magnet and separatrix in meters -#---------------------- +R: 4.2 # major radius, m (R_0 is accepted as an alias) +a: 1.2 # minor radius, m +kappa: 1.6 # elongation +delta: 0.3 # triangularity + +I_P: 14. # plasma current, MA (or give qstar instead) +B_0: 11.5 # on-axis toroidal field, T (or B_coil and wall_thickness) + +tipeak_over_tepeak: 1.1 #---------------------- -tipeak_over_tepeak: 1.1 # ratio of ion to electron temperature at peak -fuel: 2 # 1 for D-D, 2 for D-T, 3 for D-He3 +# Plasma composition #---------------------- -#---------------------- -# Impurity Fractions -# In order, the fractions of: -# 0 He ash -# 1 Ne -# 2 Ar -# 3 Kr -# 4 Xe -# 5 W -# Impfracs and Z_eff can be calculated from each other. If you specify -# Z_eff, the impurity fraction for the specified impurity will be overwritten. +fuel: 2 # 1 D-D, 2 D-T, 3 D-He3 + +# He, Ne, Ar, Kr, Xe, W impurityfractions: [0.02, 0., 0., 0.001, 0., 0.] -Zeff_target: 2.1 # average effective charge of plasma -impurity: 3 # 0 for He, 1 for Ne, 2 for Ar, 3 for Kr, 4 for Xe, 5 for W -#---------------------- + +# krypton is the Zeff carrier; the fraction above is overwritten from this +Zeff_target: 2.1 +impurity: 3 # 0 He, 1 Ne, 2 Ar, 3 Kr, 4 Xe, 5 W #---------------------- -scalinglaw: "H89" # Name of tau_E scaling law as defined in scaling_laws.yaml -H_fac: 1.3 # Factor by which to multiply the scaling law -nr: 100 # number of radial grid points +# Confinement #---------------------- +# an L-mode scaling, since the radiative scenario gives up the pedestal +scalinglaw: "H89" +H_fac: 1.3 + +nr: 100 # number of radial points + #---------------------- # Profiles -# If you specify a filename, the profiles will be read from that file and normalized. -# Otherwise, the normalized profiles will be parabolic of the form -# f(r) = (1 - offset)*(1 - r**alpha1)**alpha2 + offset -# j profile comes from the equilibrium, the others come from the profs file. +#---------------------- + gfilename: "" profsfilename: "" +# f(rho) = (1 - offset) * (1 - rho^alpha1)^alpha2 + offset j_alpha1: 2. j_alpha2: 3. j_offset: 0. @@ -80,18 +75,36 @@ Ti_offset: 0.005 Te_alpha1: 2. Te_alpha2: 1.5 Te_offset: 0.005 + #---------------------- +# Algorithm settings +#---------------------- + +Nn: 20 # number of density points +NTi: 20 # number of temperature points +# n_G = Ip / (pi a^2) = 3.09e20 m^-3 +nmax_frac: 1.2 +nmin_frac: 0.3 + +Tmax_keV: 15. +Tmin_keV: 3 + +resistivity_model: "Jardin" # Jardin, Paz-Soldan, or maximum + +verbosity: 1 +parallel: False #---------------------- -# Algorithm settings +# Scan (used by POPCON_scan; ignored by a plain POPCON) #---------------------- -Nn: 20 # resolution of densities -NTi: 20 # resolution of ion temperatures -nmax_frac: 1.2 # maximum density as a fraction of the Greenwald density -nmin_frac: 0.3 # minimum density as a fraction of the Greenwald density -Tmax_keV: 15. # maximum volume-averaged temperature in keV -Tmin_keV: 3 # minimum volume-averaged temperature in keV -resistivity_model: "Jardin" # Jardin, Paz-Soldan or maximum (suggested: Jardin) -verbosity: 1 # 0 for no output, 1 for minimal output, 2 for verbose output -parallel: False \ No newline at end of file + +# how much seeding the machine tolerates, against how good the L-mode +# confinement has to be +scan: + rows: + parameter: Zeff_target + values: [1.8, 2.1, 2.4] + cols: + parameter: H_fac + values: [1.1, 1.3, 1.5] diff --git a/openpopcon/resources/examples/radiative_ARC/plotsettings.yml b/openpopcon/resources/examples/radiative_ARC/plotsettings.yml index fe58c5a..384d42f 100644 --- a/openpopcon/resources/examples/radiative_ARC/plotsettings.yml +++ b/openpopcon/resources/examples/radiative_ARC/plotsettings.yml @@ -1,305 +1,15 @@ #----------------------------------------------------------------------- -# OpenPOPCON Plotting Settings +# OpenPOPCON Plotting Settings: radiative ARC +# +# Only the entries that differ from resources/default_plotsettings.yml are +# listed; anything left out falls back to the default. +# +# The contours are the defaults; only the axes change, to on-axis values. #----------------------------------------------------------------------- -yax: "n20_ax" # y-axis variable; nG for greenwald fraction, n20_ax for density -xax: "T_i_ax" # x-axis variable; T_i_av for average ion temperature, - # T_i_ax for maximum ion temperature, - # T_e_av for average electron temperature, - # T_e_ax for maximum electron temperature -figsize: [8, 6] # figure size in inches - -#---------------------- -# Options per output -#---------------------- - -plotoptions: - Paux: - plot: True # do not plot this output - color: r # line color - linewidth: 3 # line width - label: $P_{aux}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.1,1,10,50,100,500,2000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pfusion: - plot: True # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{fus}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [100,200,500,1000,2000,4000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pfusionheating: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{fus}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [100,200,500,1000,2000,4000] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Psol: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{SOL}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [10,20,50,80,100] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pohmic: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{OH}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.01,0.1,0.2,0.5,1.] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Prad: - plot: True # do not plot this output - color: purple # line color - linewidth: 2 # line width - label: $P_{rad}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2d' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [10,50,100,200] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 3 # number of contour levels - Pheat: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{heat}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Ploss: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{loss}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Palpha: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{\alpha}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pdd: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{DD}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Pdt: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $P_{DT}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - tauE: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\tau_E$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - Q: - plot: True # do not plot this output - color: orange # line color - linewidth: 2 # line width - label: $Q$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [0.5,1,2,5,10,25,50,100] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - min : -1 # only used if scale is specified - max : 2 # only used if scale is specified - levels: 4 # number of contour levels - H89: - plot: False # do not plot this output - color: blue # line color - linewidth: 2 # line width - label: $H_{89}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - H98: - plot: True # do not plot this output - color: blue # line color - linewidth: 2 # line width - label: $H_{98y2}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.1f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 5 # number of contour levels - vloop: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $V_{loop}$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: manual # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - betaN: - plot: True # do not plot this output - color: forestgreen # line color - linewidth: 2 # line width - label: $\beta_N$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.d' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [1,2,3] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 5 # number of contour levels - n_G_frac: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $n_G$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - n20_max: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $n_{i20}(0)$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - n20_avg: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\langle n_{i20}\rangle$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - T_i_max: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $T_i(0)$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - T_i_avg: - plot: False # do not plot this output - color: k # line color - linewidth: 2 # line width - label: $\langle T_i \rangle$ # label for the line - fontsize: 12 # font size for labels - fmt: '%.2f' # format for the on-contour labels - spacing: lin # lin, log, or manual spacing for the contour levels - manuallevels: [] # only used if manual spacing - scale: minmax # minmax or specified; below not used if manual spacing - # min : 0.0 # only used if scale is specified - # max : 1.0 # only used if scale is specified - levels: 10 # number of contour levels - - +yax: "n20_ax" # nG, n20_av or n20_ax +xax: "T_i_ax" # T_i_av, T_i_ax, T_e_av or T_e_ax; either axis takes either family +figsize: [8,6] +fill_invalid: True +plotoptions: {} diff --git a/openpopcon/resources/examples/radiative_ARC/radiative_ARC_ex.ipynb b/openpopcon/resources/examples/radiative_ARC/radiative_ARC_ex.ipynb new file mode 100644 index 0000000..83c95a5 --- /dev/null +++ b/openpopcon/resources/examples/radiative_ARC/radiative_ARC_ex.ipynb @@ -0,0 +1,226 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# OpenPOPCON radiative ARC example\n", + "\n", + "An ARC-class, high-field D-T reactor run in a radiative L-mode: 4.2 m,\n", + "11.5 T, 14 MA, with krypton seeded to Zeff = 2.1 and the `H89` L-mode\n", + "confinement scaling at H = 1.3.\n", + "\n", + "This example uses parabolic profiles rather than a gEQDSK, which keeps `R`,\n", + "`a`, `kappa`, `delta` and `I_P` free for `POPCON_scan` to vary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import openpopcon as op" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup and run\n", + "\n", + "The numerics are compiled with numba the first time they run, so the first\n", + "solve in a fresh kernel takes a few seconds longer than the rest." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "settingsfile = \"./POPCON_input_example.yaml\"\n", + "plotsettingsfile = \"./plotsettings.yml\"\n", + "\n", + "pc = op.POPCON(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)\n", + "pc.run_POPCON()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting\n", + "\n", + "The contours are the defaults; this example's plotsettings only move the\n", + "axes to on-axis density and temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = pc.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### A point on the plot\n", + "\n", + "The numbers behind one grid point, picked by Greenwald fraction and\n", + "volume-averaged ion temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "i = int(np.abs(pc.output.n_G_frac.values - 0.8).argmin())\n", + "j = int(np.abs(pc.output.T_i_avg.values - 8.0).argmin())\n", + "point = pc.output.isel(n_index=i, T_index=j)\n", + "\n", + "print(f\"n/n_G = {float(point.n_G_frac):.2f}\")\n", + "print(f\" = {float(point.T_i_avg):.1f} keV\")\n", + "print(f\"P_fus = {float(point.Pfusion):.1f} MW\")\n", + "print(f\"P_aux = {float(point.Paux):.1f} MW\")\n", + "print(f\"Q = {float(point.Q):.2f}\")\n", + "print(f\"beta_N = {float(point.betaN):.2f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scoping a single operating point\n", + "\n", + "`single_point` solves one density/temperature pair and shows the profiles\n", + "behind it, which is the quickest way to see why a point on the POPCON sits\n", + "where it does." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pc.single_point(n_G_frac=0.8, Ti_av=8.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scanning the seeding against the confinement\n", + "\n", + "`POPCON_scan` runs a full POPCON at every combination of two machine\n", + "parameters. The ranges come from the `scan:` block at the bottom of the\n", + "settings file: `Zeff_target` on the rows, which re-derives the krypton\n", + "fraction in every cell, and `H_fac` on the columns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc = op.POPCON_scan(settingsfile=settingsfile, plotsettingsfile=plotsettingsfile)\n", + "sc.run_scan()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, axs = sc.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`plot_metric` reduces each cell to one number so the trend across the scan\n", + "reads at a glance. Anything in the output works, with any reduction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = sc.plot_metric(\"Q\", reduce=\"max\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The whole scan is also available as one xarray Dataset, with the two scanned\n", + "parameters as extra dimensions, for any analysis the plots do not cover." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc.output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Saving and reloading a scan\n", + "\n", + "`write_output` saves the whole scan: the base settings, the scan\n", + "specification, the combined arrays and the grid plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sc.write_output(name=\"radiative_arc_scan\", archive=False, overwrite=True)\n", + "\n", + "back = op.POPCON_scan.read_output(\"radiative_arc_scan\")\n", + "print(back.shape, back.row.parameter, back.col.parameter)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openPOPCON", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/helpers.py b/tests/helpers.py index fa639a1..77aa8c2 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -48,12 +48,14 @@ def write_settings(tmp_path, base=SETTINGS, name="settings.yaml", **overrides): """ A settings file built from an example with some keys replaced, detached - from any gEQDSK/profile files so it is self-contained. + from any gEQDSK/profile files so it is self-contained, and from the + example's scan block so a test's scan is the only one in play. """ with open(base) as fh: data = yaml.safe_load(fh) data["gfilename"] = "" data["profsfilename"] = "" + data.pop("scan", None) data.update(overrides) path = tmp_path / name with open(path, "w") as fh: @@ -71,6 +73,9 @@ def solve_small_manta(parallel=False): pc.settings.NTi = SMALL_GRID["NTi"] pc.settings.nr = SMALL_GRID["nr"] pc.settings.parallel = parallel + # the golden values pin the plain gEQDSK path, where the equilibrium's own + # R, a and current are used + pc.settings.gfile_rescale = False pc.settings.verbosity = 0 pc.run_POPCON() return pc diff --git a/tests/test_scan.py b/tests/test_scan.py index 1a9df04..3e54c34 100644 --- a/tests/test_scan.py +++ b/tests/test_scan.py @@ -5,13 +5,15 @@ identical cells, because it looks exactly like a scan that worked. """ +import os + import matplotlib.pyplot as plt import numpy as np import pytest import yaml import openpopcon as op -from openpopcon.core import DIM_N, DIM_T +from openpopcon.core import DIM_N, DIM_T, _gfile_geometry from helpers import ( PLOTSETTINGS, @@ -191,7 +193,7 @@ def test_python_scan_overrides_the_yaml_block(tmp_path): def test_shipped_examples_with_scan_blocks_still_load_as_plain_popcons(): # 'scan' is in KNOWN_SETTINGS_KEYS, so it must not be reported as a typo - for name in ("CENTAUR", "ITER"): + for name in ("CENTAUR", "ITER", "MANTA", "SPARC", "radiative_ARC"): d = op.example_dir(name) pc = op.POPCON( settingsfile=f"{d}/POPCON_input_example.yaml", @@ -201,11 +203,27 @@ def test_shipped_examples_with_scan_blocks_still_load_as_plain_popcons(): BAD_SCANS = { + "unknown_hold": {"rows": {"parameter": "R", "scale": [0.9, 1.1], "hold": "V"}}, + "hold_I_P_and_qstar": { + "rows": {"parameter": "R", "scale": [0.9, 1.1], "hold": ["I_P", "qstar"]} + }, + "aspect_ratio_off_R": { + "rows": {"parameter": "B_0", "scale": [0.9, 1.1], "hold": "aspect_ratio"} + }, + "hold_what_is_scanned": { + "rows": {"parameter": "I_P", "values": ROWS, "hold": "I_P"} + }, + "hold_clashes_with_other_axis": { + "rows": {"parameter": "R", "scale": [0.9, 1.1], "hold": "aspect_ratio"}, + "cols": ("a", [0.5, 0.6]), + }, + "R_and_R_0_on_both_axes": {"rows": ("R", [1.6, 2.0]), "cols": ("R_0", [1.7, 1.9])}, + "scale_without_a_number": {"rows": {"parameter": "scalinglaw", "scale": [1, 2]}}, + "values_and_scale": {"rows": {"parameter": "R", "values": [1.8], "scale": [1]}}, "unknown_parameter": {"rows": ("Bfield", ROWS), "cols": ("B_0", COLS)}, "unscannable_Nn": {"rows": ("Nn", [8, 16]), "cols": ("B_0", COLS)}, "unscannable_gfilename": {"rows": ("gfilename", ["a", "b"]), "cols": ("B_0", COLS)}, "same_parameter": {"rows": ("B_0", [9.0, 11.0]), "cols": ("B_0", COLS)}, - "only_one_axis": {"rows": ("I_P", ROWS)}, "empty_values": {"rows": ("I_P", []), "cols": ("B_0", COLS)}, } @@ -227,18 +245,112 @@ def test_no_scan_at_all_is_an_error(tmp_path): ) -@pytest.mark.parametrize("parameter", ["R", "a", "kappa", "delta", "I_P", "qstar"]) -def test_geqdsk_geometry_scan_rejected(parameter): - # MANTA has a gfilename, and __get_geometry takes the geometry and the - # ohmic current from the equilibrium regardless of the settings +def _manta_settings(tmp_path, **overrides): + # MANTA keeps its gEQDSK here, unlike write_settings + with open(SETTINGS) as fh: + data = yaml.safe_load(fh) + data.pop("scan", None) + data.update(verbosity=0, parallel=False, **SMALL_GRID, **overrides) + for key in ("gfilename", "profsfilename"): + data[key] = os.path.join(os.path.dirname(SETTINGS), data[key]) + path = tmp_path / "manta.yaml" + with open(path, "w") as fh: + yaml.safe_dump(data, fh) + return str(path) + + +@pytest.mark.parametrize( + "parameter", ["R", "R_0", "a", "kappa", "delta", "I_P", "qstar"] +) +def test_geqdsk_geometry_scan_rejected(tmp_path, parameter): + # without gfile_rescale, __get_geometry takes the geometry and the ohmic + # current from the equilibrium regardless of the settings with pytest.raises(ValueError, match="gfilename"): op.POPCON_scan( - settingsfile=SETTINGS, + settingsfile=_manta_settings(tmp_path, gfile_rescale=False), plotsettingsfile=PLOTSETTINGS, scan={"rows": (parameter, [1.0, 2.0]), "cols": ("B_0", COLS)}, ) +def test_geqdsk_rejection_points_at_gfile_rescale(tmp_path): + with pytest.raises(ValueError, match="gfile_rescale"): + op.POPCON_scan( + settingsfile=_manta_settings(tmp_path, gfile_rescale=False), + plotsettingsfile=PLOTSETTINGS, + scan={"rows": ("R", [4.0, 5.0]), "cols": ("B_0", COLS)}, + ) + + +@pytest.mark.parametrize("parameter", ["kappa", "delta"]) +def test_gfile_rescale_keeps_the_shape_locked(tmp_path, parameter): + with pytest.raises(ValueError, match="gfilename"): + op.POPCON_scan( + settingsfile=_manta_settings(tmp_path, gfile_rescale=True), + plotsettingsfile=PLOTSETTINGS, + scan={"rows": (parameter, [1.0, 2.0]), "cols": ("B_0", COLS)}, + ) + + +@pytest.mark.parametrize("parameter", ["R", "R_0"]) +def test_gfile_rescale_scans_major_radius(tmp_path, parameter): + sc = op.POPCON_scan( + settingsfile=_manta_settings(tmp_path, gfile_rescale=True), + plotsettingsfile=PLOTSETTINGS, + scan={"rows": (parameter, [4.0, 5.0])}, + ) + sc.run_scan(progress=False) + small, big = sc.cells[0][0], sc.cells[1][0] + # R is kept rather than reset to the equilibrium's 4.55 m + assert small.settings.R == 4.0 and big.settings.R == 5.0 + # the cross-section keeps its shape, so volume goes as R to within the + # small offset between the flux surface centroid and the geometric centre + V = [c.algorithms.volgrid[-1] for c in (small, big)] + assert V[1] / V[0] == pytest.approx(5.0 / 4.0, rel=0.02) + assert not np.allclose(small.output.Pfusion, big.output.Pfusion) + + +def test_gfile_rescale_resizes_and_recurrents(tmp_path): + sc = op.POPCON_scan( + settingsfile=_manta_settings(tmp_path, gfile_rescale=True), + plotsettingsfile=PLOTSETTINGS, + scan={"rows": ("a", [1.0, 1.2]), "cols": ("I_P", [8.0, 12.0])}, + ) + sc.run_scan(progress=False) + cells = sc.cells + # volume goes as a^2 at fixed R, to within the centroid offset + V = [cells[i][0].algorithms.volgrid[-1] for i in range(2)] + assert V[1] / V[0] == pytest.approx(1.44, rel=0.02) + # and the ohmic current is the settings current, not the equilibrium's + assert [cells[0][j].algorithms.Itot for j in range(2)] == [8.0, 12.0] + assert not np.allclose(cells[0][0].output.Pohmic, cells[0][1].output.Pohmic) + + +def test_gfile_rescale_at_the_equilibrium_is_the_identity(tmp_path): + # given the equilibrium's own R, a, Ip and field, the rescale must change + # nothing at all + geo = _gfile_geometry( + os.path.join(os.path.dirname(SETTINGS), "gMANTA"), SMALL_GRID["nr"] + ) + names = {"R": "geq_R", "a": "geq_a", "I_P": "Ipint", "B_0": "geq_B0"} + own = {key: float(geo[name]) for key, name in names.items()} + plain = op.POPCON( + settingsfile=_manta_settings(tmp_path, gfile_rescale=False, **own), + plotsettingsfile=PLOTSETTINGS, + ) + plain.run_POPCON() + (tmp_path / "rescaled").mkdir() + rescaled = op.POPCON( + settingsfile=_manta_settings(tmp_path / "rescaled", gfile_rescale=True, **own), + plotsettingsfile=PLOTSETTINGS, + ) + rescaled.run_POPCON() + for field in ("Pfusion", "Paux", "Pohmic", "Q"): + np.testing.assert_allclose( + rescaled.output[field], plain.output[field], rtol=1e-9, err_msg=field + ) + + @pytest.mark.parametrize("parameter", ["B_0", "H_fac"]) def test_geqdsk_safe_parameters_accepted(parameter): op.POPCON_scan( @@ -289,6 +401,31 @@ def test_scanning_R_redrives_Ip_from_qstar(tmp_path): assert sc.cells[0][0].settings.Ip != pytest.approx(sc.cells[1][0].settings.Ip) +@pytest.mark.parametrize("given", ["R", "R_0"]) +def test_scanning_R_0_overrides_either_spelling(tmp_path, given): + # R_0 is an alias for R. Scanning it on a file that spells the major + # radius either way has to change it, not trip over both being present + path = _scan_settings(tmp_path) + with open(path) as fh: + raw = yaml.safe_load(fh) + raw[given] = raw.pop("R") + with open(path, "w") as fh: + yaml.safe_dump(raw, fh) + + sc = op.POPCON_scan( + settingsfile=path, + plotsettingsfile=SPARC_PLOTSETTINGS, + scan={"rows": ("R_0", [1.6, 2.2]), "cols": ("B_0", COLS)}, + ) + assert [sc.cells[i][0].settings.R for i in range(2)] == [1.6, 2.2] + + +def test_R_and_R_0_together_rejected(tmp_path): + path = _scan_settings(tmp_path, R_0=1.85) + with pytest.raises(ValueError, match="R_0"): + op.POPCON(settingsfile=path, plotsettingsfile=SPARC_PLOTSETTINGS) + + # ------------------------------------------------------------------- # Plotting and I/O # ------------------------------------------------------------------- diff --git a/tests/test_sensitivity.py b/tests/test_sensitivity.py new file mode 100644 index 0000000..72a9b0c --- /dev/null +++ b/tests/test_sensitivity.py @@ -0,0 +1,146 @@ +""" +One-parameter sensitivity scans on every shipped example: each machine +parameter varied by +-25% on its own, and the major radius varied under the +three usual constraints. These are what a first POPCON study asks for, so +every example set up for it has to support all of them, gEQDSK ones +included. +""" + +import os + +import numpy as np +import pytest +import yaml + +import openpopcon as op +from openpopcon.core import _qstar + +from helpers import SMALL_GRID + +SCALE = [0.75, 1.0, 1.25] + +SENSITIVITIES = { + "B_0": {"parameter": "B_0", "scale": SCALE}, + "H_fac": {"parameter": "H_fac", "scale": SCALE}, + "I_P": {"parameter": "I_P", "scale": SCALE}, + "Zeff_target": {"parameter": "Zeff_target", "scale": SCALE}, + "R_fixed_aspect_and_current": { + "parameter": "R", + "scale": SCALE, + "hold": ["aspect_ratio", "I_P"], + }, + "R_fixed_aspect_and_qstar": { + "parameter": "R", + "scale": SCALE, + "hold": ["aspect_ratio", "qstar"], + }, + "R_fixed_minor_radius_and_qstar": { + "parameter": "R", + "scale": SCALE, + "hold": ["a", "qstar"], + }, +} + + +def _example_settings(tmp_path, example): + """ + An example's settings on the small grid, with its gEQDSK and profiles + files made absolute so the copy still finds them. + """ + d = op.example_dir(example) + [name] = [f for f in os.listdir(d) if f.endswith(".yaml")] + with open(os.path.join(d, name)) as fh: + data = yaml.safe_load(fh) + data.pop("scan", None) + data.update(verbosity=0, parallel=False, **SMALL_GRID) + for key in ("gfilename", "profsfilename"): + if data.get(key): + data[key] = os.path.join(d, data[key]) + path = tmp_path / "settings.yaml" + with open(path, "w") as fh: + yaml.safe_dump(data, fh) + return str(path), os.path.join(d, "plotsettings.yml") + + +# the NSF examples keep their original settings, which have no Zeff_target +# and use the equilibrium's own geometry, so these scans do not apply to them +SENSITIVITY_EXAMPLES = [e for e in op.list_examples() if not e.startswith("NSF")] + + +@pytest.mark.parametrize("sensitivity", list(SENSITIVITIES)) +@pytest.mark.parametrize("example", SENSITIVITY_EXAMPLES) +def test_sensitivity_scan_runs_and_changes_something(tmp_path, example, sensitivity): + settings, plotsettings = _example_settings(tmp_path, example) + sc = op.POPCON_scan( + settingsfile=settings, + plotsettingsfile=plotsettings, + scan={"rows": SENSITIVITIES[sensitivity]}, + ) + assert sc.shape == (3, 1) + sc.run_scan(progress=False) + + cells = [sc.cells[i][0] for i in range(3)] + # the failure worth guarding: a scan whose cells silently came out the + # same. P_aux is the field every one of these moves; P_fusion at a given + # grid point does not depend on the field or the confinement at all + arrays = [np.nan_to_num(c.output.Paux.values) for c in cells] + assert not np.allclose(arrays[0], arrays[2]) + + base = sc.base_settings + st = [c.settings for c in cells] + hold = SENSITIVITIES[sensitivity].get("hold", []) + if SENSITIVITIES[sensitivity]["parameter"] == "R": + np.testing.assert_allclose([s.R for s in st], np.multiply(SCALE, base.R)) + if "aspect_ratio" in hold: + np.testing.assert_allclose([s.R / s.a for s in st], base.R / base.a) + if "a" in hold: + np.testing.assert_allclose([s.a for s in st], base.a) + if "I_P" in hold: + np.testing.assert_allclose([s.Ip for s in st], base.Ip) + if "qstar" in hold: + np.testing.assert_allclose([_qstar(s) for s in st], _qstar(base)) + + table = sc.operating_point(T_i_avg=cells[1].output.T_i_avg.values[3], n_G_frac=0.5) + assert len(table) == 3 + + +def test_scale_axis_is_relative_to_the_base(tmp_path): + settings, plotsettings = _example_settings(tmp_path, "SPARC") + sc = op.POPCON_scan( + settingsfile=settings, + plotsettingsfile=plotsettings, + scan={"cols": {"parameter": "B_0", "scale": SCALE}}, + ) + assert sc.shape == (1, 3) + np.testing.assert_allclose(sc.col.values, np.multiply(SCALE, 12.2)) + + +def test_zeff_scan_without_impurity_rejected(tmp_path): + settings, plotsettings = _example_settings(tmp_path, "SPARC") + with open(settings) as fh: + data = yaml.safe_load(fh) + del data["impurity"] + with open(settings, "w") as fh: + yaml.safe_dump(data, fh) + with pytest.raises(ValueError, match="impurity"): + op.POPCON_scan( + settingsfile=settings, + plotsettingsfile=plotsettings, + scan={"rows": {"parameter": "Zeff_target", "scale": SCALE}}, + ) + + +def test_operating_point_by_density(tmp_path): + settings, plotsettings = _example_settings(tmp_path, "SPARC") + sc = op.POPCON_scan( + settingsfile=settings, + plotsettingsfile=plotsettings, + scan={"rows": {"parameter": "I_P", "scale": SCALE}}, + ) + sc.run_scan(progress=False) + table = sc.operating_point(T_i_avg=8.0, n_e_20_avg=3.0) + np.testing.assert_allclose(table["I_P"], np.multiply(SCALE, 8.7)) + # a fixed density is a different Greenwald fraction at each current + assert table["n_G_frac"].is_monotonic_decreasing + with pytest.raises(ValueError, match="exactly one"): + sc.operating_point(T_i_avg=8.0)