diff --git a/examples/Basic_Simulation/cb.in b/examples/Basic_Simulation/cb.in index 29c8a7fca..b2cb85c35 100644 --- a/examples/Basic_Simulation/cb.in +++ b/examples/Basic_Simulation/cb.in @@ -1,12 +1,3 @@ -!! Copyright 2022 - David Minton, Carlisle Wishard, Jennifer Pouplin, Jake Elliott, & Dana Singh -!! This file is part of Swiftest. -!! Swiftest is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License -!! as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -!! Swiftest is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -!! of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -!! You should have received a copy of the GNU General Public License along with Swiftest. -!! If not, see: https://www.gnu.org/licenses. - Sun 39.476926408897626 0.004650467260962157 diff --git a/examples/Basic_Simulation/initial_conditions.py b/examples/Basic_Simulation/initial_conditions.py index 640330f1f..78900ce1f 100644 --- a/examples/Basic_Simulation/initial_conditions.py +++ b/examples/Basic_Simulation/initial_conditions.py @@ -15,14 +15,7 @@ Returns ------- -param.in : ASCII text file - Swiftest parameter input file. -pl.in : ASCII text file - Swiftest massive body input file. -tp.in : ASCII text file - Swiftest test particle input file. -cb.in : ASCII text file - Swiftest central body input file. +Updates sim.data with the simulation data """ import swiftest @@ -30,88 +23,39 @@ from numpy.random import default_rng # Initialize the simulation object as a variable -sim = swiftest.Simulation() - -# Add parameter attributes to the simulation object -sim.param['T0'] = 0.0 -sim.param['TSTOP'] = 10 -sim.param['DT'] = 0.005 -sim.param['ISTEP_OUT'] = 200 -sim.param['ISTEP_DUMP'] = 200 -sim.param['OUT_FORM'] = 'XVEL' -sim.param['OUT_TYPE'] = 'NETCDF_DOUBLE' -sim.param['OUT_STAT'] = 'REPLACE' -sim.param['IN_FORM'] = 'EL' -sim.param['IN_TYPE'] = 'ASCII' -sim.param['PL_IN'] = 'pl.in' -sim.param['TP_IN'] = 'tp.in' -sim.param['CB_IN'] = 'cb.in' -sim.param['BIN_OUT'] = 'out.nc' -sim.param['CHK_QMIN'] = swiftest.RSun / swiftest.AU2M -sim.param['CHK_RMIN'] = swiftest.RSun / swiftest.AU2M -sim.param['CHK_RMAX'] = 1000.0 -sim.param['CHK_EJECT'] = 1000.0 -sim.param['CHK_QMIN_COORD'] = 'HELIO' -sim.param['CHK_QMIN_RANGE'] = f'{swiftest.RSun / swiftest.AU2M} 1000.0' -sim.param['MU2KG'] = swiftest.MSun -sim.param['TU2S'] = swiftest.YR2S -sim.param['DU2M'] = swiftest.AU2M -sim.param['EXTRA_FORCE'] = 'NO' -sim.param['BIG_DISCARD'] = 'NO' -sim.param['CHK_CLOSE'] = 'YES' -sim.param['GR'] = 'YES' -sim.param['INTERACTION_LOOPS'] = 'ADAPTIVE' -sim.param['ENCOUNTER_CHECK'] = 'ADAPTIVE' -sim.param['RHILL_PRESENT'] = 'YES' -sim.param['FRAGMENTATION'] = 'YES' -sim.param['ROTATION'] = 'YES' -sim.param['ENERGY'] = 'YES' -sim.param['GMTINY'] = 1e-6 -sim.param['MIN_GMFRAG'] = 1e-9 - -# Set gravitational units of the system -GU = swiftest.GC / (sim.param['DU2M'] ** 3 / (sim.param['MU2KG'] * sim.param['TU2S'] ** 2)) +sim = swiftest.Simulation(tstart=0.0, tstop=10.0, dt=0.005, tstep_out=1.0, fragmentation=True, minimum_fragment_mass = 2.5e-11, mtiny=2.5e-8) +sim.get_parameter() # Add the modern planets and the Sun using the JPL Horizons Database -sim.add("Sun", idval=0, date="2022-08-08") -sim.add("Mercury", idval=1, date="2022-08-08") -sim.add("Venus", idval=2, date="2022-08-08") -sim.add("Earth", idval=3, date="2022-08-08") -sim.add("Mars", idval=4, date="2022-08-08") -sim.add("Jupiter", idval=5, date="2022-08-08") -sim.add("Saturn", idval=6, date="2022-08-08") -sim.add("Uranus", idval=7, date="2022-08-08") -sim.add("Neptune", idval=8, date="2022-08-08") +sim.add_solar_system_body(["Sun","Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"]) # Add 5 user-defined massive bodies npl = 5 density_pl = 3000.0 / (sim.param['MU2KG'] / sim.param['DU2M'] ** 3) -id_pl = np.array([9, 10, 11, 12, 13]) -name_pl = np.array(["MassiveBody_01", "MassiveBody_02", "MassiveBody_03", "MassiveBody_04", "MassiveBody_05"]) +name_pl = ["MassiveBody_01", "MassiveBody_02", "MassiveBody_03", "MassiveBody_04", "MassiveBody_05"] a_pl = default_rng().uniform(0.3, 1.5, npl) e_pl = default_rng().uniform(0.0, 0.3, npl) inc_pl = default_rng().uniform(0.0, 90, npl) capom_pl = default_rng().uniform(0.0, 360.0, npl) omega_pl = default_rng().uniform(0.0, 360.0, npl) capm_pl = default_rng().uniform(0.0, 360.0, npl) -GM_pl = (np.array([6e23, 8e23, 1e24, 3e24, 5e24]) / sim.param['MU2KG']) * GU -R_pl = np.full(npl, (3 * (GM_pl / GU) / (4 * np.pi * density_pl)) ** (1.0 / 3.0)) -Rh_pl = a_pl * ((GM_pl) / (3 * GU)) ** (1.0 / 3.0) -Ip1_pl = np.array([0.4, 0.4, 0.4, 0.4, 0.4]) -Ip2_pl = np.array([0.4, 0.4, 0.4, 0.4, 0.4]) -Ip3_pl = np.array([0.4, 0.4, 0.4, 0.4, 0.4]) -rotx_pl = np.array([0.0, 0.0, 0.0, 0.0, 0.0]) -roty_pl = np.array([0.0, 0.0, 0.0, 0.0, 0.0]) -rotz_pl = np.array([0.0, 0.0, 0.0, 0.0, 0.0]) +GM_pl = (np.array([6e23, 8e23, 1e24, 3e24, 5e24]) / sim.param['MU2KG']) * sim.GU +R_pl = np.full(npl, (3 * (GM_pl / sim.GU) / (4 * np.pi * density_pl)) ** (1.0 / 3.0)) +Rh_pl = a_pl * ((GM_pl) / (3 * sim.GU)) ** (1.0 / 3.0) +Ip1_pl = [0.4, 0.4, 0.4, 0.4, 0.4] +Ip2_pl = [0.4, 0.4, 0.4, 0.4, 0.4] +Ip3_pl = [0.4, 0.4, 0.4, 0.4, 0.4] +rotx_pl = [0.0, 0.0, 0.0, 0.0, 0.0] +roty_pl = [0.0, 0.0, 0.0, 0.0, 0.0] +rotz_pl = [0.0, 0.0, 0.0, 0.0, 0.0] -sim.addp(id_pl, name_pl, a_pl, e_pl, inc_pl, capom_pl, omega_pl, capm_pl, GMpl=GM_pl, Rpl=R_pl, rhill=Rh_pl, Ip1=Ip1_pl, Ip2=Ip2_pl, Ip3=Ip3_pl, rotx=rotx_pl, roty=roty_pl, rotz=rotz_pl) +sim.add_body(name_pl, a_pl, e_pl, inc_pl, capom_pl, omega_pl, capm_pl, GMpl=GM_pl, Rpl=R_pl, rhill=Rh_pl, Ip1=Ip1_pl, Ip2=Ip2_pl, Ip3=Ip3_pl, rotx=rotx_pl, roty=roty_pl, rotz=rotz_pl) # Add 10 user-defined test particles ntp = 10 -id_tp = np.array([14, 15, 16, 17, 18, 19, 20, 21, 22, 23]) -name_tp = np.array(["TestParticle_01", "TestParticle_02", "TestParticle_03", "TestParticle_04", "TestParticle_05", "TestParticle_06", "TestParticle_07", "TestParticle_08", "TestParticle_09", "TestParticle_10"]) +name_tp = ["TestParticle_01", "TestParticle_02", "TestParticle_03", "TestParticle_04", "TestParticle_05", "TestParticle_06", "TestParticle_07", "TestParticle_08", "TestParticle_09", "TestParticle_10"] a_tp = default_rng().uniform(0.3, 1.5, ntp) e_tp = default_rng().uniform(0.0, 0.3, ntp) inc_tp = default_rng().uniform(0.0, 90, ntp) @@ -119,7 +63,7 @@ omega_tp = default_rng().uniform(0.0, 360.0, ntp) capm_tp = default_rng().uniform(0.0, 360.0, ntp) -sim.addp(id_tp, name_tp, a_tp, e_tp, inc_tp, capom_tp, omega_tp, capm_tp) +sim.add_body(name_tp, a_tp, e_tp, inc_tp, capom_tp, omega_tp, capm_tp) -# Save everything to a set of initial conditions files -sim.save('param.in') +# Run the simulation +sim.run() diff --git a/examples/Basic_Simulation/param.in b/examples/Basic_Simulation/param.in index bec3573de..0ee870562 100644 --- a/examples/Basic_Simulation/param.in +++ b/examples/Basic_Simulation/param.in @@ -1,46 +1,37 @@ -!! Copyright 2022 - David Minton, Carlisle Wishard, Jennifer Pouplin, Jake Elliott, & Dana Singh -!! This file is part of Swiftest. -!! Swiftest is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License -!! as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -!! Swiftest is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -!! of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -!! You should have received a copy of the GNU General Public License along with Swiftest. -!! If not, see: https://www.gnu.org/licenses. - -! VERSION Swiftest parameter input +! VERSION Swiftest input file T0 0.0 -TSTOP 10 +TSTART 0.0 +TSTOP 10.0 DT 0.005 ISTEP_OUT 200 ISTEP_DUMP 200 +NC_IN init_cond.nc +IN_TYPE NETCDF_DOUBLE +IN_FORM EL +BIN_OUT bin.nc OUT_FORM XVEL OUT_TYPE NETCDF_DOUBLE OUT_STAT REPLACE -IN_TYPE ASCII -PL_IN pl.in -TP_IN tp.in -CB_IN cb.in -BIN_OUT out.nc CHK_QMIN 0.004650467260962157 CHK_RMIN 0.004650467260962157 -CHK_RMAX 1000.0 -CHK_EJECT 1000.0 +CHK_RMAX 10000.0 +CHK_EJECT 10000.0 CHK_QMIN_COORD HELIO -CHK_QMIN_RANGE 0.004650467260962157 1000.0 +CHK_QMIN_RANGE 0.004650467260962157 10000.0 MU2KG 1.988409870698051e+30 TU2S 31557600.0 DU2M 149597870700.0 -IN_FORM EL -EXTRA_FORCE NO -BIG_DISCARD NO +GMTINY 9.869231602224408e-07 +RESTART NO CHK_CLOSE YES -RHILL_PRESENT YES +GR YES FRAGMENTATION YES ROTATION YES +ENERGY NO +EXTRA_FORCE NO +BIG_DISCARD NO +RHILL_PRESENT NO +INTERACTION_LOOPS TRIANGULAR +ENCOUNTER_CHECK TRIANGULAR TIDES NO -ENERGY YES -GR YES -INTERACTION_LOOPS ADAPTIVE -ENCOUNTER_CHECK ADAPTIVE -GMTINY 1e-06 -MIN_GMFRAG 1e-09 +MIN_GMFRAG 9.869231602224408e-10 diff --git a/examples/Basic_Simulation/pl.in b/examples/Basic_Simulation/pl.in index 3f18aca0e..215b91f1d 100644 --- a/examples/Basic_Simulation/pl.in +++ b/examples/Basic_Simulation/pl.in @@ -1,88 +1,85 @@ -!! Copyright 2022 - David Minton, Carlisle Wishard, Jennifer Pouplin, Jake Elliott, & Dana Singh -!! This file is part of Swiftest. -!! Swiftest is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License -!! as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -!! Swiftest is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -!! of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -!! You should have received a copy of the GNU General Public License along with Swiftest. -!! If not, see: https://www.gnu.org/licenses. - -13 -Mercury 6.553709809565314146e-06 0.0014751262621647182575 -1.6306381826061645943e-05 -0.38709864823618972407 0.20562513690973019398 7.0036250456070128223 -48.30204974520415817 29.18823342267911869 114.9720047697775982 -0.0 0.0 0.34599999999999997424 -3.5734889863322150192 -18.38008501876561206 34.361513668512199956 -Venus 9.6633133995815381836e-05 0.006759085242739840662 -4.0453784346544178454e-05 -0.72332603580811538624 0.0067486079191121668003 3.394406261149808035 -76.61738837179606776 54.777760273590551776 315.37095689555837907 -0.0 0.0 0.4000000000000000222 -0.17650282045605921225 -3.6612475825356215592 8.702866268072763821 -Earth 0.000120026935827952456416 0.010044668314295209318 -4.25875607065040958e-05 -0.9999943732711822353 0.016707309394717231171 0.0028984767436730000077 -174.05498211951208987 289.04709044403989537 213.07530468023790604 -0.0 0.0 0.33069999999999999396 -5.002093202481912218 0.055213529850334066125 2301.2110537292529557 -Mars 1.2739802010675941808e-05 0.007246950762048707243 -2.265740805092889601e-05 -1.5237812078483019551 0.0935087708803710449 1.8479353068000929916 -49.489305419773351957 286.70300191753761965 24.878418068365981242 -0.0 0.0 0.3644000000000000017 -997.9357048213454125 -909.4072592492943007 1783.4501726537997323 -Jupiter 0.03769225108898567778 0.3552222491747608486 -0.00046732617030490929307 -5.2028063728088866924 0.048395118271449058533 1.3035670146561249005 -100.516498130230701236 273.44233262595901124 346.26538105843917492 -0.0 0.0 0.27560000000000001164 --80.96619889339111482 -2388.0060524649362916 5008.7314931237953832 -Saturn 0.01128589982009127331 0.43757948578866074266 -0.00038925687730393611812 -9.580020069168169172 0.053193613750490406633 2.4864365613724639381 -113.597044717589099605 335.10179422401358806 237.66485199561481068 -0.0 0.0 0.22000000000000000111 -441.93538182505989814 378.5284220382117538 5135.9110455622733884 -Uranus 0.001723658947826773068 0.4705353566089944894 -0.00016953449859497231466 -19.272143108769419939 0.043779687288749750962 0.7707536154556786645 -74.077748995180698444 93.42271392662131291 242.37685081109759722 -0.0 0.0 0.23000000000000000999 --677.3000258209181323 -3008.109907190578637 -836.301326618569835 -Neptune 0.0020336100526728302882 0.78184929587893868845 -0.000164587904124493665 -30.305539399096510067 0.014544938874222059638 1.7686697746048700708 -131.73604731224671127 249.9779420269553043 332.54824537252648042 -0.0 0.0 0.23000000000000000999 -1231.1804455066093229 -2178.0887091151860042 2329.6411363603121418 -MassiveBody_01 1.1912109366578087428e-05 0.0016092923734511708263 -2.425055692051244981e-05 -0.3460404950890429432 0.2093906512182220625 0.11109012870384793459 -114.31328763688792094 347.82259114762894114 96.0534391561842682 -0.4000000000000000222 0.4000000000000000222 0.4000000000000000222 +14 +Mercury 6.553709809565314e-06 +1.6306381826061646e-05 +0.3870985843095394 0.2056234010897001 7.003302508001384 +48.29611837378607 29.20442403952454 338.3394874682879 +0.0 0.0 0.346 +3.573018077015318 -18.380317085416586 34.36143850429876 +Venus 9.663313399581537e-05 +4.0453784346544176e-05 +0.7233297579736101 0.006717605698865438 3.394439273342282 +76.60235891771119 54.96037946082961 200.4789339550648 +0.0 0.0 0.4 +0.17650282045605922 -3.6612475825356214 8.702866268072764 +Earth 0.00012002693582795245 +4.25875607065041e-05 +0.9999904874543223 0.01671400376545858 0.003637862608863003 +175.025172600231 287.9619628812575 114.3482934042427 +0.0 0.0 0.3307 +6.157239621449141 0.057224133705898524 2301.2082528350797 +Mars 1.2739802010675942e-05 +2.2657408050928896e-05 +1.523711925589535 0.09344151133508208 1.847441673557901 +49.4728572124747 286.7379771285891 209.3396773477138 +0.0 0.0 0.3644 +997.9224351226384 -909.5549030011778 1783.3823046046184 +Jupiter 0.037692251088985676 +0.0004673261703049093 +5.2027278008516 0.04824497711637968 1.303631134570075 +100.5192588433081 273.5898402882514 129.5536700659942 +0.0 0.0 0.2756 +-80.9864396731672 -2388.0246092955053 5008.722318533006 +Saturn 0.011285899820091273 +0.00038925687730393614 +9.532011952667288 0.05486329870433341 2.487906363280301 +113.6305781676206 339.5467356402391 290.8995806568904 +0.0 0.0 0.22 +441.95954822014636 378.52638822638795 5135.909115928892 +Uranus 0.001723658947826773 +0.00016953449859497232 +19.24498838290236 0.04796174942301296 0.7730102596086205 +74.0125809801658 93.59554912280227 262.8658637277515 +0.0 0.0 0.23 +-677.3000258209181 -3008.1099071905787 -836.3013266185699 +Neptune 0.0020336100526728304 +0.00016458790412449367 +30.03895991152209 0.008955570138096731 1.771119354296142 +131.8221159748827 284.4748429674216 308.4513720536233 +0.0 0.0 0.23 +1232.224106980634 -2177.3040821077648 2329.8227878119233 +Pluto 2.924216771029454e-07 +7.943294877391593e-06 +39.36791814672583 0.2487178537481577 17.1705505990969 +110.3314332962701 113.0826635900664 55.11416408345664 +0.0 0.0 0.4 +-243.59404988903637 261.28663002814227 -38.57352022187049 +MassiveBody_01 1.1912109366578089e-05 +2.425055692051245e-05 +0.7452368716298337 0.0633011418780484 0.11151363780595558 +203.01823417718037 284.9353898127118 266.79344592519305 +0.4 0.4 0.4 0.0 0.0 0.0 -MassiveBody_02 1.5882812488770779849e-05 0.0016802531895603555184 -2.6691191565570073646e-05 -0.32826188156947710972 0.27866488696288682636 77.21223337306255985 -251.99014895640269174 53.772702227560969845 165.6085387284213084 -0.4000000000000000222 0.4000000000000000222 0.4000000000000000222 +MassiveBody_02 1.5882812488770783e-05 +2.6691191565570074e-05 +0.7671203280602826 0.10149029388964753 53.46706656938751 +61.74738152068808 68.20565593722856 271.3352706902475 +0.4 0.4 0.4 0.0 0.0 0.0 -MassiveBody_03 1.9853515610963475658e-05 0.004324919007577881884 -2.8752214513575297366e-05 -0.7843689028022314824 0.06176128116947356833 78.74144231136708072 -286.63765100951468412 347.55933571120488068 266.36960496595537506 -0.4000000000000000222 0.4000000000000000222 0.4000000000000000222 +MassiveBody_03 1.985351561096348e-05 +2.8752214513575297e-05 +1.4698824276418418 0.13621250684495437 23.635498327264845 +38.071905339231236 283.134612455057 250.67457601578352 +0.4 0.4 0.4 0.0 0.0 0.0 -MassiveBody_04 5.9560546832890430362e-05 0.0056765613035874530265 -4.146786902759040254e-05 -0.7138176832994267418 0.28016098557400787028 22.725690778108500467 -203.41845532080247949 219.74297850728484605 14.730732982803269593 -0.4000000000000000222 0.4000000000000000222 0.4000000000000000222 +MassiveBody_04 5.9560546832890444e-05 +4.14678690275904e-05 +0.9741731590760117 0.1519713326893784 20.51335588582416 +350.53780805624825 304.05941264938997 142.62713592644738 +0.4 0.4 0.4 0.0 0.0 0.0 -MassiveBody_05 9.9267578054817388455e-05 0.010382929139161686458 -4.916559523190238318e-05 -1.101215402063684401 0.076651567404004070094 52.41961577462824806 -142.90070862650665617 293.70542448390904156 318.5666754758643151 -0.4000000000000000222 0.4000000000000000222 0.4000000000000000222 +MassiveBody_05 9.926757805481742e-05 +4.916559523190238e-05 +0.6633500122731075 0.13550930562584215 10.680323453316653 +328.45970148163457 49.93948991697533 316.2109831817007 +0.4 0.4 0.4 0.0 0.0 0.0 diff --git a/examples/Basic_Simulation/run_from_file.py b/examples/Basic_Simulation/run_from_file.py new file mode 100644 index 000000000..9c477410a --- /dev/null +++ b/examples/Basic_Simulation/run_from_file.py @@ -0,0 +1,3 @@ +import swiftest +sim = swiftest.Simulation() +sim.run(tstop=20.0) \ No newline at end of file diff --git a/examples/Basic_Simulation/run_simulation.ipynb b/examples/Basic_Simulation/run_simulation.ipynb new file mode 100644 index 000000000..8f2ab51b3 --- /dev/null +++ b/examples/Basic_Simulation/run_simulation.ipynb @@ -0,0 +1,159 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "86c845ce-1801-46ca-8a8a-1cabb266e6a6", + "metadata": {}, + "outputs": [], + "source": [ + "import swiftest\n", + "import xarray as xr\n", + "import numpy as np\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d716c371-8eb4-4fc1-82af-8b5c444c831e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading Swiftest file /home/daminton/git_debug/swiftest/examples/Basic_Simulation/param.in\n" + ] + } + ], + "source": [ + "sim = swiftest.Simulation()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ec7452d6-4c9b-4df3-acc0-b11c32264b91", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tstop 10.0 y\n", + "Writing parameter inputs to file /home/daminton/git_debug/swiftest/examples/Basic_Simulation/param.in\n", + "Running a Swiftest symba run from tstart=0.0 y to tstop=10.0 y\n", + "\u001b]2;cd /home/daminton/git_debug/swiftest/examples/Basic_Simulation\u0007\u001b]1;\u0007\u001b]2;/home/daminton/git_debug/swiftest/bin/swiftest_driver symba \u0007\u001b]1;\u0007 Parameter input file is /home/daminton/git_debug/swiftest/examples/Basic_Simulation/param.in\n", + " \n", + " Warning! NPLM variable not set in input file. Calculating.\n", + " *************** Main Loop *************** \n", + "Time = 1.00000E+00; fraction done = 0.100; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 6.30684E-01; Interval wall time: 5.29890E-01;Interval wall time/step: 2.70141E-03\n", + "Time = 2.00000E+00; fraction done = 0.200; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 1.66455E+00; Interval wall time: 5.27720E-01;Interval wall time/step: 2.99766E-03\n", + "Time = 3.00000E+00; fraction done = 0.300; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 2.64051E+00; Interval wall time: 5.20805E-01;Interval wall time/step: 2.88832E-03\n", + "Time = 4.00000E+00; fraction done = 0.400; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 3.60585E+00; Interval wall time: 5.24579E-01;Interval wall time/step: 2.82311E-03\n", + "Time = 5.00000E+00; fraction done = 0.500; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 4.58823E+00; Interval wall time: 5.37595E-01;Interval wall time/step: 2.96439E-03\n", + "Time = 6.00000E+00; fraction done = 0.600; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 5.55600E+00; Interval wall time: 5.27663E-01;Interval wall time/step: 2.83397E-03\n", + "Time = 7.00000E+00; fraction done = 0.700; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 6.64194E+00; Interval wall time: 5.83431E-01;Interval wall time/step: 3.11589E-03\n", + "Time = 8.00000E+00; fraction done = 0.800; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 7.63044E+00; Interval wall time: 5.52408E-01;Interval wall time/step: 2.95843E-03\n", + "Time = 9.00000E+00; fraction done = 0.900; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 8.62339E+00; Interval wall time: 5.46944E-01;Interval wall time/step: 3.01578E-03\n", + "Time = 1.00000E+01; fraction done = 1.000; Number of active plm, pl, tp = 13, 14, 10\n", + "Integration steps: Total wall time: 9.67297E+00; Interval wall time: 6.00750E-01;Interval wall time/step: 3.20243E-03\n", + "\n", + "Normal termination of Swiftest (version 1.0)\n", + "------------------------------------------------\n", + "\n", + "Creating Dataset from NetCDF file\n", + "Successfully converted 11 output frames.\n", + "Swiftest simulation data stored as xarray DataSet .ds\n" + ] + } + ], + "source": [ + "sim.run(tstop=10.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5b0a57a6-dbd5-4d34-8e6e-91fc8ad8789f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjMAAAGwCAYAAABcnuQpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAACEU0lEQVR4nOzdfXyN9f/A8de12c7uN5vZDWez2TDGRiKjkHszFaEIaygJCSWiKFnubyIhG2VuSlTfiuKbkbvITS18VWyNH2uRtsy2s51z/f6YnXZsY9M456z389H16Fyfz+f6XO9zOTvX+3yuO0VVVRUhhBBCCCtlY+4AhBBCCCH+CUlmhBBCCGHVJJkRQgghhFWTZEYIIYQQVk2SGSGEEEJYNUlmhBBCCGHVJJkRQgghhFWrYe4A7jSDwcCFCxdwdXVFURRzhyOEEEKIClBVlb/++gt/f39sbG4+9lLtk5kLFy6g1WrNHYYQQgghbsO5c+eoW7fuTdtU+2TG1dUVKNoYbm5uZo5GCCGEEBWRnZ2NVqs17sdvptonM8WHltzc3CSZEUIIIaxMRU4RkROAhRBCCGHVJJkRQgghhFWTZEYIIYQQVq3anzMjhCXT6/UUFBSYOwzxL2dnZ4etra25wxDitkkyI4QZqKpKRkYGf/75p7lDEQIADw8PfH195X5cwipZTDITHx/PlClTeO6551i0aBFQ9IU/Y8YMVq5cyZUrV2jdujXLli2jSZMm5g1WiH+oOJGpXbs2Tk5OsgMRZqOqKteuXSMzMxMAPz8/M0ckROVZRDJz+PBhVq5cSbNmzUzK58yZw4IFC1izZg0NGjRg5syZdOnShdOnT1founMhLJFerzcmMl5eXuYORwgcHR0ByMzMpHbt2nLISVgds58AfPXqVQYNGsSqVauoWbOmsVxVVRYtWsTLL79Mnz59CA8PZ+3atVy7do3169ebMWIh/pnic2ScnJzMHIkQfyv+PMo5XMIamT2ZefbZZ4mOjqZz584m5ampqWRkZNC1a1djmUajoX379uzfv7/c/vLz88nOzjaZhLBEcmhJWBL5PAprZtbDTBs3buTo0aMcPny4VF1GRgYAPj4+JuU+Pj78+uuv5fYZHx/PjBkzqjZQIYQQQlgss43MnDt3jueee45169bh4OBQbrsbfy2oqnrTXxCTJ08mKyvLOJ07d67KYhZCCCGE5THbyMyRI0fIzMzknnvuMZbp9Xr27NnD0qVLOX36NFA0QlPy7PrMzMxSozUlaTQaNBrNnQtcCCGEEBbFbMlMp06dSElJMSl78sknadSoEZMmTSI4OBhfX1927NhB8+bNAdDpdOzevZvZs2ebI+S7TlVVoGhSVcP1/xeXGa6/NlxvazBthwrX64teG0r0WXLESwFFQUEpen3DfFG7vyeT5cqsL66zMamT4/FCCCHuFLMlM66uroSHh5uUOTs74+XlZSwfN24cs2bNIjQ0lNDQUGbNmoWTkxMDBw40R8gmzv/fetLTV/2dXKjXk4brSYRKyeTi72Tj7ySkYklK9XOrJKh43ub66+LEyxZFsUFRbP+esAXFBkWpUaLu+jw2ULJtmXUlluXvdrdfZ2MSm2JTAxvFDkWpgY2N3fVl7CgsrIHBYIten4deT6ntIUmgEEJUjkXcZ6Y8L774Irm5uYwaNcp407yvvvrKIu4xU1iQTW5uurnDuAXTxODvnaRaNGhDyaRKLTF/JxWv74bSO71aC2Jj44+H+6vk5iro9RVIVm4yclZUXV5d6XnTtpRfd5ME62Z9mvZx8ytkOnToQLNmzXBwcODdd9/F3t6ekSNHMn36dAAWLFhAYmIiZ8+exdPTk5iYGObMmYOLiwsAa9asYdy4caxbt44JEyZw7tw5evbsydq1a9m8eTOvvvoqWVlZPPHEEyxatMh47xSdTsfUqVNJSkrizz//JDw8nNmzZ9OhQ4db/1sIISySRSUzycnJJvOKojB9+nTjl5sl8fV9CI+a917/Ure54dDM9VEFxebvL31FKRoRKPGF//foQ4lDMhVtV2aSUlxv849/0RePDpkmObdOgsoadeL6mBUl64yHwUovZ7psyVj0f08YUNXConYly6/XoeoxqPrrdSWW5e92xjr0qIbiuuL+Cv+uU8tZF6Z9c0NcqlqIwVBwfb7A+Bq1ZolRI+WG7VHmP0bxFrw+f+dTzqpTfiKk1+eydm0io0fHsWvXhxw6dIynn36Re+4JoVOnB9Drs5g7dyqBgVrSfj3PuOemMGHCNRYvfhNFUSgoyObatWssWjSf999/h7/+usqAAbE8/HAMHh4efPLJJlJT0xgwYAht2txD//6PAgpDh8bx66/pJCWtxd/fn48//oTu3bvz/fdHCA1tUHbcyKXLwrxUVb3+XVLy+67w7+8cgx6uf78Z1MKiowVltS81X047KrY8qgGDWoiXZztq1XrQbNtHUdXq/Zs4Ozsbd3d3srKycHNzM3c4QpCXl0dqaipBQUGlruQzTQbLSxpvnlSWbltWEslN6m623K36rLjo6Dj0ej3bt681lnXsOJAHHmjFjBnjSrXfuvUrxo+fSWrqHgCSkj5h1KhpHDv2OcHBWgDGjXudTZv+w88/J+PiUnQTuD59RhIQUIdFi6Zx9uw5WrToxalTO/Dzq23su3fvEdxzTzivvvrcTSK+cTSrqOzGpOfGkbNSo2XGMkr0U8Zr47rKqTe+LNnq9tvk5RWQnn4ed/ffsLPXXz8ca4Ni8iPq+g+M4nTa+DksLisuL/EZK1V2wzIl2t6s//KWMUn0jespbmu4vlP/+4eJ8QcJaukfNer19iV+qFD8g8XYV/GPlus/Yq73c2Nb47yxraHED56S9fq/6411NyYZhSW2g2UKDHyGkPoTq7TPyuy/LWpkRoh/u793GmBtAwEVTqiu74hsbBwID2+Eo2Ogsc7fX8uVK3k4OPizK/kb5sxeyP/+9xPZ2X9RWKgnLy+PggJHnJwdsbV1wsnJkYYNw43Jna+vH4GBWtw9vI07PB8fHy5d+hMbGw0pKT+jqir33NObkjuH/PwCPL08KLnDLuMdXv/PtL66/BzU6VQKCq7wy5l4DIYL5g5HVIKi2JU4l6/keYI1riejNVBsbAFbbIrLjecA3jjd2McNfdnU+Pu8wBLtPTxamXUbSDIjhKgSlU3EFMUWjcYZO7u/f3HZ2toDdly8eJWHH3qckSNH8sYbs/H09GTv3r0MGzYMW9taODp4YG/viZ2dPY6OAcbl7e1rYm/vhLNTsLHMzs4NGxsDLi4NsLc/hq2tLUeOHC31/CEXFxfc3HxRbxgpUEv80i9rvmSCdvP5G/oprjZJjtQb/l88EnFjfRltbtrfrdep1xdiY6PB3a0FKIGYXiFZdMi56Dz9v0eliv+vlBhxMh35uT66pJTR1mSZstuW3X/JuoosU+LiAEqerF80X7q+eETq74sOjCf+F49WGXfm1+uxKdG2ZL1StCx/Xxzw92hX8bpuqEcpkVAUX9xQRpJS4sIGIcmMEMICfffddxQWFjJ//nxsbIq+rD/44IN/3G/z5s3R6/VkZmZy//33l9lGuWHHaG0jZLfLxiYPe/sCgoJm3/RGpkJYIknphBAWp379+hQWFvLWW29x9uxZ3n//fd55551/3G+DBg0YNGgQQ4YMYcuWLaSmpnL48GFmz57NF198UQWRCyHMQZIZIYTFiYyMZMGCBcyePZvw8HCSkpKIj4+vkr4TExMZMmQIEyZMoGHDhvTu3Ztvv/0WrVZbJf0LIe4+uZpJiLvsZlczCWEu8rkUlqYy+28ZmRFCCCGEVZNkRgghhBBWTZIZIYQQQlg1SWaEEEIIYdUkmRFCCCGEVZNkRgghhBBWTZIZIYQQQlg1SWaEEEIIYdUkmRFCCCGEVZNkRghRYbGxsSiKwsiRI0vVjRo1CkVRiI2NvfuBCSH+1SSZEUJUilarZePGjeTm5hrL8vLy2LBhAwEBAbfdr6qqFBYWVkWIJvR6PQaDocr7FUJYDklmhBCV0qJFCwICAtiyZYuxbMuWLWi1Wpo3b24sU1WVOXPmEBwcjKOjIxEREWzevNlYn5ycjKIofPnll7Rs2RKNRsM333yDwWBg9uzZhISEoNFoCAgI4I033jBZ5s8//zT2c/z4cRRFIS0tDYA1a9bg4eHBZ599RuPGjY392tnZkZGRYfJeJkyYwAMPPHAHtpIQ4m6qYe4AhBBFO/7cAr1Z1u1oZ4uiKJVa5sknnyQxMZFBgwYBkJCQQFxcHMnJycY2U6dOZcuWLSxfvpzQ0FD27NnDE088gbe3N+3btze2e/HFF5k3bx7BwcF4eHgwefJkVq1axcKFC2nXrh0XL17kf//7X6Xiu3btGvHx8bz77rt4eXlRt25dgoODef/993nhhRcAKCwsZN26dbz55puV6lsIYXkkmRHCAuQW6Gn8ypdmWffJ17rhZF+5r4LBgwczefJk0tLSUBSFffv2sXHjRmMyk5OTw4IFC/j6669p06YNAMHBwezdu5cVK1aYJDOvvfYaXbp0AeCvv/5i8eLFLF26lKFDhwJQv3592rVrV6n4CgoKePvtt4mIiDCWDRs2jMTERGMy8/nnn3Pt2jX69+9fqb6FEJZHkhkhRKXVqlWL6Oho1q5di6qqREdHU6tWLWP9yZMnycvLMyYpxXQ6ncmhKICWLVsaX586dYr8/Hw6der0j+Kzt7enWbNmJmWxsbFMnTqVgwcPct9995GQkED//v1xdnb+R+sSQpifJDNCWABHO1tOvtbNbOu+HXFxcYwePRqAZcuWmdQVn3D7+eefU6dOHZM6jUZjMl8ymXB0dLzpOm1sik7zU1XVWFZQUFCqnaOjY6lDZ7Vr1yYmJobExESCg4P54osvTA6LCSGslyQzQlgARVEqfajH3Lp3745OpwOgWzfTRKz4xNv09HSTQ0q3EhoaiqOjI//9738ZPnx4qXpvb28ALl68SM2aNYGiE4Aravjw4Tz22GPUrVuX+vXr07Zt2wovK4SwXNb17SmEsBi2tracOnXK+LokV1dXJk6cyPPPP4/BYKBdu3ZkZ2ezf/9+XFxcjOfD3MjBwYFJkybx4osvYm9vT9u2bfn99985ceIEw4YNIyQkBK1Wy/Tp05k5cyY///wz8+fPr3DM3bp1w93dnZkzZ/Laa6/d/psXQlgUSWaEELfNzc2t3LrXX3+d2rVrEx8fz9mzZ/Hw8KBFixZMmTLlpn1OmzaNGjVq8Morr3DhwgX8/PyMN+mzs7Njw4YNPPPMM0RERHDvvfcyc+ZM+vXrV6F4bWxsiI2NZdasWQwZMqTib1QIYdEUteTB52ooOzsbd3d3srKybvrFK8TdkpeXR2pqKkFBQTg4OJg7nH+dESNG8Ntvv/Hpp5+aOxSLIp9LYWkqs/+WkRkhxL9CVlYWhw8fJikpiU8++cTc4QghqpAkM0KIf4WHHnqIQ4cO8fTTT5e6ZFwIYd0kmRFC/CvIZdhCVF/ybCYhhBBCWDVJZoQQQghh1cyazCxfvpxmzZrh5uaGm5sbbdq0Ydu2bcb62NhYFEUxme677z4zRiyEEEIIS2PWc2bq1q3Lm2++SUhICABr167loYce4tixYzRp0gQoustoYmKicRl7e3uzxCqEEEIIy2TWZCYmJsZk/o033mD58uUcPHjQmMxoNBp8fX3NEZ4QQgghrIDFnDOj1+vZuHEjOTk5tGnTxlienJxM7dq1adCgASNGjCAzM/Om/eTn55OdnW0yCSGEEKL6Mnsyk5KSgouLCxqNhpEjR7J161YaN24MQI8ePUhKSuLrr79m/vz5HD58mAcffJD8/Pxy+4uPj8fd3d04abXau/VWhBBCCGEGZn+cgU6nIz09nT///JOPPvqId999l927dxsTmpIuXrxIYGAgGzdupE+fPmX2l5+fb5LsZGdno9Vq5XEGwmJY623jY2JiyM3NZefOnaXqDhw4QFRUFEeOHKFFixZmiE78U9b6uRTVl1U9zsDe3t54AnDLli05fPgwixcvZsWKFaXa+vn5ERgYyM8//1xufxqNBo1Gc8fiFeLfatiwYfTp04dff/2VwMBAk7qEhAQiIyMlkRFCmIXZDzPdSFXVcg8jXb58mXPnzuHn53eXoxJC9OrVi9q1a7NmzRqT8mvXrrFp0yaGDRvG/v37eeCBB3B0dESr1TJ27FhycnKMbevVq8esWbOIi4vD1dWVgIAAVq5caaxPTk5GURT+/PNPY9nx48dRFIW0tDQAfv31V2JiYqhZsybOzs40adKEL7744k6+dSGEhTNrMjNlyhS++eYb0tLSSElJ4eWXXyY5OZlBgwZx9epVJk6cyIEDB0hLSyM5OZmYmBhq1arFI488Ys6whah6qgq6HPNMFTzSXKNGDYYMGcKaNWsoeXT6ww8/RKfTERERQbdu3ejTpw8//PADmzZtYu/evYwePdqkn/nz59OyZUuOHTvGqFGjeOaZZ/jf//5X4U317LPPkp+fz549e0hJSWH27Nm4uLhUeHkhRPVj1sNMv/32G4MHD+bixYu4u7vTrFkztm/fTpcuXcjNzSUlJYX33nuPP//8Ez8/Pzp27MimTZtwdXU1Z9hCVL2CazDL3zzrnnIB7J0r1DQuLo65c+eSnJxMx44dgaJDTH369GHVqlUMHDiQcePGARAaGsqSJUto3749y5cvN56H0bNnT0aNGgXApEmTWLhwIcnJyTRq1KhCMaSnp9O3b1+aNm0KQHBwcGXerRCiGjJrMrN69epy6xwdHfnyyy/vYjRCiFtp1KgRUVFRJCQk0LFjR86cOcM333zDV199xXPPPccvv/xCUlKSsb2qqhgMBlJTUwkLCwOgWbNmxnpFUfD19b3lLRdKGjt2LM888wxfffUVnTt3pm/fviZ9CiH+fcx+ArAQArBzKhohMde6K2HYsGGMHj2aZcuWkZiYSGBgIJ06dcJgMPD0008zduzYUssEBAT8vTo7O5M6RVEwGAwA2NgUHfkueRiroKDApP3w4cPp1q0bn3/+OV999RXx8fHMnz+fMWPGVOp9CCGqD0lmhLAEilLhQz3m1r9/f5577jnWr1/P2rVrGTFiBIqi0KJFC06cOGG8OvF2eHt7A0W3YahZsyZQdALwjbRaLSNHjmTkyJFMnjyZVatWSTIjxL+YxV3NJISwbC4uLgwYMIApU6Zw4cIFYmNjgaLzXw4cOMCzzz7L8ePH+fnnn/n0008rlWSEhISg1WqZPn06P/30E59//jnz5883aTNu3Di+/PJLUlNTOXr0KF9//bXxEJYQ4t9JkhkhRKUNGzaMK1eu0LlzZ+MhpGbNmrF7925+/vln7r//fpo3b860adMqdSsFOzs7NmzYwP/+9z8iIiKYPXs2M2fONGmj1+t59tlnCQsLo3v37jRs2JC33367St+fEMK6mP0OwHdaZe4gKMTdIHdaFZZIPpfC0lRm/y0jM0IIIYSwapLMCCGEEMKqSTIjhBBCCKsmyYwQQgghrJokM0IIIYSwapLMCCGEEMKqSTIjhBBCCKsmyYwQQgghrJokM0IIIYSwapLMCCEsUlpaGoqilPmgSSGEKEmSGSFEhcXGxqIoSqmpe/fu/7jfhx9+uGqCFEL869QwdwBCCOvSvXt3EhMTTco0Gs1t9aXX61EUpSrCEkL8i8nIjBCiUjQaDb6+viZTzZo1AViwYAFNmzbF2dkZrVbLqFGjuHr1qnHZNWvW4OHhwWeffUbjxo3RaDQ8+eSTrF27lk8++cQ40pOcnGxc5uzZs3Ts2BEnJyciIiI4cODA3X7LQggLJyMzQlgAVVXJLcw1y7odazhW2eiIjY0NS5YsoV69eqSmpjJq1ChefPFF3n77bWOba9euER8fz7vvvouXlxe+vr7k5eWRnZ1tHPHx9PTkwoULALz88svMmzeP0NBQXn75ZR5//HF++eUXatSQry8hRBH5NhDCAuQW5tJ6fWuzrPvbgd/iZOdU4fafffYZLi4uJmWTJk1i2rRpjBs3zlgWFBTE66+/zjPPPGOSzBQUFPD2228TERFhLHN0dCQ/Px9fX99S65s4cSLR0dEAzJgxgyZNmvDLL7/QqFGjCscshKjeJJkRQlRKx44dWb58uUmZp6cnALt27WLWrFmcPHmS7OxsCgsLycvLIycnB2dnZwDs7e1p1qxZhddXsq2fnx8AmZmZkswIIYwkmRHCAjjWcOTbgd+abd2V4ezsTEhISKnyX3/9lZ49ezJy5Ehef/11PD092bt3L8OGDaOgoODv9TlW7rCWnZ2d8XXxcgaDoVIxCyGqN0lmhLAAiqJU6lCPJfruu+8oLCxk/vz52NgUXVvwwQcfVGhZe3t79Hr9nQxPCFGNSTIjhKiU/Px8MjIyTMpq1KhB/fr1KSws5K233iImJoZ9+/bxzjvvVKjPevXq8eWXX3L69Gm8vLxwd3e/E6ELIaopuTRbCFEp27dvx8/Pz2Rq164dkZGRLFiwgNmzZxMeHk5SUhLx8fEV6nPEiBE0bNiQli1b4u3tzb59++7wuxBCVCeKqqqquYO4k7Kzs3F3dycrKws3NzdzhyMEeXl5pKamEhQUhIODg7nDEQKQz6WwPJXZf8vIjBBCCCGsmiQzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqZk1mli9fTrNmzXBzc8PNzY02bdqwbds2Y72qqkyfPh1/f38cHR3p0KEDJ06cMGPEQgghhLA0Zr0DcN26dXnzzTeNz3lZu3YtDz30EMeOHaNJkybMmTOHBQsWsGbNGho0aMDMmTPp0qULp0+fxtXV1ZyhWy1VVUtN5ZVbUtuS8Zf3viypvJiiKMbnCRW/Ll4mLy/PZPnynld0s+cYVXaZipbfbL4yz1USQoi7weJumufp6cncuXOJi4vD39+fcePGMWnSJKDoNuo+Pj7Mnj2bp59+ukL93amb5qWlpXH27FlUVcVgMJj835LLhPm5uLjQtm1b6tSpQ40a1v1EkZslOVVdVxWvzdXWGhJAuWmesDSV2X9bzDepXq/nww8/JCcnhzZt2pCamkpGRgZdu3Y1ttFoNLRv3579+/eXm8zk5+eTn59vnM/Ozr4j8Z47d449e/bckb4tTVmjC7eaqrJtcX3JeG4Wa2XK7+Qy5Y04aTQaatSogZ2dXbnJzM1+Y9ytUaWKqMiomTD1T5Kyf7r8zV7rdDry8vI4cuSI8W/PxsYGGxsb4+uK/k1b0nQ729Aaks877cZR8eKysl5D0TaztbW9K7GVxezJTEpKCm3atCEvLw8XFxe2bt1K48aN2b9/PwA+Pj4m7X18fPj111/L7S8+Pp4ZM2bc0ZgB/P39adWqlckf/Y1/8JZYduNruHXSIapW8S/gmjVrWuQv4JJJ2I3lcXFxvPfee4wYMYK3337bpG7MmDGsWLGCwYMH8+6775bbR1mvb1Z3O+3Kel3Rstvtq6IsNQEsLCwkLy+PAwcOcPXqVXOHY3FuJ6m83XbFbvbZv3G+qupuV7t27ejcuXOV9HU7zJ7MNGzYkOPHj/Pnn3/y0UcfMXToUHbv3m2sv/EfWFXVm+5gJ0+ezPjx443z2dnZaLXaKo+7fv361K9fv8r7FcLcbvbrVFEUtFotH3zwAYsXL8bR0REoStA2bdpEQEAANjY22Nvb39a6CwoKsLOzu/3gzayiSdKdSNb+6fI6nQ57e3saNmxIQUGByeHpkoetLWEqGc/dcqvEQpiX2ZMZe3t74wnALVu25PDhwyxevNh4nkxGRgZ+fn7G9pmZmaVGa0rSaDRoNJo7G7QQ/2ItWrTg7NmzbNmyhUGDBgGwZcsWtFotwcHBxnbbt29n5syZ/Pjjj9ja2tKmTRsWL15s/BGQlpZGUFAQmzZt4u233+bgwYMsX76cBx98kNGjR7N37150Oh316tVj7ty59OzZ0yzvtzKs+TBFXl4eTk5OdOnSxSJHDMtzq8SnZLsbl7tbdZXto6KjO7eav5ttzXmICSwgmbmRqqrk5+cTFBSEr68vO3bsoHnz5kDRL4fdu3cze/ZsM0cpRNVSVRU1N9cs61YcHSu9833yySdJTEw0JjMJCQnExcWRnJxsbJOTk8P48eNp2rQpOTk5vPLKKzzyyCMcP34cG5u/7woxadIk5s+fT2JiIhqNhqeeegqdTseePXtwdnbm5MmTuLi4VMl7FdWPHA4XYOZkZsqUKfTo0QOtVstff/3Fxo0bSU5OZvv27SiKwrhx45g1axahoaGEhoYya9YsnJycGDhwoDnDFqLKqbm5nG5xj1nW3fDoERQnp0otM3jwYCZPnkxaWhqKorBv3z7j32+xvn37miyzevVqateuzcmTJwkPDzeWjxs3jj59+hjn09PT6du3L02bNgUwGe0RQoiymDWZ+e233xg8eDAXL17E3d2dZs2asX37drp06QLAiy++SG5uLqNGjeLKlSu0bt2ar776Su4xI4SZ1apVi+joaNauXYuqqkRHR1OrVi2TNmfOnGHatGkcPHiQS5cuGW8NkJ6ebpLMtGzZ0mS5sWPH8swzz/DVV1/RuXNn+vbtS7Nmze78mxJCWC2zJjOrV6++ab2iKEyfPp3p06ffnYCEMBPF0ZGGR4+Ybd23Iy4ujtGjRwOwbNmyUvUxMTFotVpWrVqFv78/BoOB8PBwdDqdSTtnZ2eT+eHDh9OtWzc+//xzvvrqK+Lj45k/fz5jxoy5rTiFENWfxZ0zI8S/kaIolT7UY27du3c3JibdunUzqbt8+TKnTp1ixYoV3H///QDs3bu3wn1rtVpGjhzJyJEjmTx5MqtWrZJkRghRLklmhBC3xdbWllOnThlfl1SzZk28vLxYuXIlfn5+pKen89JLL1Wo33HjxtGjRw8aNGjAlStX+PrrrwkLC6vy+IUQ1YckM0KI21beLcZtbGzYuHEjY8eOJTw8nIYNG7JkyRI6dOhwyz71ej3PPvss58+fx83Nje7du7Nw4cIqjlwIUZ1Y3LOZqtqdejaTELdLnoEjLJF8LoWlqcz+2+amtUIIIYQQFk6SGSGEEEJYNUlmhBBCCGHVJJkRQgghhFWTZEYIIYQQVk2SGSGEEEJYNUlmhBBCCGHVJJkRQgghhFWTZEYIIYQQVk2SGSGE2aSlpaEoCsePHzd3KEIIKybJjBCiwmJjY3n44YerrD+tVsvFixcJDw8HIDk5GUVR+PPPP6tsHUKI6k8eNCmEMBtbW1t8fX2rvF9VVdHr9dSoIV9xQvwbyMiMEBZAVVUK8vVmmW73WbP16tVj0aJFJmWRkZFMnz7dOK8oCsuXL6dHjx44OjoSFBTEhx9+aKwveZgpLS2Njh07AlCzZk0URSE2Nta4febMmUNwcDCOjo5ERESwefNmYz/FIzpffvklLVu2RKPR8M0339zW+xJCWB/52SKEBSjUGVj53G6zrPupxe2x09jesf6nTZvGm2++yeLFi3n//fd5/PHHCQ8PJywszKSdVqvlo48+om/fvpw+fRo3NzccHR0BmDp1Klu2bGH58uWEhoayZ88ennjiCby9vWnfvr2xjxdffJF58+YRHByMh4fHHXtPQgjLIsmMEOKO6tevH8OHDwfg9ddfZ8eOHbz11lu8/fbbJu1sbW3x9PQEoHbt2sZkJCcnhwULFvD111/Tpk0bAIKDg9m7dy8rVqwwSWZee+01unTpchfelRDCkkgyI4QFqGFvw1OL29+64R1a951UnICUnK/M1UsnT54kLy+vVJKi0+lo3ry5SVnLli1vO04hhPWSZEYIC6Aoyh091HMn2NjYlDrfpqCgoELLKopS4fUYDAYAPv/8c+rUqWNSp9FoTOadnZ0r3K8QovqQZEYIcVu8vb25ePGicT47O5vU1NRS7Q4ePMiQIUNM5m8cUSlmb28PgF6vN5Y1btwYjUZDenq6ySElIYQoJsmMEOK2PPjgg6xZs4aYmBhq1qzJtGnTsLUtPbr04Ycf0rJlS9q1a0dSUhKHDh1i9erVZfYZGBiIoih89tln9OzZE0dHR1xdXZk4cSLPP/88BoOBdu3akZ2dzf79+3FxcWHo0KF3+q0KISycJDNCiAozGAzGe7dMnjyZs2fP0qtXL9zd3Xn99dfLHJmZMWMGGzduZNSoUfj6+pKUlETjxo3L7L9OnTrMmDGDl156iSeffJIhQ4awZs0aXn/9dWrXrk18fDxnz57Fw8ODFi1aMGXKlDv6foUQ1kFRb/cmE1YiOzsbd3d3srKycHNzM3c4QpCXl0dqaipBQUE4ODiYO5xK6d69OyEhISxdurRC7RVFYevWrVV612BxZ1jz51JUT5XZf8tN84QQt3TlyhU+//xzkpOT6dy5s7nDEUIIE3KYSQhxS3FxcRw+fJgJEybw0EMPmTscIYQwIcmMEOKWtm7delvLVfOj2EIICyGHmYQQQghh1SSZEUIIIYRVk2RGCCGEEFZNkhkhhBBCWDWzJjPx8fHce++9uLq6Urt2bR5++GFOnz5t0iY2NhZFUUym++67z0wRCyGEEMLSmDWZ2b17N88++ywHDx5kx44dFBYW0rVrV3Jyckzade/enYsXLxqnL774wkwRCyGEEMLSmPXS7O3bt5vMJyYmUrt2bY4cOcIDDzxgLNdoNPj6+t7t8IQQQghhBSzqnJmsrCwAPD09TcqTk5OpXbs2DRo0YMSIEWRmZpbbR35+PtnZ2SaTEKLqZGZm8vTTTxMQEGD8odGtWzcOHDhQoeXXrFmDh4fHnQ1SCPGvYjE3zVNVlfHjx9OuXTvCw8ON5T169KBfv34EBgaSmprKtGnTePDBBzly5AgajaZUP/Hx8cyYMeNuhi7Ev0rfvn0pKChg7dq1BAcH89tvv/Hf//6XP/74467HUlBQgJ2d3V1frxDCwqgWYtSoUWpgYKB67ty5m7a7cOGCamdnp3700Udl1ufl5alZWVnG6dy5cyqgZmVl3Ymwhai03Nxc9eTJk2pubq6xzGAwqLrcXLNMBoOhwrFfuXJFBdTk5ORy28yfP18NDw9XnZyc1Lp166rPPPOM+tdff6mqqqq7du1SAZPp1VdfVVVVVQF169atJn25u7uriYmJqqqqampqqgqomzZtUtu3b69qNBo1ISFBHTp0qPrQQw+pc+fOVX19fVVPT0911KhRqk6nq/D7EmV/LoUwp6ysrArvvy1iZGbMmDF8+umn7Nmzh7p16960rZ+fH4GBgfz8889l1ms0mjJHbISwZIX5+SwZ+qhZ1j127WbsKviUZBcXF1xcXPj444+57777yvxbs7GxYcmSJdSrV4/U1FRGjRrFiy++yNtvv01UVBSLFi3ilVdeMV656OLiUql4J02axPz580lMTESj0bB792527dqFn58fu3bt4pdffmHAgAFERkYyYsSISvUthLBOZj1nRlVVRo8ezZYtW/j6668JCgq65TKXL1/m3Llz+Pn53YUIhRAl1ahRgzVr1rB27Vo8PDxo27YtU6ZM4YcffjC2GTduHB07diQoKIgHH3yQ119/nQ8++AAAe3t73N3dURQFX19ffH19K53MjBs3jj59+hAUFIS/vz8ANWvWZOnSpTRq1IhevXoRHR3Nf//736p740IIi2bWkZlnn32W9evX88knn+Dq6kpGRgYA7u7uODo6cvXqVaZPn07fvn3x8/MjLS2NKVOmUKtWLR555BFzhi5Elaqh0TB27Wazrbsy+vbtS3R0NN988w0HDhxg+/btzJkzh3fffZfY2Fh27drFrFmzOHnyJNnZ2RQWFpKXl0dOTg7Ozs7/ON6WLVuWKmvSpAm2trbGeT8/P1JSUv7xuoQQ1sGsyczy5csB6NChg0l5YmIisbGx2NrakpKSwnvvvceff/6Jn58fHTt2ZNOmTbi6upohYiHuDEVRKnyoxxI4ODjQpUsXunTpwiuvvMLw4cN59dVX6dixIz179mTkyJG8/vrreHp6snfvXoYNG0ZBQcFN+1QUpdRTtstapqyE6MaTgBVFwWAw3MY7E0JYI7MmMzd+cd3I0dGRL7/88i5FI4S4XY0bN+bjjz/mu+++o7CwkPnz52NjU3QUu/gQUzF7e3v0en2pPry9vbl48aJx/ueff+batWt3NnAhRLVgEScACyGsw+XLl+nXrx9xcXE0a9YMV1dXvvvuO+bMmcNDDz1E/fr1KSws5K233iImJoZ9+/bxzjvvmPRRr149rl69yn//+18iIiJwcnLCycmJBx98kKVLl3LfffdhMBiYNGmSXHYthKgQi7ppnhDCsrm4uNC6dWsWLlzIAw88QHh4ONOmTWPEiBEsXbqUyMhIFixYwOzZswkPDycpKYn4+HiTPqKiohg5ciQDBgzA29ubOXPmADB//ny0Wi0PPPAAAwcOZOLEiTg5OZnjbQohrIyi3upYj5XLzs7G3d2drKws3NzczB2OEOTl5ZGamkpQUBAOVnSejKje5HMpLE1l9t8yMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQogK69ChA+PGjStV/vHHH6Moyt0PSAghkGRGCHGHFRQUmDsEIUQ1J8mMEBZAVVUMOr1Zpqp+1uz06dOJjIwkISGB4OBgNBoNqqqyfft22rVrh4eHB15eXvTq1YszZ84Yl0tLS0NRFLZs2ULHjh1xcnIiIiKCAwcOlOq7pEWLFlGvXj3jfHJyMq1atcLZ2RkPDw/atm3Lr7/+WqXvUQhhWWqYOwAhBKgFBi68st8s6/Z/LQrF3rZK+/zll1/44IMP+Oijj7C1Leo7JyeH8ePH07RpU3JycnjllVd45JFHOH78ODY2f/+uevnll5k3bx6hoaG8/PLLPP744/zyyy/UqHHrr6vCwkIefvhhRowYwYYNG9DpdBw6dEgOgQlRzUkyI4Socjqdjvfffx9vb29jWd++fU3arF69mtq1a3Py5EnCw8ON5RMnTiQ6OhqAGTNm0KRJE3755RcaNWp0y/VmZ2eTlZVFr169qF+/PgBhYWFV8ZaEEBZMkhkhLIBiZ4P/a1FmW3dVCwwMNElkAM6cOcO0adM4ePAgly5dwmAwAJCenm6SzDRr1sz42s/PD4DMzMwKJTOenp7ExsbSrVs3unTpQufOnenfv7+xHyFE9STnzAhhARRFwcbe1ixTZQ7BuLm5kZWVVar8zz//xM3NzTjv7Oxcqk1MTAyXL19m1apVfPvtt3z77bdA0ShOSXZ2dibbBTAmPjY2NqXO8bnxBOPExEQOHDhAVFQUmzZtokGDBhw8eLDC71EIYX0kmRFCVFijRo347rvvSpUfPnyYhg0blrvc5cuXOXXqFFOnTqVTp06EhYVx5cqVSq/f29ubjIwMk4Tm+PHjpdo1b96cyZMns3//fsLDw1m/fn2l1yWEsB6SzAghKmzUqFGcOXOGZ599lu+//56ffvqJZcuWsXr1al544YVyl6tZsyZeXl6sXLmSX375ha+//prx48dXev0dOnTg999/Z86cOZw5c4Zly5axbds2Y31qaiqTJ0/mwIED/Prrr3z11Vf89NNPct6MENWcJDNCiAqrV68e33zzDWfOnKFr167ce++9rFmzhjVr1tCvX79yl7OxsWHjxo0cOXKE8PBwnn/+eebOnVvp9YeFhfH222+zbNkyIiIiOHToEBMnTjTWOzk58b///Y++ffvSoEEDnnrqKUaPHs3TTz99W+9XCGEdFLWqbzJhYbKzs3F3dycrK8vkmL4Q5pKXl0dqaipBQUE4ODiYOxwhAPlcCstTmf23jMwIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqZk1m4uPjuffee3F1daV27do8/PDDnD592qSNqqpMnz4df39/HB0d6dChAydOnDBTxEIIIYSwNGZNZnbv3s2zzz7LwYMH2bFjB4WFhXTt2pWcnBxjmzlz5rBgwQKWLl3K4cOH8fX1pUuXLvz1119mjFwIIYQQlsKsycz27duJjY2lSZMmREREkJiYSHp6OkeOHAGKRmUWLVrEyy+/TJ8+fQgPD2ft2rVcu3ZNnoIrhBnExsaiKApvvvmmSfnHH3+MoihVui5FUfj444+rtE8hRPVU458sfPLkSdLT09HpdCblvXv3vq3+srKyAPD09ASKnoCbkZFB165djW00Gg3t27dn//79ZT48Lj8/n/z8fON8dnb2bcUihCibg4MDs2fP5umnn6ZmzZrmDkcIIW5vZObs2bNEREQQHh5OdHQ0Dz/8MA8//DCPPPIIjzzyyG0Foqoq48ePp127doSHhwOQkZEBgI+Pj0lbHx8fY92N4uPjcXd3N05arfa24hHiblJVFZ1OZ5apss+a7dy5M76+vsTHx5fbZv/+/TzwwAM4Ojqi1WoZO3asyeHjevXq8frrrzNw4EBcXFzw9/fnrbfeMqkHeOSRR1AUxTgfGxvLww8/bLKucePG0aFDB+N8hw4dGDt2LC+++CKenp74+voyffp0k2WysrJ46qmnqF27Nm5ubjz44IN8//33ldoOQgjLcVsjM8899xxBQUHs3LmT4OBgDh06xOXLl5kwYQLz5s27rUBGjx7NDz/8wN69e0vV3Th8rapquUPakydPZvz48cb57OxsSWiExSsoKGDWrFlmWfeUKVOwt7evcHtbW1tmzZrFwIEDGTt2LHXr1jWpT0lJoVu3brz++uusXr2a33//ndGjRzN69GgSExON7ebOncuUKVOYPn06X375Jc8//zyNGjWiS5cuHD58mNq1a5OYmEj37t2xtbWt1Htau3Yt48eP59tvv+XAgQPExsbStm1bunTpgqqqREdH4+npyRdffIG7uzsrVqygU6dO/PTTT8aRYSGE9bitkZkDBw7w2muv4e3tjY2NDTY2NrRr1474+HjGjh1b6f7GjBnDp59+yq5du0y+GH19fQFKjcJkZmaWGq0pptFocHNzM5mEEFXrkUceITIykldffbVU3dy5cxk4cCDjxo0jNDSUqKgolixZwnvvvUdeXp6xXdu2bXnppZdo0KABY8aM4dFHH2XhwoUAeHt7A+Dh4YGvr69xvqKaNWvGq6++SmhoKEOGDKFly5b897//BWDXrl2kpKTw4Ycf0rJlS0JDQ5k3bx4eHh5s3rz5djeJEMKMbmtkRq/X4+LiAkCtWrW4cOECDRs2JDAwsNSl1Tejqipjxoxh69atJCcnExQUZFIfFBSEr68vO3bsoHnz5gDodDp2797N7Nmzbyd0ISySnZ0dU6ZMMdu6b8fs2bN58MEHmTBhgkn5kSNH+OWXX0hKSjKWqaqKwWAgNTWVsLAwANq0aWOyXJs2bVi0aNFtxXKjZs2amcz7+fmRmZlpjO/q1at4eXmZtMnNzeXMmTNVsn4hxN11W8lMeHg4P/zwA8HBwbRu3Zo5c+Zgb2/PypUrCQ4OrnA/zz77LOvXr+eTTz7B1dXVOALj7u6Oo6MjiqIwbtw4Zs2aRWhoKKGhocyaNQsnJycGDhx4O6ELYZEURanUoR5L8MADD9CtWzemTJlCbGyssdxgMPD000+XOUobEBBw0z5vdUWUjY2N8Ryf4v8XX4BQ8tyfGjVqmMwrioLBYEBVVfR6PX5+fuzatQtuOF3Iw8MD1VC5c4jKVMnzkMzqeqhqoQHVoKL/S0dhfsWvTCuzZUULK7qaCrcrv6FJVfHMjc2VG17cUF+02O0ti3Lrz7e4fbeVzEydOtV4Mt/MmTPp1asX999/P15eXmzatKnC/SxfvhzA5OQ9gMTEROOX44svvkhubi6jRo3iypUrtG7dmq+++gpXV9fbCb3K5J35k7zTf/z9ZVjWd9eNX2hltCl18uWNbcr6UizVppy+bre/Eu3UCrSpaF+oZTS5ne12Q32pMEoWlFd34ypKvtFytu+t4lNvUmdcToUCRxV9RA0KLl3D1k5PKZXYD5ZqWu6yt+i0gus0XCvAkFeI7sJVAF4f/wr3do2ivl89AHT/d5XIRs1IOfoDAY6+pTv4XYcOHehV9ifvRTfs7/tF7d/1DQ3qhaA7X1RmZ2dHfuZV4zyAp4M7Kb9+b1J2/PBR7GrYUfB/RTGp+XoMOQXGeQBDbiEGu6KyZgFhZGRkoF7Kp5420DQ+HRRcuMq/UUGhDn1WPr9//AM1/rKiZMya3SwhUm4sLyow5kMVaFuUQJVRd7P2t+q7ZIclqp1b++H6gOn5c3fTbSUz3bp1M74ODg7m5MmT/PHHH9SsWbNSmWdFrqJQFIXp06eXuhrB3HTn/uLqnv8zdxjCChW6Kqh6Z9RCFRWDucOpHPX6dH30IrxhYx5/pD9vJ664Xq8ycdQ47u/dibFTnmfYwFicnJz4388/8d9vvmbR639fIHDgu2+Z9/YieneL5r97dvHRZx/z8ZoPjfWBdQP4em8ybVreh8benpoeNekQ9QAL3lnMus3raX1PKzZs2cSJ06eIbGJ6WOlmOt3fkftatKLf8IG8MXkGDeqHcvG3DLZ//SW9u/XinogWVbGlrI9yfbJRoHLnW/+tItl1RfKkf0suVe4PpzJ/5ZVbU5lV3CmGa4V3aU1l+0f3mSnp33YFgH1dV1weqHN9Tinrfzdk3bcamiyeV242W/YwalmZehl9K2UFVlbuWcl2pm+znPdZumGF2ikVGdItr+56LKa/NMpY/sb6G+pK/xuUs1wZ6y4VoqKQr9dxreASNWpqqKFx4KZu+tugnMrbHcmuwHKKYw2UfFtq+DgZy15/8w02f7YVADsfZ1r4tib5611MfWUqDz7aHVVVqR9cn/79+2Pn61y0kK3C+PHj+f5kCm8sehNXV1fmzZlH9GMPGWOZv2A+E16YSMKGtdSpU4fUM2eJfvwhpp6eypQ3XyUvL48nY59k8JDB/JjyI3Z+RX0r9rbYONsZ5wFsHGyxcaqBnX/RuX5f7NjOy1Nf5ulJo/n999/x9fXlgfsfoE54kLFNVW+723E3D0sY8vKocc0B3+fDcHC4xefSAtxyVLvMupuNvBYfvryNZf/RaPENL9Qbi0oUlLU+k/ZlrKecdatqZdvfvK2tmwZzUtTK3mTCymRnZ+Pu7k5WVpZc2SQsQl5eHqmpqQQFBVnFTuNOqFevHuPGjWPcuHHmDkVcJ59LYWkqs/+Wp2YLIYQQwqpJMiOEEEIIq1Zl58wIIURFpaWlmTsEIUQ1IiMzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqkswIIYQQwqpJMiOEqBLJyckoisKff/5p7lCEEP8ykswIISosNjYWRVFQFAU7OzuCg4OZOHEiOTk5t93fww8/XLVBCiH+deSmeUKISunevTuJiYkUFBTwzTffMHz4cHJychgwYIC5QxNC/EvJyIwQFkBVVfT6a2aZKvusWY1Gg6+vL1qtloEDBzJo0CA+/vjjUu2mT59OZGSkSdmiRYuoV6+esX7t2rV88sknxtGe5ORkAFJSUnjwwQdxdHTEy8uLp556iqtXr97GlhVC/BvIyIwQFsBgyCV5d1OzrLtD+xRsbZ1ue3lHR0cKCgoqvdzEiRM5deoU2dnZJCYmAuDp6cm1a9fo3r079913H4cPHyYzM5Phw4czevRo1qxZc9txCiGqL0lmhBC37dChQ6xfv55OnTpVelkXFxccHR3Jz8/H19fXWL527Vpyc3N57733cHZ2BmDp0qXExMQwe/ZsfHx8qix+IUT1IMmMEBbAxsaRDu1TzLbuyvjss89wcXGhsLCQgoICHnroId566y1OnjxZJfGcOnWKiIgIYyID0LZtWwwGA6dPn5ZkRghRiiQzQlgARVH+0aGeu6ljx44sX74cOzs7/P39sbOzAyiVzNjY2JQ6H6cih6NUVUVRlDLryisXQvy7yQnAQohKcXZ2JiQkhMDAQGMiUxZvb28yMjJMEprjx4+btLG3t0ev15uUNW7cmOPHj5tc7r1v3z5sbGxo0KBB1bwJIUS1IsmMEOKO6NChA7///jtz5szhzJkzLFu2jG3btpm0qVevHj/88AOnT5/m0qVLFBQUMGjQIBwcHBg6dCg//vgju3btYsyYMQwePFgOMQkhyiTJjBDijggLC+Ptt99m2bJlREREcOjQISZOnGjSZsSIETRs2JCWLVvi7e3Nvn37cHJy4ssvv+SPP/7g3nvv5dFHH6VTp04sXbrUTO9ECGHpFLWyN5mwMtnZ2bi7u5OVlYWbm5u5wxGCvLw8UlNTCQoKwsHBwdzhCAHI51JYnsrsv2VkRgghhBBWTZIZIYQQQlg1SWaEEEIIYdUkmRFCCCGEVZNkRgghhBBWTZIZIYQQQlg1SWaEEEIIYdUkmRFCCCGEVZNkRgghhBBWzazJzJ49e4iJicHf3x9FUfj4449N6mNjY1EUxWS67777zBOsEMJi1KtXj0WLFpk7jApJS0tDUZRSD9kUQlQdsyYzOTk5RERE3PSZK927d+fixYvG6YsvvriLEQohSir+gTFy5MhSdaNGjUJRFGJjY+94HIcPH+app56qsv6KE47iyd7enpCQEGbOnImlPfElPz+fMWPGUKtWLZydnenduzfnz583afPGG28QFRWFk5MTHh4e5glUiLuohjlX3qNHD3r06HHTNhqNBl9f37sUkRDiVrRaLRs3bmThwoU4OjoCRc/12bBhAwEBAXclBm9v7zvS786dO2nSpAn5+fns3buX4cOH4+fnx7Bhw+7I+m7HuHHj+M9//sPGjRvx8vJiwoQJ9OrViyNHjmBrawuATqejX79+tGnThtWrV5s5YiHuPIs/ZyY5OZnatWvToEEDRowYQWZm5k3b5+fnk52dbTIJYelUVSVHrzfLVNmRhxYtWhAQEMCWLVuMZVu2bEGr1dK8eXNj2fbt22nXrh0eHh54eXnRq1cvzpw5Y6zX6XSMHj0aPz8/HBwcqFevHvHx8cb66dOnExAQgEajwd/fn7FjxxrrSh5mevzxx3nsscdMYiwoKKBWrVokJiYat++cOXMIDg7G0dGRiIgINm/eXOq9eXl54evrS2BgIIMGDSIqKoqjR48a6w0GA6+99hp169ZFo9EQGRnJ9u3bTfo4dOgQzZs3x8HBgZYtW3Ls2DFjnaqqhISEMG/ePJNlfvzxR2xsbEy2T1mysrJYvXo18+fPp3PnzjRv3px169aRkpLCzp07je1mzJjB888/T9OmTW/anxDVhVlHZm6lR48e9OvXj8DAQFJTU5k2bRoPPvggR44cQaPRlLlMfHw8M2bMuMuRCvHPXDMYqL8nxSzrPvNAU5yv/6KvqCeffJLExEQGDRoEQEJCAnFxcSQnJxvb5OTkMH78eJo2bUpOTg6vvPIKjzzyCMePH8fGxoYlS5bw6aef8sEHHxAQEMC5c+c4d+4cAJs3b2bhwoVs3LiRJk2akJGRwffff19mLIMGDaJ///5cvXoVFxcXAL788ktycnLo27cvAFOnTmXLli0sX76c0NBQ9uzZwxNPPIG3tzft27cvs9/vvvuOo0ePMnToUGPZ4sWLmT9/PitWrKB58+YkJCTQu3dvTpw4QWhoKDk5OfTq1YsHH3yQdevWkZqaynPPPWdcXlEU4uLiSExMZOLEicbyhIQE7r//furXr3/T7X7kyBEKCgro2rWrsczf35/w8HD2799Pt27dbrq8ENWVRSczAwYMML4ODw+nZcuWBAYG8vnnn9OnT58yl5k8eTLjx483zmdnZ6PVau94rEL8mwwePJjJkycbzzXZt28fGzduNElmihOJYqtXr6Z27dqcPHmS8PBw0tPTCQ0NpV27diiKQmBgoLFteno6vr6+dO7cGTs7OwICAmjVqlWZsXTr1g1nZ2e2bt3K4MGDAVi/fj0xMTG4ubmRk5PDggUL+Prrr2nTpg0AwcHB7N27lxUrVpgkM1FRUdjY2KDT6SgoKOCpp55iyJAhxvp58+YxadIk40jQ7Nmz2bVrF4sWLWLZsmUkJSWh1+tJSEjAycmJJk2acP78eZ555hljH08++SSvvPIKhw4dolWrVhQUFLBu3Trmzp17y+2ekZGBvb09NWvWNCn38fEhIyPjlssLUV1ZdDJzIz8/PwIDA/n555/LbaPRaModtRHCUjnZ2HDmAfMcEnCyqfzR5lq1ahEdHc3atWtRVZXo6Ghq1apl0ubMmTNMmzaNgwcPcunSJQwGA1CUqISHhxMbG0uXLl1o2LAh3bt3p1evXsYRh379+rFo0SKCg4Pp3r07PXv2JCYmhho1Sn9l2dnZ0a9fP5KSkhg8eDA5OTl88sknrF+/HoCTJ0+Sl5dHly5dTJbT6XQmh8UANm3aRFhYGAUFBaSkpDB27Fhq1qzJm2++SXZ2NhcuXKBt27Ymy7Rt29Y4anTq1CkiIiJwcnIy1hcnUMX8/PyIjo4mISGBVq1a8dlnn5GXl0e/fv0qvP1vpKoqiqLc9vJCWDurSmYuX77MuXPn8PPzM3coQlQpRVEqfajH3OLi4hg9ejQAy5YtK1UfExODVqtl1apV+Pv7YzAYCA8PR6fTAUXn3qSmprJt2zZ27txJ//796dy5M5s3b0ar1XL69Gl27NjBzp07GTVqFHPnzmX37t3Y2dmVWtegQYNo3749mZmZ7NixAwcHB+PFBcVJ1Oeff06dOnVMlrvxh49WqyUkJASAsLAwzp49y7Rp05g+fbqxzY1JQ8lEoqLnHw0fPpzBgwezcOFCEhMTGTBggEkCVB5fX190Oh1XrlwxGZ3JzMwkKiqqQusWojoy6wnAV69e5fjx48b7L6SmpnL8+HHS09O5evUqEydO5MCBA6SlpZGcnExMTAy1atXikUceMWfYQgiKbpug0+nQ6XSlztW4fPkyp06dYurUqXTq1ImwsDCuXLlSqg83NzcGDBjAqlWr2LRpEx999BF//PEHAI6OjvTu3ZslS5aQnJzMgQMHSEkp+7yiqKgotFotmzZtIikpiX79+mFvbw9A48aN0Wg0pKenExISYjLd6hC0ra0thYWF6HQ63Nzc8Pf3Z+/evSZt9u/fT1hYmHFd33//Pbm5ucb6gwcPluq3Z8+eODs7s3z5crZt20ZcXNxN4yh2zz33YGdnx44dO4xlFy9e5Mcff5RkRvyrmXVk5rvvvqNjx47G+eJzXYYOHcry5ctJSUnhvffe488//8TPz4+OHTuyadMmXF1dzRWyEOI6W1tbTp06ZXxdUs2aNfHy8mLlypX4+fmRnp7OSy+9ZNJm4cKF+Pn5ERkZiY2NDR9++CG+vr54eHiwZs0a9Ho9rVu3xsnJiffffx9HR0eT82pKUhSFgQMH8s477/DTTz+xa9cuY52rqysTJ07k+eefx2Aw0K5dO7Kzs9m/fz8uLi4mJ/hevnyZjIwMCgsLSUlJYfHixXTs2BE3NzcAXnjhBV599VXq169PZGQkiYmJHD9+nKSkJAAGDhzIyy+/zLBhw5g6dSppaWmlrlwq3l6xsbFMnjyZkJCQUoeiyuPu7s6wYcOYMGECXl5eeHp6MnHiRJo2bUrnzp2N7dLT0/njjz9IT09Hr9cbfzCGhIQYT5IWolpRq7msrCwVULOysswdihCqqqpqbm6uevLkSTU3N9fcoVTa0KFD1Yceeqjc+oceekgdOnSoqqqqumPHDjUsLEzVaDRqs2bN1OTkZBVQt27dqqqqqq5cuVKNjIxUnZ2dVTc3N7VTp07q0aNHVVVV1a1bt6qtW7dW3dzcVGdnZ/W+++5Td+7caVxPYGCgunDhQpN1nzhxQgXUwMBA1WAwmNQZDAZ18eLFasOGDVU7OzvV29tb7datm7p7925VVVU1NTVVBYyTra2tWrduXXXEiBFqZmamsR+9Xq/OmDFDrVOnjmpnZ6dGRESo27ZtM1nXgQMH1IiICNXe3l6NjIxUP/roIxVQjx07ZtLuzJkzKqDOmTPnVpvdRG5urjp69GjV09NTdXR0VHv16qWmp6ebtBk6dKjJ+ymedu3addN+rfVzKaqnyuy/FVW1sNtbVrHs7Gzc3d3Jysoy/roSwpzy8vJITU0lKCgIBwcHc4cjzGTfvn106NCB8+fP4+PjY+5w5HMpLE5l9t9WdQKwEEJYu/z8fM6dO8e0adPo37+/RSQyQlg7i78DsBBCVCcbNmygYcOGZGVlMWfOHJO6pKQkXFxcypyaNGlipoiFsHwyMiOEEHdRbGxsuQ/j7N27N61bty6zrqxL0oUQRSSZEUIIC+Hq6ipXawpxG+QwkxBCCCGsmiQzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgirU69ePRYtWmTuMCokLS0NRVGMz0cSQlQ9SWaEEBUWGxuLoiiMHDmyVN2oUaNQFKXce6hUpcOHD/PUU09VWX/FCUfxZG9vT0hICDNnzsTSnviSn5/PmDFjqFWrFs7OzvTu3Zvz588b69PS0hg2bBhBQUE4OjpSv359Xn31VXQ6nRmjFuLOkmRGCFEpWq2WjRs3kpubayzLy8tjw4YNBAQE3JUYvL29cXJyqvJ+d+7cycWLF/n555+ZMWMGb7zxBgkJCVW+nn9i3LhxbN26lY0bN7J3716uXr1Kr1690Ov1APzvf//DYDCwYsUKTpw4wcKFC3nnnXeYMmWKmSMX4s6RZEYIC6CqKtd0hWaZKjvy0KJFCwICAtiyZYuxbMuWLWi1Wpo3b24s2759O+3atcPDwwMvLy969erFmTNnjPU6nY7Ro0fj5+eHg4MD9erVIz4+3lg/ffp0AgIC0Gg0+Pv7M3bsWGNdycNMjz/+OI899phJjAUFBdSqVYvExETj9p0zZw7BwcE4OjoSERHB5s2bS703Ly8vfH19CQwMZNCgQURFRXH06FFjvcFg4LXXXqNu3bpoNBoiIyPZvn27SR+HDh2iefPmODg40LJlS44dO2asU1WVkJAQ5s2bZ7LMjz/+iI2Njcn2KUtWVharV69m/vz5dO7cmebNm7Nu3TpSUlLYuXMnAN27dycxMZGuXbsSHBxM7969mThxosm/lxDVjdwBWAgLkFugp/ErX5pl3Sdf64aTfeW+Cp588kkSExMZNGgQAAkJCcTFxZGcnGxsk5OTw/jx42natCk5OTm88sorPPLIIxw/fhwbGxuWLFnCp59+ygcffEBAQADnzp3j3LlzAGzevJmFCxeyceNGmjRpQkZGBt9//32ZsQwaNIj+/ftz9epVXFxcAPjyyy/Jycmhb9++AEydOpUtW7awfPlyQkND2bNnD0888QTe3t60b9++zH6/++47jh49ytChQ41lixcvZv78+axYsYLmzZuTkJBA7969OXHiBKGhoeTk5NCrVy8efPBB1q1bR2pqKs8995xxeUVRiIuLIzExkYkTJxrLExISuP/++6lfv/5Nt/uRI0coKCiga9euxjJ/f3/Cw8PZv38/3bp1K3O5rKwsPD09b9q3ENZMkhkhRKUNHjyYyZMnG8812bdvHxs3bjRJZooTiWKrV6+mdu3anDx5kvDwcNLT0wkNDaVdu3YoikJgYKCxbXp6Or6+vnTu3Bk7OzsCAgJo1apVmbF069YNZ2dntm7dyuDBgwFYv349MTExuLm5kZOTw4IFC/j6669p06YNAMHBwezdu5cVK1aYJDNRUVHY2Nig0+koKCjgqaeeYsiQIcb6efPmMWnSJONI0OzZs9m1axeLFi1i2bJlJCUlodfrSUhIwMnJiSZNmnD+/HmeeeYZYx9PPvkkr7zyCocOHaJVq1YUFBSwbt065s6de8vtnpGRgb29PTVr1jQp9/HxISMjo8xlzpw5w1tvvcX8+fNv2b8Q1kqSGSEsgKOdLSdfK/tX9d1Yd2XVqlWL6Oho1q5di6qqREdHU6tWLZM2Z86cYdq0aRw8eJBLly5hMBiAokQlPDyc2NhYunTpQsOGDenevTu9evUyjjj069ePRYsWERwcTPfu3enZsycxMTHUqFH6K8vOzo5+/fqRlJTE4MGDycnJ4ZNPPmH9+vUAnDx5kry8PLp06WKynE6nMzksBrBp0ybCwsIoKCggJSWFsWPHUrNmTd58802ys7O5cOECbdu2NVmmbdu2xlGjU6dOERERYXI+T3ECVczPz4/o6GgSEhJo1aoVn332GXl5efTr16/C2/9GqqqiKEqp8gsXLtC9e3f69evH8OHDb7t/ISydJDNCWABFUSp9qMfc4uLiGD16NADLli0rVR8TE4NWq2XVqlX4+/tjMBgIDw83XlXTokULUlNT2bZtGzt37qR///507tyZzZs3o9VqOX36NDt27GDnzp2MGjWKuXPnsnv37jKfHj1o0CDat29PZmYmO3bswMHBgR49egAYk6jPP/+cOnXqmCyn0WhM5rVaLSEhIQCEhYVx9uxZpk2bxvTp041tbkwaSiYSFT3/aPjw4QwePJiFCxeSmJjIgAEDKnRCs6+vLzqdjitXrpiMzmRmZhIVFWXS9sKFC3Ts2JE2bdqwcuXKCsUlhLWSE4CFELele/fu6HQ6dDpdqXM1Ll++zKlTp5g6dSqdOnUiLCyMK1eulOrDzc2NAQMGsGrVKjZt2sRHH33EH3/8AYCjoyO9e/dmyZIlJCcnc+DAAVJSUsqMJSoqCq1Wy6ZNm0hKSqJfv37Y29sD0LhxYzQaDenp6YSEhJhMWq32pu/R1taWwsJCdDodbm5u+Pv7s3fvXpM2+/fvJywszLiu77//3uRKr4MHD5bqt2fPnjg7O7N8+XK2bdtGXFzcTeMods8992BnZ8eOHTuMZRcvXuTHH380SWb+7//+jw4dOtCiRQsSExOxsZGvelG9WddPQSGExbC1teXUqVPG1yXVrFkTLy8vVq5ciZ+fH+np6bz00ksmbRYuXIifnx+RkZHY2Njw4Ycf4uvri4eHB2vWrEGv19O6dWucnJx4//33cXR0NDmvpiRFURg4cCDvvPMOP/30E7t27TLWubq6MnHiRJ5//nkMBgPt2rUjOzub/fv34+LiYnKC7+XLl8nIyKCwsJCUlBQWL15Mx44dcXNzA+CFF17g1VdfpX79+kRGRpKYmMjx48dJSkoCYODAgbz88ssMGzaMqVOnkpaWVurKpeLtFRsby+TJkwkJCSl1KKo87u7uDBs2jAkTJuDl5YWnpycTJ06kadOmdO7cGSgakenQoQMBAQHMmzeP33//3bi8r69vhdYjhNVRq7msrCwVULOysswdihCqqqpqbm6uevLkSTU3N9fcoVTa0KFD1Yceeqjc+oceekgdOnSoqqqqumPHDjUsLEzVaDRqs2bN1OTkZBVQt27dqqqqqq5cuVKNjIxUnZ2dVTc3N7VTp07q0aNHVVVV1a1bt6qtW7dW3dzcVGdnZ/W+++5Td+7caVxPYGCgunDhQpN1nzhxQgXUwMBA1WAwmNQZDAZ18eLFasOGDVU7OzvV29tb7datm7p7925VVVU1NTVVBYyTra2tWrduXXXEiBFqZmamsR+9Xq/OmDFDrVOnjmpnZ6dGRESo27ZtM1nXgQMH1IiICNXe3l6NjIxUP/roIxVQjx07ZtLuzJkzKqDOmTPnVpvdRG5urjp69GjV09NTdXR0VHv16qWmp6cb6xMTE03eS8npVv1a6+dSVE+V2X8rqmpht7esYtnZ2bi7u5OVlWX8dSWEOeXl5ZGamkpQUBAODg7mDkeYyb59++jQoQPnz5/Hx8fH3OHI51JYnMrsv+UwkxBC3EX5+fmcO3eOadOm0b9/f4tIZISwdnJWmBBC3EUbNmygYcOGZGVlMWfOHJO6pKQkXFxcypyaNGlipoiFsHwyMiOEEHdRbGxsuQ/j7N27N61bty6zrqxL0oUQRSSZEUIIC+Hq6oqrq6u5wxDC6shhJiGEEEJYNUlmhBBCCGHVJJkRQgghhFWTZEYIIYQQVk2SGSGEEEJYNUlmhBBWp169eixatMjcYVRIWloaiqJw/Phxc4ciRLUlyYwQosJiY2NRFIWRI0eWqhs1ahSKopR7D5WqdPjwYZ566qkq66844Sie7O3tCQkJYebMmVjaE1/y8/MZM2YMtWrVwtnZmd69e3P+/HmTNr179yYgIAAHBwf8/PwYPHgwFy5cMFPEQtx5Zk1m9uzZQ0xMDP7+/iiKwscff2xSr6oq06dPx9/fH0dHRzp06MCJEyfME6wQAgCtVsvGjRvJzc01luXl5bFhwwYCAgLuSgze3t44OTlVeb87d+7k4sWL/Pzzz8yYMYM33niDhISEKl/PPzFu3Di2bt3Kxo0b2bt3L1evXqVXr17o9Xpjm44dO/LBBx9w+vRpPvroI86cOcOjjz5qxqiFuLPMmszk5OQQERHB0qVLy6yfM2cOCxYsYOnSpRw+fBhfX1+6dOnCX3/9dZcjFeIOU1XQ5ZhnquTIQ4sWLQgICGDLli3Gsi1btqDVamnevLmxbPv27bRr1w4PDw+8vLzo1asXZ86cMdbrdDpGjx6Nn58fDg4O1KtXj/j4eGP99OnTCQgIQKPR4O/vz9ixY411JQ8zPf744zz22GMmMRYUFFCrVi0SExOvb16VOXPmEBwcjKOjIxEREWzevLnUe/Py8sLX15fAwEAGDRpEVFQUR48eNdYbDAZee+016tati0ajITIyku3bt5v0cejQIZo3b46DgwMtW7bk2LFjxjpVVQkJCWHevHkmy/z444/Y2NiYbJ+yZGVlsXr1aubPn0/nzp1p3rw569atIyUlhZ07dxrbPf/889x3330EBgYSFRXFSy+9xMGDBykoKLhp/0JYK7PeAbhHjx706NGjzDpVVVm0aBEvv/wyffr0AWDt2rX4+Piwfv16nn766TKXy8/PJz8/3zifnZ1d9YELUdUKrsEsf/Ose8oFsHeu1CJPPvkkiYmJDBo0CICEhATi4uJITk42tsnJyWH8+PE0bdqUnJwcXnnlFR555BGOHz+OjY0NS5Ys4dNPP+WDDz4gICCAc+fOce7cOQA2b97MwoUL2bhxI02aNCEjI4Pvv/++zFgGDRpE//79uXr1Ki4uLgB8+eWX5OTk0LdvXwCmTp3Kli1bWL58OaGhoezZs4cnnngCb29v2rdvX2a/3333HUePHmXo0KHGssWLFzN//nxWrFhB8+bNSUhIoHfv3pw4cYLQ0FBycnLo1asXDz74IOvWrSM1NZXnnnvOuLyiKMTFxZGYmMjEiRON5QkJCdx///3Ur1//ptv9yJEjFBQU0LVrV2OZv78/4eHh7N+/n27dupVa5o8//iApKYmoqCh5JIKotiz2nJnU1FQyMjJM/mg1Gg3t27dn//795S4XHx+Pu7u7cdJqtXcjXCH+VQYPHszevXtJS0vj119/Zd++fTzxxBMmbfr27UufPn0IDQ0lMjKS1atXk5KSwsmTJwFIT08nNDSUdu3aERgYSLt27Xj88ceNdb6+vnTu3JmAgABatWrFiBEjyoylW7duODs7s3XrVmPZ+vXriYmJwc3NjZycHBYsWEBCQgLdunUjODiY2NhYnnjiCVasWGHSV1RUFC4uLtjb23PvvffSv39/hgwZYqyfN28ekyZN4rHHHqNhw4bMnj2byMhI4yhRUlISer2ehIQEmjRpQq9evXjhhRdM1vHkk09y+vRpDh06BBSNIq1bt464uLhbbveMjAzs7e2pWbOmSbmPjw8ZGRkmZZMmTcLZ2RkvLy/S09P55JNPbtm/ENbKYp/NVPyH6ePjY1Lu4+PDr7/+Wu5ykydPZvz48cb57OxsSWiE5bNzKhohMde6K6lWrVpER0ezdu1aVFUlOjqaWrVqmbQ5c+YM06ZN4+DBg1y6dAmDwQAUJSrh4eHExsbSpUsXGjZsSPfu3enVq5fxx0u/fv1YtGgRwcHBdO/enZ49exITE0ONGqW/suzs7OjXrx9JSUkMHjyYnJwcPvnkE9avXw/AyZMnycvLo0uXLibL6XQ6k8NiAJs2bSIsLIyCggJSUlIYO3YsNWvW5M033yQ7O5sLFy7Qtm1bk2Xatm1rHDU6deoUERERJufztGnTxqS9n58f0dHRJCQk0KpVKz777DPy8vLo169fhbf/jVRVRVEUk7IXXniBYcOG8euvvzJjxgyGDBnCZ599VqqdENWBxSYzxW78wyvrj7YkjUaDRqO502EJUbUUpdKHeswtLi6O0aNHA7Bs2bJS9TExMWi1WlatWoW/vz8Gg4Hw8HB0Oh1QdO5Namoq27ZtY+fOnfTv35/OnTuzefNmtFotp0+fZseOHezcuZNRo0Yxd+5cdu/eXeahkkGDBtG+fXsyMzPZsWMHDg4OxkPYxUnU559/Tp06dUyWu/G7QqvVEhISAkBYWBhnz55l2rRpTJ8+3djmZt9JFb3yafjw4QwePJiFCxeSmJjIgAEDKnRCs6+vLzqdjitXrpiMzmRmZhIVFWXStlatWtSqVYsGDRoQFhaGVqvl4MGDpZIrIaoDi01mfH19gaIRGj8/P2N5ZmZmqdEaczj25Wcc+uT6CYQlv8BKvDb5WrteXt6XnVqJPkq3KVleRn8l6m/5XXuTBiq3WPhm1bdY8S37/gcU/sEv0X/yK7aMRRUUHGt60uzRJ7hkZ4udrW3VdF2hMP/5L/K8q3+Rfy2HzLSztGjUgLzrVzQ1bxhKZtpZ8q/lkFfDlv8dO8KpU6d4c/qrNK0fBMC3330HwJ+ZGWT+etbYZ8f7WtHxvlZ0vr8djw2N5fT3x/D08ACgTURT2kQ05fGHHyKqU2f2fLWdZuHhGAoLufrHZX7/NRWA0Dp+1PHzY/Xyt/lvcjK9uncj6+L/AVDbxQmNvT0/Hj1C43o3XG1lKOT39DQu/1/Rpc1/XLzA754exurc7CwKCwu5cOZnXF1d8fXx4cvP/kNYiX72JO+ieUQEv6enofWtzXtrj5H+0/9wdHAAYMcXn5fq+97wxjg6ODBv1hts27aNTz/YxKX0NMr9N7peXM/HGzs7Oz7asJ6HY3oBkPFbJj/++CMvvzCBS+fKHrW+fP2y7Mxzv3KpbtnnZhUUFnL1ymW2fvAeur/KOdewnL+Hm36yyvsbKrevm/RWblc3i6B0XZnNyygsM5Z/tGwFY7lh2VLvr9TsrddVej03dnKLdZahZJuGbe4nvGOXm7S+syw2mQkKCsLX15cdO3YYh4J1Oh27d+9m9uzZZo4OdLm5XL18ydxhCCtkp9OBqqIaDKi3mSyZ684nqqqiqioGvR4F2LN9G1D0tWjQ6431bi4u1KzpwXvr1+Pt5cX/XbzAG3OLruBRDQYMhXpWJCTiU9ubJmFh2NjY8Olnn1Hb2xtXJyfWb/oAvV5Pi8gIHB0c2PTRR0X3TPHxQV9YiErRiIu+sNAY28MxvVizLomzaWlsXveesc7RwYGRw4cx7fXX0RcW0Oqelvx19SrfHT2Ks7MT/fv0wVBYdJXP5Uu/c/HCBQoL9Zz66TSrEhJpe999ODk4oC8o4Jnhw5i3eAkBdevQJCyMTZs/4seTp1g6fx76ggIe7tmTWXPmMW7iCzw3ahTn/+88b69YCYChsAB9iauJ+vd5hJmz51AvMIDmzZpSWIErjZwcHHi836O8+vpM3F1d8HD34LU33ySsYQPatmpFoU7Hse+/59j3P9Cq5T24u7uTnn6OOYsXUy8ggMjwphReHxm7UaFej6FQz58ZF7n2h3y3icqpHXTzk9fvNLMmM1evXuWXX34xzqempnL8+HE8PT0JCAhg3LhxzJo1i9DQUEJDQ5k1axZOTk4MHDjQjFEXCe/QmXrNShxvL7FTKi+jNZaXbGvaoJw+lLJfltPGWGyyHpMFy4yvYtW3Wrbyv6oq0eA2/IPd/j9ZtMyRqKIyXUEBmVeyqOlXB4frv94rtdI7lsmU7vjGEo2zM/YFhXjVLRqZ8Lqh3t7RCY2TM94B9Vi/LonnJ0ygY3QvGjRowML58+nctSuuXt541Q3Ap05d3lm5kl9++QVbW1ta3nMP//nPf/AOqEedoGDmzpvLjDdno9frCQ9vwsdbthDatBkAtra2OHt44FX37/Ph4p56iiXL3yEwIIAevR82+SzOmTefwOD6LFu5kokvT8PDw4PmkZFMevFFPOtoyS4sOhTVf0issX8/X196REfz2vTpeHp7A/DilJfR29jw+uy5ZP7+O2Fhjfho84e0bNsOAE/g461bGT12DF0fepiwsEa8+WY8Ax4fiHttHzzr1DVu2GdGj2HJ8neIixuGp3/dCv8rvbV0GS9NmcLIcePJzc2lY4cOrFn7Ht516wIqPn9cYceSt5i/dBk5OTn4+vrStUsXJk+ahF+dv0dlbvy3zc/P58/cfLo9M44aNqX/Fss9hHaTz2O5I67l9nWzkeHyKio5mlxW0U3+Xm+5qjIKy15vBWO5sfCG5Up3U1act+jjVrGV+Z5uvoyXNrDUMneToprx9pbJycl07NixVPnQoUNZs2YNqqoyY8YMVqxYwZUrV2jdujXLli0jPDy8wuvIzs7G3d2drKws3NzcqjJ8IW5LXl4eqampBAUFlZHMiH+Lffv20aFDB86fP28Rh87lcyksTWX232ZNZu4GSWaEpZGdxr9bfn4+586d46mnnsLPz4+kpCRzhwTI51JYnsrsvy32PjNCCFEdbdiwgYYNG5KVlcWcOXNM6pKSknBxcSlzatKkiZkiFsLyWewJwEIIUR3FxsaW+zDO3r1707p16zLr5O69QpRPkhkhhLAQrq6uuLq6mjsMIayOHGYSQgghhFWTZEYIIYQQVk2SGSGEEEJYNUlmhBBCCGHVJJkRQgghhFWTZEYIYXXq1avHokWLzB1GhaSlpaEoCsePHzd3KEJUW5LMCCEqLDY2FkVRGDlyZKm6UaNGoShKufdQqUqHDx/mqaeeqrL+ihOO4sne3p6QkBBmzpxZ/jOJzCQ/P58xY8ZQq1YtnJ2d6d27N+fPny+3bWRkpCRTotqTZEYIUSlarZaNGzeSm5trLMvLy2PDhg0EBATclRi8vb1xcnKq8n537tzJxYsX+fnnn5kxYwZvvPEGCQkJVb6ef2LcuHFs3bqVjRs3snfvXq5evUqvXr3Q6/Wl2r744ov4+/uX0YsQ1YskM0JYAFVVuVZwzSxTZUceWrRoQUBAAFu2bDGWbdmyBa1WS/Pmfz9Jfvv27bRr1w4PDw+8vLzo1asXZ86cMdbrdDpGjx6Nn58fDg4O1KtXj/j4eGP99OnTCQgIQKPR4O/vz9ixY411JQ8zPf744zz22GMmMRYUFFCrVi0SExON23fOnDkEBwfj6OhIREQEmzdvLvXevLy88PX1JTAwkEGDBhEVFcXRo0eN9QaDgddee426deui0WiIjIxk+/btJn0cOnSI5s2b4+DgQMuWLTl27JixTlVVQkJCmDdvnskyP/74IzY2NibbpyxZWVmsXr2a+fPn07lzZ5o3b866detISUlh586dJm23bdvGV199VWpdQlRHcgdgISxAbmEurdeXfRv7O+3bgd/iZFe5UY4nn3ySxMREBg0aBEBCQgJxcXEkJycb2+Tk5DB+/HiaNm1KTk4Or7zyCo888gjHjx/HxsaGJUuW8Omnn/LBBx8QEBDAuXPnOHfuHACbN29m4cKFbNy4kSZNmpCRkcH3339fZiyDBg2if//+XL16FRcXFwC+/PJLcnJy6Nu3LwBTp05ly5YtLF++nNDQUPbs2cMTTzyBt7c37du3L7Pf7777jqNHjzJ06FBj2eLFi5k/fz4rVqygefPmJCQk0Lt3b06cOEFoaCg5OTn06tWLBx98kHXr1pGamspzzz1nXF5RFOLi4khMTGTixInG8oSEBO6//37q169/0+1+5MgRCgoK6Nq1q7HM39+f8PBw9u/fT7du3QD47bffGDFiBB9//PEdGcESwtJIMiOEqLTBgwczefJk47km+/btY+PGjSbJTHEiUWz16tXUrl2bkydPEh4eTnp6OqGhobRr1w5FUQgMDDS2TU9Px9fXl86dO2NnZ0dAQACtWrUqM5Zu3brh7OzM1q1bGTx4MADr168nJiYGNzc3cnJyWLBgAV9//TVt2rQBIDg4mL1797JixQqTZCYqKgobGxt0Oh0FBQU89dRTDBkyxFg/b948Jk2aZBwJmj17Nrt27WLRokUsW7aMpKQk9Ho9CQkJODk50aRJE86fP88zzzxj7OPJJ5/klVde4dChQ7Rq1YqCggLWrVvH3Llzb7ndMzIysLe3p2bNmiblPj4+ZGRkAEWjP7GxsYwcOZKWLVuSlpZ2y36FsHaSzAhhARxrOPLtwG/Ntu7KqlWrFtHR0axduxZVVYmOjqZWrVombc6cOcO0adM4ePAgly5dwmAwAEWJSnh4OLGxsXTp0oWGDRvSvXt3evXqZRxx6NevH4sWLSI4OJju3bvTs2dPYmJiqFGj9FeWnZ0d/fr1IykpicGDB5OTk8Mnn3zC+vXrATh58iR5eXl06dLFZDmdTmdyWAxg06ZNhIWFUVBQQEpKCmPHjqVmzZq8+eabZGdnc+HCBdq2bWuyTNu2bY2jRqdOnSIiIsJkNKQ4gSrm5+dHdHQ0CQkJtGrVis8++4y8vDz69etX4e1/I1VVURQFgLfeeovs7GwmT5582/0JYW0kmRHCAiiKUulDPeYWFxfH6NGjAVi2bFmp+piYGLRaLatWrcLf3x+DwUB4eDg6nQ4oOvcmNTWVbdu2sXPnTvr370/nzp3ZvHkzWq2W06dPs2PHDnbu3MmoUaOYO3cuu3fvLvPp0YMGDaJ9+/ZkZmayY8cOHBwc6NGjB4Axifr888+pU6eOyXIajcZkXqvVEhISAkBYWBhnz55l2rRpTJ8+3dimOGkoVjKRqOj5R8OHD2fw4MEsXLiQxMREBgwYUKHDQb6+vuh0Oq5cuWIyOpOZmUlUVBQAX3/9NQcPHiz13lq2bMmgQYNYu3ZthWIUwprICcBCiNvSvXt3dDodOp3OeK5GscuXL3Pq1CmmTp1Kp06dCAsL48qVK6X6cHNzY8CAAaxatYpNmzbx0Ucf8ccffwDg6OhI7969WbJkCcnJyRw4cICUlJQyY4mKikKr1bJp0yaSkpLo168f9vb2ADRu3BiNRkN6ejohISEmk1arvel7tLW1pbCwEJ1Oh5ubG/7+/uzdu9ekzf79+wkLCzOu6/vvvze50uvgwYOl+u3ZsyfOzs4sX76cbdu2ERcXd9M4it1zzz3Y2dmxY8cOY9nFixf58ccfjcnMkiVL+P777zl+/DjHjx/niy++AIpGnd54440KrUcIayMjM0KI22Jra8upU6eMr0uqWbMmXl5erFy5Ej8/P9LT03nppZdM2ixcuBA/Pz8iIyOxsbHhww8/xNfXFw8PD9asWYNer6d169Y4OTnx/vvv4+joaHJeTUmKojBw4EDeeecdfvrpJ3bt2mWsc3V1ZeLEiTz//PMYDAbatWtHdnY2+/fvx8XFxeQE38uXL5ORkUFhYSEpKSksXryYjh074ubmBsALL7zAq6++Sv369YmMjCQxMZHjx4+TlJQEwMCBA3n55ZcZNmwYU6dOJS0trcyriWxtbYmNjWXy5MmEhISUOhRVHnd3d4YNG8aECRPw8vLC09OTiRMn0rRpUzp37gxQ6vL44pOi69evT926dSu0HiGsjSQzQojbVryTv5GNjQ0bN25k7NixhIeH07BhQ5YsWUKHDh2MbVxcXJg9ezY///wztra23HvvvXzxxRfY2Njg4eHBm2++yfjx49Hr9TRt2pT//Oc/eHl5lRvLoEGDmDVrFoGBgaXOa3n99depXbs28fHxnD17Fg8PD1q0aMGUKVNM2hUnBLa2tvj5+dGzZ0+T0YyxY8eSnZ3NhAkTyMzMpHHjxnz66aeEhoYa39N//vMfRo4cSfPmzWncuDGzZ88udTI0wLBhw5g1a1aFR2WKLVy4kBo1atC/f39yc3Pp1KkTa9asKZVQCvFvoqiWdnvLKpadnY27uztZWVnlfvEKcTfl5eWRmppKUFAQDg4O5g5HmMm+ffvo0KED58+fx8fHx9zhyOdSWJzK7L9lZEYIIe6i/Px8zp07x7Rp0+jfv79FJDJCWDs5AVgIIe6iDRs20LBhQ7KyspgzZ45JXVJSEi4uLmVOTZo0MVPEQlg+GZkRQoi7KDY2ttyHcfbu3ZvWrcu+E3RZl6QLIYpIMiOEEBbC1dUVV1dXc4chhNWRw0xCCCGEsGqSzAghhBDCqkkyI4QQQgirJsmMEEIIIayaJDNCCCGEsGqSzAghrE69evVYtGiRucOokLS0NBRF4fjx4+YORYhqS5IZIUSFxcbGoigKI0eOLFU3atQoFEUp9x4qVenw4cM89dRTVdZfccJRPNnb2xMSEsLMmTOxtCe+5OfnM2bMGGrVqoWzszO9e/fm/PnzJm3q1atn8n4URSn1oE8hqhNJZoQQlaLVatm4cSO5ubnGsry8PDZs2FDqic13ire3N05OTlXe786dO7l48SI///wzM2bM4I033iAhIaHK1/NPjBs3jq1bt7Jx40b27t3L1atX6dWrF3q93qTda6+9xsWLF43T1KlTzRSxEHeeRd80b/r06cyYMcOkzMfHh4yMDDNF9LdfT1zm7PHfzR2GsEI29gbcggq5eiUPnb1l/eq/lYJ8Pc3CI0j7NY2ktRvp3+8xAD7cvIk6/nWpF1iPgnw92Zdz2fnfr5i7YDanTp3ExtaWVi1b8easeQQHBQOg0+mYMm0Sn/7nY/7M+hOf2j7EDh3GhHEvABA/eybr1r9H5u+ZeNb05KHejzAnfj4ATZs34pmnRzNq5GjiRgxFVVUS333v7zgLCmjQJJjXp7/BEwOHoKoqi99aSMLad/nttwxC6ofywoSXeLj3IwD8dSUPAI2tM0527jjZQUz3PrS6910O7j9Ev4cHAmAwGJg7/03WvJfApcuXaNigIdOnvU7nTl2N6z5y9DDPTRjDTz+dJqxRYyaOfxGAq3/mkXXpGs1bNSVu6HCeGz3OuMzJUydo80Arjh1KMW6fsmRlZ7F69WpWvr2a1s2Lngz+zpJVhEU04NMtn9P5wS4AqAYVOxsHnO3cjcuq+fBXfm6Z/Rb9e+STl1PAof+cxaCT37nmYl3fCH8LbOxFcHNvs63fopMZgCZNmrBz507jvKU85v7Sub84+c0Fc4chrJCDuw1N/dzJv1Zo3Gmoqgr5eeYJSOOAoigVaqovMKAvVOnfZyDvrVtL7x59AVj7/hoG9B3E/oN70RcYyLtawJ9/ZPPUk6MIa9iEa9dymL1wFgMHD+DrL/ZiY2PD2yvf4ottn7Ny6Rrq+NflwsX/4/8u/B95Vwv4zxcfs+ydpaxYkkDDBo3I/D2TE6dSyLtaAIBqKEqs8q4W8HCvR3nq2Vgu/3YFZ2cXAL7673au5eTQrWM0eVcLmDX3Nb7Y/h9mvzafoKD6HPx2P0+NjMPNyYOo+9qRn1PUr+5aoXEdx384yvffH6Pfw48Zy955dxlvvb2EuW8spGmTCDZ88D6PPdGPPV99S3BQfXKu5dDv8b60a/MAS+evJP3cr7w8bZKx7/ycQh57dBDrkt7jqdhnjds1cc0a7rs3Cj9vLbnX11WWbw8epqCggKhWDxjbebh606hBY/bt3U/bVh2M22fh4vnMmRePv19dYqIf4tmnnsPe3r7cvgsKCynI0/Pzd3+Ql2Wo0OdBiGIOznaSzNxMjRo18PX1rXD7/Px88vPzjfPZ2dl3Iiz8QzxoFRN0R/oW1UO5+YGtnhpOuTi62aOx1wBgyM3lfPcOdy22kuomH8DG0aFCbWvY21DDzoahTw5h1twZXMq6iKIoHD7yLe+/l8ShI/upYW+Ds4eGAY/3N1l2RcOVBIVoSc84S5PGTfjt0kVCQkJ4sEsHFEWhUXiose3vf2Tg6+NDj17dsLOzoyEh3N8hyliv2IDGsQbOHhp69e6J04vO/Peb7Tz+2CAAPt32ET16ROOr9SYnJ4cVq5fx+afbad3qPgCaNGvE0R8OsX7zWrp074RTVtFOvtejXbGxsUGn01FQUMCTscN4cliscb3vrF7K+HETeGJI0Xoi7pnNgcP7SEhawcJ5i9n08fuoBgOrVr6Lk5MT97SK5HJWJuPGj8HR1R5nDw3DhsUxZ8EsTv3yAy3vuZeCggI++uQD3nhtFs7umptu/+yrf2Bvb0+dANPvRF8/H/7IumRc/tlRo4mIiKSme02+O3qYV197hYu/nWfZknfK7TtfB/ZZNWjWsS7oLeNHo7AefvXdb93oDrL4ZObnn3/G398fjUZD69atmTVrFsHB5Q/DxsfHlzo0dSf4hXjgF+Jxx9cjqp+8vDxSU1NxcrXHweF6MmOnv8VSd46zmz02TjffiRarYW+LrZ0NgcF1iI6O5sOtG1FVlejoaAKD62BrZ0MNe1uc3TWcOXOGadOmcfDgQS5duoTBUPRr/9KVDJzdWzDi6WF06dKFFq2a0b17d3r16kXXrkWHawYNeZy3VyylafMwunfvTs+ePYmJiaFGjaKvLMVGwd6xxvWdt4b+/fuxeesHDH86jpycHD7/4jPWr1+Ps7uGkz/9QF5eHr0fiTZ5LzqdjubNm+PsrsHJrej9b9q0ibCwMAoKCkhJSWHs2LHU9qnFm2++SXZ2NhcvXqBjp/YmScf9D7Tj+++/x9ldw9m0X4iIjMDbr6axvsOD9wMUJTPuGuq71yM6OpoNH66jfad2bN36Bfn5eTwxdCBOt/h30DgXPWzS2cO0nY2tgr1DDWP5pCkvGOtat2uJb53aPProo8xfOA8vL68y+7bNU9E41qBRh7o4OFQsuRXCUlh0MtO6dWvee+89GjRowG+//cbMmTOJiorixIkT5f5BTp48mfHjxxvns7Oz0Wq1dytkIW6L4uhIw6NHzLbu2xEXF8fo0aMBWLZsWan6mJgYtFotq1atwt/fH4PBQHh4ODqdDoAWLVqQmprKtm3b2LlzJ/3796dz585s3rwZrVbL6dOn2bFjBzt37mTUqFHMnTuX3bt3l/n06EGDBtG+fXsyMzPZsWMHDg4O9OjRA8CYRH3++efUqVPHZDmNxjQp0Gq1hISEABAWFsbZs2eZNm0a06dP/3t73TDkpqqqsayiVz4NHz6cwYMHs3DhQhITExkwYECFTmj29fVFp9Nx5coVatb8O2HKzMwkKiqq3OXuu69oROqXX34p97tTCGtm0clM8ZcRQNOmTWnTpg3169dn7dq1JglLSRqNptQXlBCWTlEUlDtwdc6d1L17d2Ni0q1bN5O6y5cvc+rUKVasWMH99xeNTOzdu7dUH25ubgwYMIABAwbw6KOP0r17d/744w88PT1xdHSkd+/e9O7dm2effZZGjRqRkpJCixYtSvUTFRWFVqtl06ZNbNu2jX79+hnPD2ncuDEajYb09HTat29fqfdoa2tLYWEhOp0ONzc3/P392bt3Lw888ICxzf79+2nVqpVxXe+//z65ubk4Xk8SDx48WKrfnj174uzszPLly9m2bRt79uypUDz33HMPdnZ27Nixg/79iw7jXbx4kR9//JE5c+aUu9yxY8cA8PPzq9gbF8LKWHQycyNnZ2eaNm3Kzz//bO5QhPjXs7W15dSpU8bXJdWsWRMvLy9WrlyJn58f6enppe5zsnDhQvz8/IiMjMTGxoYPP/wQX19fPDw8WLNmDXq9ntatW+Pk5MT777+Po6MjgYGBZcaiKAoDBw7knXfe4aeffmLXrl3GOldXVyZOnMjzzz+PwWCgXbt2ZGdns3//flxcXBg6dKix7eXLl8nIyKCwsJCUlBQWL15Mx44dcXNzA+CFF17g1VdfpX79+kRGRpKYmMjx48dJSkoCYODAgbz88ssMGzaMqVOnkpaWxrx588rcdrGxsUyePJmQkBDatGlToW3u7u7OsGHDmDBhAl5eXnh6ejJx4kSaNm1K586dAThw4AAHDx6kY8eOuLu7c/jwYZ5//nl69+591y6dF+KuU61IXl6eWqdOHXXGjBkVXiYrK0sF1KysrDsYmRAVl5ubq548eVLNzc01dyiVNnToUPWhhx4qt/6hhx5Shw4dqqqqqu7YsUMNCwtTNRqN2qxZMzU5OVkF1K1bt6qqqqorV65UIyMjVWdnZ9XNzU3t1KmTevToUVVVVXXr1q1q69atVTc3N9XZ2Vm977771J07dxrXExgYqC5cuNBk3SdOnFABNTAwUDUYDCZ1BoNBXbx4sdqwYUPVzs5O9fb2Vrt166bu3r1bVVVVTU1NVSm6KlYFVFtbW7Vu3brqiBEj1MzMTGM/er1enTFjhlqnTh3Vzs5OjYiIULdt22ayrgMHDqgRERGqvb29GhkZqX700UcqoB47dsyk3ZkzZ1RAnTNnzq02u4nc3Fx19OjRqqenp+ro6Kj26tVLTU9PN9YfOXJEbd26teru7q46ODioDRs2VF999VU1Jyfnlv1a6+dSVE+V2X8rqmpht7csYeLEicTExBAQEEBmZiYzZ85k9+7dpKSklPsL7UbZ2dm4u7uTlZVl/HUlhDkVnwAcFBQkJ1r+i+3bt48OHTpw/vx5fHx8zB2OfC6FxanM/tuiDzOdP3+exx9/nEuXLuHt7c19993HwYMHK5zICCGEpcnPz+fcuXNMmzaN/v37W0QiI4S1s+hkZuPGjeYOQQghqtSGDRsYNmwYkZGRvP/++yZ1SUlJPP3002UuFxgYyIkTJ+5GiEJYHYtOZoQQorqJjY0t92GcvXv3pnXr1mXWlXVJuhCiiCQzQghhIVxdXXF1dTV3GEJYHXmamBBmYsHn3ot/Ifk8CmsmyYwQd1nx4YJr166ZORIh/lb8eZTDWcIayWEmIe4yW1tbPDw8yMzMBMDJyanCT60Woqqpqsq1a9fIzMzEw8Oj1A0QhbAGkswIYQbFT4IvTmiEMDcPDw/j51IIayPJjBBmoCgKfn5+1K5dm4KCAnOHI/7l7OzsZERGWDVJZoQwI1tbW9mJCCHEPyQnAAshhBDCqkkyI4QQQgirJsmMEEIIIayaJDNCCCGEsGqSzAghhBDCqkkyI4QQQgirJsmMEEIIIayaJDNCCCGEsGqSzAghhBDCqkkyI4QQQgirJsmMEEIIIayaJDNCCCGEsGqSzAghhBDCqslTs2/TH++9z6UVK8wdhhBCCGF2noMHU2vk02ZbvyQzt8mQl4f+8mVzhyGEEEKYneHaNbOuX5KZ22Tr6YlT69bmDkMIAaCYO4B/AdXcAQhLVqN2bfOu36xrt2L6P/7g2rffmjsMIYQQwuwcIyLMun5JZm7T783q8n+jeps7DCGEEMLsCiPqYc6xGUlmbtMau7/YVM/b3GEIIYQQZtfLIYelZly/JDO3KdfWnzzXBuYOQwghhDC7nBrZZl2/JDO3yeWCG057fjV3GEIIIYTZubWuC/eYb/2SzNymuvY1MMg9B4UQQgjq2Js3nZBk5jbVr3WCIfftMHcYQgghhNmF1b4faGq29Usyc5v2ff8hWzll7jCEEEIIs8u7/H/E3DvYbOu3imTm7bffZu7cuVy8eJEmTZqwaNEi7r//frPGVMcugOjfJZkRQggh6ngEmnX9iqqqFn1fx02bNjF48GDefvtt2rZty4oVK3j33Xc5efIkAQEBt1w+Ozsbd3d3srKycHNzuwsRCyGEEOKfqsz+2+KTmdatW9OiRQuWL19uLAsLC+Phhx8mPj6+VPv8/Hzy8/ON89nZ2Wi1WklmhBBCCCtSmWTGoi/H0el0HDlyhK5du5qUd+3alf3795e5THx8PO7u7sZJq9XejVCFEEIIYSYWncxcunQJvV6Pj4+PSbmPjw8ZGRllLjN58mSysrKM07lz5+5GqEIIIYQwE6s4AVhRTB+Jq6pqqbJiGo0GjUZzN8ISQgghhAWw6JGZWrVqYWtrW2oUJjMzs9RojRBCCCH+nSw6mbG3t+eee+5hxw7Tm9Pt2LGDqKgoM0UlhBBCCEti8YeZxo8fz+DBg2nZsiVt2rRh5cqVpKenM3LkSHOHJoQQQggLYPHJzIABA7h8+TKvvfYaFy9eJDw8nC+++ILAQPPeoEcIIYQQlsHi7zPzT8lN84QQQgjrU23uMyOEEEIIcSuSzAghhBDCqkkyI4QQQgirJsmMEEIIIayaJDNCCCGEsGoWf2n2P1V8sVZ2draZIxFCCCFERRXvtyty0XW1T2b++usvAHl6thBCCGGF/vrrL9zd3W/aptrfZ8ZgMHDhwgVcXV3LfTjl7crOzkar1XLu3Dm5h80dJNv57pDtfHfIdr47ZDvfHXdyO6uqyl9//YW/vz82Njc/K6baj8zY2NhQt27dO7oONzc3+WO5C2Q73x2yne8O2c53h2znu+NObedbjcgUkxOAhRBCCGHVJJkRQgghhFWTZOYf0Gg0vPrqq2g0GnOHUq3Jdr47ZDvfHbKd7w7ZzneHpWznan8CsBBCCCGqNxmZEUIIIYRVk2RGCCGEEFZNkhkhhBBCWDVJZoQQQghh1SSZuU1vv/02QUFBODg4cM899/DNN9+YO6RqJT4+nnvvvRdXV1dq167Nww8/zOnTp80dVrUXHx+PoiiMGzfO3KFUS//3f//HE088gZeXF05OTkRGRnLkyBFzh1WtFBYWMnXqVIKCgnB0dCQ4OJjXXnsNg8Fg7tCs2p49e4iJicHf3x9FUfj4449N6lVVZfr06fj7++Po6EiHDh04ceLEXYtPkpnbsGnTJsaNG8fLL7/MsWPHuP/+++nRowfp6enmDq3a2L17N88++ywHDx5kx44dFBYW0rVrV3JycswdWrV1+PBhVq5cSbNmzcwdSrV05coV2rZti52dHdu2bePkyZPMnz8fDw8Pc4dWrcyePZt33nmHpUuXcurUKebMmcPcuXN56623zB2aVcvJySEiIoKlS5eWWT9nzhwWLFjA0qVLOXz4ML6+vnTp0sX4fMQ7ThWV1qpVK3XkyJEmZY0aNVJfeuklM0VU/WVmZqqAunv3bnOHUi399ddfamhoqLpjxw61ffv26nPPPWfukKqdSZMmqe3atTN3GNVedHS0GhcXZ1LWp08f9YknnjBTRNUPoG7dutU4bzAYVF9fX/XNN980luXl5anu7u7qO++8c1dikpGZStLpdBw5coSuXbualHft2pX9+/ebKarqLysrCwBPT08zR1I9Pfvss0RHR9O5c2dzh1Jtffrpp7Rs2ZJ+/fpRu3ZtmjdvzqpVq8wdVrXTrl07/vvf//LTTz8B8P3337N371569uxp5siqr9TUVDIyMkz2ixqNhvbt29+1/WK1f9BkVbt06RJ6vR4fHx+Tch8fHzIyMswUVfWmqirjx4+nXbt2hIeHmzucamfjxo0cPXqUw4cPmzuUau3s2bMsX76c8ePHM2XKFA4dOsTYsWPRaDQMGTLE3OFVG5MmTSIrK4tGjRpha2uLXq/njTfe4PHHHzd3aNVW8b6vrP3ir7/+eldikGTmNimKYjKvqmqpMlE1Ro8ezQ8//MDevXvNHUq1c+7cOZ577jm++uorHBwczB1OtWYwGGjZsiWzZs0CoHnz5pw4cYLly5dLMlOFNm3axLp161i/fj1NmjTh+PHjjBs3Dn9/f4YOHWru8Ko1c+4XJZmppFq1amFra1tqFCYzM7NUVir+uTFjxvDpp5+yZ88e6tata+5wqp0jR46QmZnJPffcYyzT6/Xs2bOHpUuXkp+fj62trRkjrD78/Pxo3LixSVlYWBgfffSRmSKqnl544QVeeuklHnvsMQCaNm3Kr7/+Snx8vCQzd4ivry9QNELj5+dnLL+b+0U5Z6aS7O3tueeee9ixY4dJ+Y4dO4iKijJTVNWPqqqMHj2aLVu28PXXXxMUFGTukKqlTp06kZKSwvHjx41Ty5YtGTRoEMePH5dEpgq1bdu21O0FfvrpJwIDA80UUfV07do1bGxMd222trZyafYdFBQUhK+vr8l+UafTsXv37ru2X5SRmdswfvx4Bg8eTMuWLWnTpg0rV64kPT2dkSNHmju0auPZZ59l/fr1fPLJJ7i6uhpHwtzd3XF0dDRzdNWHq6trqfOQnJ2d8fLykvOTqtjzzz9PVFQUs2bNon///hw6dIiVK1eycuVKc4dWrcTExPDGG28QEBBAkyZNOHbsGAsWLCAuLs7coVm1q1ev8ssvvxjnU1NTOX78OJ6engQEBDBu3DhmzZpFaGgooaGhzJo1CycnJwYOHHh3Arwr10xVQ8uWLVMDAwNVe3t7tUWLFnLJcBUDypwSExPNHVq1J5dm3zn/+c9/1PDwcFWj0aiNGjVSV65cae6Qqp3s7Gz1ueeeUwMCAlQHBwc1ODhYffnll9X8/Hxzh2bVdu3aVeZ38tChQ1VVLbo8+9VXX1V9fX1VjUajPvDAA2pKSspdi09RVVW9O2mTEEIIIUTVk3NmhBBCCGHVJJkRQgghhFWTZEYIIYQQVk2SGSGEEEJYNUlmhBBCCGHVJJkRQgghhFWTZEYIIYQQVk2SGSGEEEJYNUlmhBAWKTk5GUVR+PPPP80dihDCwskdgIUQFqFDhw5ERkayaNEioOhBdX/88Qc+Pj4oimLe4IQQFk0eNCmEsEj29vb4+vqaOwwhhBWQw0xCCLOLjY1l9+7dLF68GEVRUBSFNWvWmBxmWrNmDR4eHnz22Wc0bNgQJycnHn30UXJycli7di316tWjZs2ajBkzBr1eb+xbp9Px4osvUqdOHZydnWndujXJycnmeaNCiDtCRmaEEGa3ePFifvrpJ8LDw3nttdcAOHHiRKl2165dY8mSJWzcuJG//vqLPn360KdPHzw8PPjiiy84e/Ysffv2pV27dgwYMACAJ598krS0NDZu3Ii/vz9bt26le/fupKSkEBoaelffpxDizpBkRghhdu7u7tjb2+Pk5GQ8tPS///2vVLuCggKWL19O/fr1AXj00Ud5//33+e2333BxcaFx48Z07NiRXbt2MWDAAM6cOcOGDRs4f/48/v7+AEycOJHt27eTmJjIrFmz7t6bFELcMZLMCCGshpOTkzGRAfDx8aFevXq4uLiYlGVmZgJw9OhRVFWlQYMGJv3k5+fj5eV1d4IWQtxxkswIIayGnZ2dybyiKGWWGQwGAAwGA7a2thw5cgRbW1uTdiUTICGEdZNkRghhEezt7U1O3K0KzZs3R6/Xk5mZyf3331+lfQshLIdczSSEsAj16tXj22+/JS0tjUuXLhlHV/6JBg0aMGjQIIYMGcKWLVtITU3l8OHDzJ49my+++KIKohZCWAJJZoQQFmHixInY2trSuHFjvL29SU9Pr5J+ExMTGTJkCBMmTKBhw4b07t2bb7/9Fq1WWyX9CyHMT+4ALIQQQgirJiMzQgghhLBqkswIIYQQwqpJMiOEEEIIqybJjBBCCCGsmiQzQgghhLBqkswIIcT/t1sHJAAAAACC/r9uR6ArBNZkBgBYkxkAYE1mAIA1mQEA1mQGAFgLiLFkSit7X7AAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sim.data.where(sim.data['particle_type'] == 'Massive Body',drop=True)['a'].plot(hue=\"name\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f46b22e-dbd0-4562-a4cf-ea55247a2126", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (My debug_env Kernel)", + "language": "python", + "name": "debug_env" + }, + "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.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/Basic_Simulation/tp.in b/examples/Basic_Simulation/tp.in index 162f8c0af..9e3c6b9bc 100644 --- a/examples/Basic_Simulation/tp.in +++ b/examples/Basic_Simulation/tp.in @@ -1,40 +1,31 @@ -!! Copyright 2022 - David Minton, Carlisle Wishard, Jennifer Pouplin, Jake Elliott, & Dana Singh -!! This file is part of Swiftest. -!! Swiftest is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License -!! as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -!! Swiftest is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -!! of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -!! You should have received a copy of the GNU General Public License along with Swiftest. -!! If not, see: https://www.gnu.org/licenses. - 10 TestParticle_01 -1.0288790290699558749 0.088535376967516773994 39.679062610233010844 -51.107327480220099858 20.90477961082723013 183.92936958121950397 +0.5697767200549144 0.11834332057809717 80.87229166892182 +222.5373287698991 111.76944690551065 270.3754252090885 TestParticle_02 -0.54831541717225318333 0.20048853000030755767 32.784952066779297297 -66.59436607119042151 196.2609764918545352 76.40623742948525887 +1.0092273242276735 0.05399051523964523 82.35601343884242 +117.598050656921 116.26893959032198 208.13367102381633 TestParticle_03 -0.9815578696795972391 0.25717438840188583393 38.959194313128776344 -158.31201212846775661 93.86793546512863884 126.96040079919036714 +0.7590606700221183 0.027936243833100036 88.12926503625694 +188.5414923780248 169.44578516513008 282.4333664924941 TestParticle_04 -0.70510786000431613374 0.068740260615181125736 39.981235917453354034 -26.668314027440779057 12.507089902982141183 53.606668142734086757 +0.9972940668463142 0.2569338531459909 22.75744046640894 +199.388648818177 152.8772634782716 307.54746324357103 TestParticle_05 -1.336737159289705712 0.25044351028750111432 74.189544264066626056 -313.1647935522670423 152.39951433565357775 322.52271715518395467 +0.341620427452812 0.052524493325270837 18.839916665085173 +327.2732232808312 334.4396604858765 359.4553699087638 TestParticle_06 -0.64018428687513928566 0.1478972702874425671 73.39555666508663023 -74.03379498826825511 124.22185942531125136 106.36095293685497154 +1.1670239341669624 0.07778574626824612 22.716135789279228 +196.30730270195494 154.1907148406917 159.6860762761786 TestParticle_07 -1.2738161048947760356 0.17129911220230967239 78.723790909435408025 -111.971184037421835455 315.6294407604535195 108.74538288631850946 +1.0954005977667731 0.10128096358705894 9.878980088486013 +101.85525076444998 124.8686145955563 254.7757898831765 TestParticle_08 -0.5196197141250760154 0.26607581023277782073 79.221465395061358095 -240.07768052067768849 177.4793327047061382 189.85775920180086018 +0.39125485873865873 0.17327367123519463 22.276414655828646 +105.99598258123197 235.2803528505754 348.706620365582 TestParticle_09 -0.7160151851892424535 0.29584187625470087513 58.1214116614385361 -24.22606869850931588 338.4678256522021229 136.32070882113120547 +0.48720268138208256 0.29707768397862033 39.717820434869466 +307.9106242922398 221.91704762104942 333.56626647648994 TestParticle_10 -1.0782431797945351004 0.11096856177737297877 54.729767236739554903 -280.85362091874827684 116.780853088052467115 286.99855363222661708 +0.6458552755486036 0.07522818043309405 45.225887389492826 +74.40348173261458 92.77221707157337 260.30279498657796 diff --git a/python/swiftest/swiftest/init_cond.py b/python/swiftest/swiftest/init_cond.py index 287e5ecae..ea28bcb8c 100644 --- a/python/swiftest/swiftest/init_cond.py +++ b/python/swiftest/swiftest/init_cond.py @@ -8,17 +8,26 @@ You should have received a copy of the GNU General Public License along with Swiftest. If not, see: https://www.gnu.org/licenses. """ +from __future__ import annotations import swiftest import numpy as np +import numpy.typing as npt from astroquery.jplhorizons import Horizons import astropy.units as u from astropy.coordinates import SkyCoord import datetime -from datetime import date import xarray as xr - -def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): +from typing import ( + Literal, + Dict, + List, + Any +) +def solar_system_horizons(plname: str, + param: Dict, + ephemerides_start_date: str, + idval: int | None = None): """ Initializes a Swiftest dataset containing the major planets of the Solar System at a particular data from JPL/Horizons @@ -28,8 +37,6 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): Swiftest paramuration parameters. This method uses the unit conversion factors to convert from JPL's AU-day system into the system specified in the param file ephemerides_start_date : string Date to use when obtaining the ephemerides in the format YYYY-MM-DD. - ds : xarray Dataset - Dataset to append Returns ------- @@ -120,10 +127,10 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): THIRDLONG = np.longdouble(1.0) / np.longdouble(3.0) # Central body value vectors - GMcb = np.array([swiftest.GMSun * param['TU2S'] ** 2 / param['DU2M'] ** 3]) - Rcb = np.array([swiftest.RSun / param['DU2M']]) - J2RP2 = np.array([swiftest.J2Sun * (swiftest.RSun / param['DU2M']) ** 2]) - J4RP4 = np.array([swiftest.J4Sun * (swiftest.RSun / param['DU2M']) ** 4]) + GMcb = swiftest.GMSun * param['TU2S'] ** 2 / param['DU2M'] ** 3 + Rcb = swiftest.RSun / param['DU2M'] + J2RP2 = swiftest.J2Sun * (swiftest.RSun / param['DU2M']) ** 2 + J4RP4 = swiftest.J4Sun * (swiftest.RSun / param['DU2M']) ** 4 solarpole = SkyCoord(ra=286.13 * u.degree, dec=63.87 * u.degree) solarrot = planetrot['Sun'] * param['TU2S'] @@ -146,13 +153,13 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): Rpl = Rcb J2 = J2RP2 J4 = J4RP4 - if param['ROTATION'] == 'YES': - Ip1 = [Ipsun[0]] - Ip2 = [Ipsun[1]] - Ip3 = [Ipsun[2]] - rotx = [rotcb.x] - roty = [rotcb.y] - rotz = [rotcb.z] + if param['ROTATION']: + Ip1 = Ipsun[0] + Ip2 = Ipsun[1] + Ip3 = Ipsun[2] + rotx = rotcb.x.value + roty = rotcb.y.value + rotz = rotcb.z.value else: Ip1 = None Ip2 = None @@ -170,12 +177,6 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): ephemerides_end_date = tend.isoformat() ephemerides_step = '1d' - v1 = [] - v2 = [] - v3 = [] - v4 = [] - v5 = [] - v6 = [] J2 = None J4 = None @@ -185,42 +186,33 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): 'step': ephemerides_step}) if param['IN_FORM'] == 'XV': - v1.append(pldata[plname].vectors()['x'][0] * DCONV) - v2.append(pldata[plname].vectors()['y'][0] * DCONV) - v3.append(pldata[plname].vectors()['z'][0] * DCONV) - v4.append(pldata[plname].vectors()['vx'][0] * VCONV) - v5.append(pldata[plname].vectors()['vy'][0] * VCONV) - v6.append(pldata[plname].vectors()['vz'][0] * VCONV) + v1 = pldata[plname].vectors()['x'][0] * DCONV + v2 = pldata[plname].vectors()['y'][0] * DCONV + v3 = pldata[plname].vectors()['z'][0] * DCONV + v4 = pldata[plname].vectors()['vx'][0] * VCONV + v5 = pldata[plname].vectors()['vy'][0] * VCONV + v6 = pldata[plname].vectors()['vz'][0] * VCONV elif param['IN_FORM'] == 'EL': - v1.append(pldata[plname].elements()['a'][0] * DCONV) - v2.append(pldata[plname].elements()['e'][0]) - v3.append(pldata[plname].elements()['incl'][0]) - v4.append(pldata[plname].elements()['Omega'][0]) - v5.append(pldata[plname].elements()['w'][0]) - v6.append(pldata[plname].elements()['M'][0]) + v1 = pldata[plname].elements()['a'][0] * DCONV + v2 = pldata[plname].elements()['e'][0] + v3 = pldata[plname].elements()['incl'][0] + v4 = pldata[plname].elements()['Omega'][0] + v5 = pldata[plname].elements()['w'][0] + v6 = pldata[plname].elements()['M'][0] if ispl: - GMpl = [] - GMpl.append(GMcb[0] / MSun_over_Mpl[plname]) - if param['CHK_CLOSE'] == 'YES': - Rpl = [] - Rpl.append(planetradius[plname] * DCONV) + GMpl = GMcb / MSun_over_Mpl[plname] + if param['CHK_CLOSE']: + Rpl = planetradius[plname] * DCONV else: Rpl = None # Generate planet value vectors - if (param['RHILL_PRESENT'] == 'YES'): - rhill = [] - rhill.append(pldata[plname].elements()['a'][0] * DCONV * (3 * MSun_over_Mpl[plname]) ** (-THIRDLONG)) + if (param['RHILL_PRESENT']): + rhill = pldata[plname].elements()['a'][0] * DCONV * (3 * MSun_over_Mpl[plname]) ** (-THIRDLONG) else: rhill = None - if (param['ROTATION'] == 'YES'): - Ip1 = [] - Ip2 = [] - Ip3 = [] - rotx = [] - roty = [] - rotz = [] + if (param['ROTATION']): RA = pldata[plname].ephemerides()['NPole_RA'][0] DEC = pldata[plname].ephemerides()['NPole_DEC'][0] @@ -228,12 +220,12 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): rotrate = planetrot[plname] * param['TU2S'] rot = rotpole.cartesian * rotrate Ip = np.array([0.0, 0.0, planetIpz[plname]]) - Ip1.append(Ip[0]) - Ip2.append(Ip[1]) - Ip3.append(Ip[2]) - rotx.append(rot.x) - roty.append(rot.y) - rotz.append(rot.z) + Ip1 = Ip[0] + Ip2 = Ip[1] + Ip3 = Ip[2] + rotx = rot.x.value + roty = rot.y.value + rotz = rot.z.value else: Ip1 = None Ip2 = None @@ -245,13 +237,33 @@ def solar_system_horizons(plname, idval, param, ephemerides_start_date, ds): GMpl = None if idval is None: - plid = np.array([planetid[plname]], dtype=int) + plid = planetid[plname] else: - plid = np.array([idval], dtype=int) + plid = idval - return plid,[plname],v1,v2,v3,v4,v5,v6,GMpl,Rpl,rhill,Ip1,Ip2,Ip3,rotx,roty,rotz,J2,J4 + return plname,v1,v2,v3,v4,v5,v6,idval,GMpl,Rpl,rhill,Ip1,Ip2,Ip3,rotx,roty,rotz,J2,J4 -def vec2xr(param, idvals, namevals, v1, v2, v3, v4, v5, v6, GMpl=None, Rpl=None, rhill=None, Ip1=None, Ip2=None, Ip3=None, rotx=None, roty=None, rotz=None, J2=None, J4=None,t=0.0): +def vec2xr(param: Dict, + namevals: npt.NDArray[np.str_], + v1: npt.NDArray[np.float_], + v2: npt.NDArray[np.float_], + v3: npt.NDArray[np.float_], + v4: npt.NDArray[np.float_], + v5: npt.NDArray[np.float_], + v6: npt.NDArray[np.float_], + idvals: npt.NDArray[np.int_], + GMpl: npt.NDArray[np.float_] | None=None, + Rpl: npt.NDArray[np.float_] | None=None, + rhill: npt.NDArray[np.float_] | None=None, + Ip1: npt.NDArray[np.float_] | None=None, + Ip2: npt.NDArray[np.float_] | None=None, + Ip3: npt.NDArray[np.float_] | None=None, + rotx: npt.NDArray[np.float_] | None=None, + roty: npt.NDArray[np.float_] | None=None, + rotz: npt.NDArray[np.float_] | None=None, + J2: npt.NDArray[np.float_] | None=None, + J4: npt.NDArray[np.float_] | None=None, + t: float=0.0): """ Converts and stores the variables of all bodies in an xarray dataset. @@ -299,12 +311,7 @@ def vec2xr(param, idvals, namevals, v1, v2, v3, v4, v5, v6, GMpl=None, Rpl=None, ------- ds : xarray dataset """ - if v1 is None: # This is the central body - iscb = True - else: - iscb = False - - if param['ROTATION'] == 'YES': + if param['ROTATION']: if Ip1 is None: Ip1 = np.full_like(v1, 0.4) if Ip2 is None: @@ -320,15 +327,28 @@ def vec2xr(param, idvals, namevals, v1, v2, v3, v4, v5, v6, GMpl=None, Rpl=None, dims = ['time', 'id', 'vec'] infodims = ['id', 'vec'] - if not iscb and GMpl is not None: - ispl = True + + # The central body is always given id 0 + + if GMpl is not None: + icb = (~np.isnan(GMpl)) & (idvals == 0) + ipl = (~np.isnan(GMpl)) & (idvals != 0) + itp = (np.isnan(GMpl)) & (idvals != 0) + iscb = any(icb) + ispl = any(ipl) + istp = any(itp) else: + icb = np.full_like(idvals,False) + ipl = np.full_like(idvals,False) + itp = idvals != 0 + iscb = False ispl = False - - if ispl and param['CHK_CLOSE'] == 'YES' and Rpl is None: + istp = any(itp) + + if ispl and param['CHK_CLOSE'] and Rpl is None: print("Massive bodies need a radius value.") return None - if ispl and rhill is None and param['RHILL_PRESENT'] == 'YES': + if ispl and rhill is None and param['RHILL_PRESENT']: print("rhill is required.") return None @@ -337,84 +357,47 @@ def vec2xr(param, idvals, namevals, v1, v2, v3, v4, v5, v6, GMpl=None, Rpl=None, param['OUT_FORM'] = param['IN_FORM'] clab, plab, tlab, infolab_float, infolab_int, infolab_str = swiftest.io.make_swiftest_labels(param) param['OUT_FORM'] = old_out_form - vec_str = np.vstack([namevals]) - label_str = ["name"] + particle_type = np.empty_like(namevals) + vec = np.vstack([v1,v2,v3,v4,v5,v6]) + if iscb: - label_float = clab.copy() - vec_float = np.vstack([GMpl,Rpl,J2,J4]) - if param['ROTATION'] == 'YES': - vec_float = np.vstack([vec_float, Ip1, Ip2, Ip3, rotx, roty, rotz]) - particle_type = "Central Body" + lab_cb = clab.copy() + vec_cb = np.vstack([GMpl[icb],Rpl[icb],J2[icb],J4[icb]]) + if param['ROTATION']: + vec_cb = np.vstack([vec_cb, Ip1[icb], Ip2[icb], Ip3[icb], rotx[icb], roty[icb], rotz[icb]]) + particle_type[icb] = "Central Body" + vec_cb = np.expand_dims(vec_cb.T,axis=0) # Make way for the time dimension! + ds_cb = xr.DataArray(vec_cb, dims=dims, coords={'time': [t], 'id': idvals[icb], 'vec': lab_cb}).to_dataset(dim='vec') else: - vec_float = np.vstack([v1, v2, v3, v4, v5, v6]) - if ispl: - label_float = plab.copy() - vec_float = np.vstack([vec_float, GMpl]) - if param['CHK_CLOSE'] == 'YES': - vec_float = np.vstack([vec_float, Rpl]) - if param['RHILL_PRESENT'] == 'YES': - vec_float = np.vstack([vec_float, rhill]) - if param['ROTATION'] == 'YES': - vec_float = np.vstack([vec_float, Ip1, Ip2, Ip3, rotx, roty, rotz]) - particle_type = np.repeat("Massive Body",idvals.size) - else: - label_float = tlab.copy() - particle_type = np.repeat("Test Particle",idvals.size) - origin_type = np.repeat("User Added Body",idvals.size) - origin_time = np.full_like(v1,t) - collision_id = np.full_like(idvals,0) - origin_xhx = v1 - origin_xhy = v2 - origin_xhz = v3 - origin_vhx = v4 - origin_vhy = v5 - origin_vhz = v6 - discard_time = np.full_like(v1,-1.0) - status = np.repeat("ACTIVE",idvals.size) - discard_xhx = np.zeros_like(v1) - discard_xhy = np.zeros_like(v1) - discard_xhz = np.zeros_like(v1) - discard_vhx = np.zeros_like(v1) - discard_vhy = np.zeros_like(v1) - discard_vhz = np.zeros_like(v1) - discard_body_id = np.full_like(idvals,-1) - info_vec_float = np.vstack([ - origin_time, - origin_xhx, - origin_xhy, - origin_xhz, - origin_vhx, - origin_vhy, - origin_vhz, - discard_time, - discard_xhx, - discard_xhy, - discard_xhz, - discard_vhx, - discard_vhy, - discard_vhz]) - info_vec_int = np.vstack([collision_id, discard_body_id]) - info_vec_str = np.vstack([particle_type, origin_type, status]) - frame_float = info_vec_float.T - frame_int = info_vec_int.T - frame_str = info_vec_str.T - if param['IN_TYPE'] == 'NETCDF_FLOAT': - ftype=np.float32 - elif param['IN_TYPE'] == 'NETCDF_DOUBLE' or param['IN_TYPE'] == 'ASCII': - ftype=np.float64 - da_float = xr.DataArray(frame_float, dims=infodims, coords={'id': idvals, 'vec': infolab_float}).astype(ftype) - da_int = xr.DataArray(frame_int, dims=infodims, coords={'id': idvals, 'vec': infolab_int}) - da_str = xr.DataArray(frame_str, dims=infodims, coords={'id': idvals, 'vec': infolab_str}) - ds_float = da_float.to_dataset(dim="vec") - ds_int = da_int.to_dataset(dim="vec") - ds_str = da_str.to_dataset(dim="vec") - info_ds = xr.combine_by_coords([ds_float, ds_int, ds_str]) + ds_cb = None + if ispl: + lab_pl = plab.copy() + vec_pl = np.vstack([vec[:,ipl], GMpl[ipl]]) + if param['CHK_CLOSE']: + vec_pl = np.vstack([vec_pl, Rpl[ipl]]) + if param['RHILL_PRESENT']: + vec_pl = np.vstack([vec_pl, rhill[ipl]]) + if param['ROTATION']: + vec_pl = np.vstack([vec_pl, Ip1[ipl], Ip2[ipl], Ip3[ipl], rotx[ipl], roty[ipl], rotz[ipl]]) + particle_type[ipl] = np.repeat("Massive Body",idvals[ipl].size) + vec_pl = np.expand_dims(vec_pl.T,axis=0) # Make way for the time dimension! + ds_pl = xr.DataArray(vec_pl, dims=dims, coords={'time': [t], 'id': idvals[ipl], 'vec': lab_pl}).to_dataset(dim='vec') + else: + ds_pl = None + if istp: + lab_tp = tlab.copy() + vec_tp = np.expand_dims(vec[:,itp].T,axis=0) # Make way for the time dimension! + ds_tp = xr.DataArray(vec_tp, dims=dims, coords={'time': [t], 'id': idvals[itp], 'vec': lab_tp}).to_dataset(dim='vec') + particle_type[itp] = np.repeat("Test Particle",idvals[itp].size) + else: + ds_tp = None + + ds_info = xr.DataArray(np.vstack([namevals,particle_type]).T, dims=infodims, coords={'id': idvals, 'vec' : ["name", "particle_type"]}).to_dataset(dim='vec') + ds = [d for d in [ds_cb, ds_pl, ds_tp] if d is not None] + if len(ds) > 1: + ds = xr.combine_by_coords(ds) + else: + ds = ds[0] + ds = xr.merge([ds_info,ds]) - frame_float = np.expand_dims(vec_float.T, axis=0) - frame_str = vec_str.T - da_float = xr.DataArray(frame_float, dims=dims, coords={'time': [t], 'id': idvals, 'vec': label_float}).astype(ftype) - da_str= xr.DataArray(frame_str, dims=infodims, coords={'id': idvals, 'vec': label_str}) - ds_float = da_float.to_dataset(dim="vec") - ds_str = da_str.to_dataset(dim="vec") - ds = xr.combine_by_coords([ds_float, ds_str,info_ds]) return ds \ No newline at end of file diff --git a/python/swiftest/swiftest/io.py b/python/swiftest/swiftest/io.py index 4678b4784..2b8c47cb3 100644 --- a/python/swiftest/swiftest/io.py +++ b/python/swiftest/swiftest/io.py @@ -17,8 +17,108 @@ import tempfile import re -newfeaturelist = ("FRAGMENTATION", "ROTATION", "TIDES", "ENERGY", "GR", "YARKOVSKY", "YORP", "IN_FORM") +# This defines features that are new in Swiftest and not in Swifter (for conversion between param.in files) +newfeaturelist = ("RESTART", + "FRAGMENTATION", + "ROTATION", + "TIDES", + "ENERGY", + "GR", + "YARKOVSKY", + "YORP", + "IN_FORM", + "SEED", + "INTERACTION_LOOPS", + "ENCOUNTER_CHECK", + "TSTART") + +# This list defines features that are booleans, so must be converted to/from string when writing/reading from file +bool_param = ["RESTART", + "CHK_CLOSE", + "EXTRA_FORCE", + "RHILL_PRESENT", + "BIG_DISCARD", + "FRAGMENTATION", + "ROTATION", + "TIDES", + "ENERGY", + "GR", + "YARKOVSKY", + "YORP"] + +int_param = ["ISTEP_OUT", "ISTEP_DUMP"] +float_param = ["T0", "TSTART", "TSTOP", "DT", "CHK_RMIN", "CHK_RMAX", "CHK_EJECT", "CHK_QMIN", "DU2M", "MU2KG", + "TU2S", "MIN_GMFRAG", "GMTINY"] + +upper_str_param = ["OUT_TYPE","OUT_FORM","OUT_STAT","IN_TYPE","IN_FORM"] + +# This defines Xarray Dataset variables that are strings, which must be processed due to quirks in how NetCDF-Fortran +# handles strings differently than Python's Xarray. string_varnames = ["name", "particle_type", "status", "origin_type"] +int_varnames = ["id", "ntp", "npl", "nplm", "discard_body_id", "collision_id"] + +def bool2yesno(boolval): + """ + Converts a boolean into a string of either "YES" or "NO". + + Parameters + ---------- + boolval : bool + Input value + + Returns + ------- + {"YES","NO"} + + """ + if boolval: + return "YES" + else: + return "NO" + +def bool2tf(boolval): + """ + Converts a boolean into a string of either "T" or "F". + + Parameters + ---------- + boolval : bool + Input value + + Returns + ------- + {"T","F"} + + """ + if boolval: + return "T" + else: + return "F" + +def str2bool(input_str): + """ + Converts a string into an equivalent boolean. + + Parameters + ---------- + input_str : {"YES", "Y", "T", "TRUE", ".TRUE.", "NO", "N", "F", "FALSE", ".FALSE."} + Input string. Input is case-insensitive. + + Returns + ------- + {True, False} + + """ + valid_true = ["YES", "Y", "T", "TRUE", ".TRUE."] + valid_false = ["NO", "N", "F", "FALSE", ".FALSE."] + if input_str.upper() in valid_true: + return True + elif input_str.upper() in valid_false: + return False + else: + raise ValueError(f"{input_str} is not recognized as boolean") + + def real2float(realstr): """ @@ -27,7 +127,7 @@ def real2float(realstr): Parameters ---------- - realstr : string + realstr : str Fortran-generated ASCII string of a real value. Returns @@ -52,8 +152,8 @@ def read_swiftest_param(param_file_name, param, verbose=True): param : dict A dictionary containing the entries in the user parameter file """ - param['! VERSION'] = f"Swiftest parameter input from file {param_file_name}" - + param['! VERSION'] = f"Swiftest parameter input file" + # Read param.in file if verbose: print(f'Reading Swiftest file {param_file_name}') try: @@ -64,47 +164,26 @@ def read_swiftest_param(param_file_name, param, verbose=True): if fields[0][0] != '!': key = fields[0].upper() param[key] = fields[1] - #for key in param: - # if (key == fields[0].upper()): param[key] = fields[1] # Special case of CHK_QMIN_RANGE requires a second input if fields[0].upper() == 'CHK_QMIN_RANGE': alo = real2float(fields[1]) ahi = real2float(fields[2]) param['CHK_QMIN_RANGE'] = f"{alo} {ahi}" - - param['ISTEP_OUT'] = int(param['ISTEP_OUT']) - param['ISTEP_DUMP'] = int(param['ISTEP_DUMP']) - param['OUT_TYPE'] = param['OUT_TYPE'].upper() - param['OUT_FORM'] = param['OUT_FORM'].upper() - param['OUT_STAT'] = param['OUT_STAT'].upper() - param['IN_TYPE'] = param['IN_TYPE'].upper() - param['IN_FORM'] = param['IN_FORM'].upper() - param['T0'] = real2float(param['T0']) - param['TSTOP'] = real2float(param['TSTOP']) - param['DT'] = real2float(param['DT']) - param['CHK_RMIN'] = real2float(param['CHK_RMIN']) - param['CHK_RMAX'] = real2float(param['CHK_RMAX']) - param['CHK_EJECT'] = real2float(param['CHK_EJECT']) - param['CHK_QMIN'] = real2float(param['CHK_QMIN']) - param['DU2M'] = real2float(param['DU2M']) - param['MU2KG'] = real2float(param['MU2KG']) - param['TU2S'] = real2float(param['TU2S']) - param['EXTRA_FORCE'] = param['EXTRA_FORCE'].upper() - param['BIG_DISCARD'] = param['BIG_DISCARD'].upper() - param['CHK_CLOSE'] = param['CHK_CLOSE'].upper() - param['RHILL_PRESENT'] = param['RHILL_PRESENT'].upper() - param['FRAGMENTATION'] = param['FRAGMENTATION'].upper() - if param['FRAGMENTATION'] == 'YES' and param['PARTICLE_OUT'] == '': - if param['OUT_TYPE'] == 'REAL8' or param['OUT_TYPE'] == 'REAL4': - param['PARTICLE_OUT'] = 'particle.dat' - param['ROTATION'] = param['ROTATION'].upper() - param['TIDES'] = param['TIDES'].upper() - param['ENERGY'] = param['ENERGY'].upper() - param['GR'] = param['GR'].upper() - param['INTERACTION_LOOPS'] = param['INTERACTION_LOOPS'].upper() - param['ENCOUNTER_CHECK'] = param['ENCOUNTER_CHECK'].upper() - if 'GMTINY' in param: - param['GMTINY'] = real2float(param['GMTINY']) + + for uc in upper_str_param: + if uc in param: + param[uc] = param[uc].upper() + + for i in int_param: + if i in param and type(i) != int: + param[i] = int(param[i]) + + for f in float_param: + if f in param and type(f) is str: + param[f] = real2float(param[f]) + for b in bool_param: + if b in param: + param[b] = str2bool(param[b]) except IOError: print(f"{param_file_name} not found.") return param @@ -140,7 +219,7 @@ def read_swifter_param(param_file_name, verbose=True): 'OUT_STAT': "NEW", 'J2': "0.0", 'J4': "0.0", - 'CHK_CLOSE': 'NO', + 'CHK_CLOSE': False, 'CHK_RMIN': "-1.0", 'CHK_RMAX': "-1.0", 'CHK_EJECT': "-1.0", @@ -148,9 +227,9 @@ def read_swifter_param(param_file_name, verbose=True): 'CHK_QMIN_COORD': "HELIO", 'CHK_QMIN_RANGE': "", 'ENC_OUT': "", - 'EXTRA_FORCE': 'NO', - 'BIG_DISCARD': 'NO', - 'RHILL_PRESENT': 'NO', + 'EXTRA_FORCE': False, + 'BIG_DISCARD': False, + 'RHILL_PRESENT': False, 'C': "-1.0", } @@ -180,10 +259,9 @@ def read_swifter_param(param_file_name, verbose=True): param['CHK_RMAX'] = real2float(param['CHK_RMAX']) param['CHK_EJECT'] = real2float(param['CHK_EJECT']) param['CHK_QMIN'] = real2float(param['CHK_QMIN']) - param['EXTRA_FORCE'] = param['EXTRA_FORCE'].upper() - param['BIG_DISCARD'] = param['BIG_DISCARD'].upper() - param['CHK_CLOSE'] = param['CHK_CLOSE'].upper() - param['RHILL_PRESENT'] = param['RHILL_PRESENT'].upper() + for b in bool_param: + if b in param: + param[b] = str2bool(param[b]) if param['C'] != '-1.0': param['C'] = real2float(param['C']) else: @@ -216,7 +294,6 @@ def read_swift_param(param_file_name, startfile="swift.in", verbose=True): 'DTOUT': 0.0, 'DTDUMP': 0.0, 'L1': "F", - 'L1': "F", 'L2': "F", 'L3': "F", 'L4': "F", @@ -325,18 +402,21 @@ def write_labeled_param(param, param_file_name): outfile = open(param_file_name, 'w') keylist = ['! VERSION', 'T0', + 'TSTART', 'TSTOP', 'DT', 'ISTEP_OUT', 'ISTEP_DUMP', - 'OUT_FORM', - 'OUT_TYPE', - 'OUT_STAT', - 'IN_TYPE', + 'NC_IN', 'PL_IN', 'TP_IN', 'CB_IN', + 'IN_TYPE', + 'IN_FORM', 'BIN_OUT', + 'OUT_FORM', + 'OUT_TYPE', + 'OUT_STAT', 'CHK_QMIN', 'CHK_RMIN', 'CHK_RMAX', @@ -345,15 +425,27 @@ def write_labeled_param(param, param_file_name): 'CHK_QMIN_RANGE', 'MU2KG', 'TU2S', - 'DU2M' ] + 'DU2M', + 'GMTINY', + 'FRAGMENTATION' + 'MIN_GMFRAG', + 'RESTART'] ptmp = param.copy() # Print the list of key/value pairs in the preferred order for key in keylist: val = ptmp.pop(key, None) - if val is not None: print(f"{key:<16} {val}", file=outfile) + if val is not None: + if type(val) is bool: + print(f"{key:<16} {bool2yesno(val)}", file=outfile) + else: + print(f"{key:<16} {val}", file=outfile) # Print the remaining key/value pairs in whatever order for key, val in ptmp.items(): - if val != "": print(f"{key:<16} {val}", file=outfile) + if val != "": + if type(val) is bool: + print(f"{key:<16} {bool2yesno(val)}", file=outfile) + else: + print(f"{key:<16} {val}", file=outfile) outfile.close() return @@ -484,12 +576,12 @@ def make_swiftest_labels(param): tlab.append('capm') plab = tlab.copy() plab.append('Gmass') - if param['CHK_CLOSE'] == 'YES': + if param['CHK_CLOSE']: plab.append('radius') - if param['RHILL_PRESENT'] == 'YES': + if param['RHILL_PRESENT']: plab.append('rhill') clab = ['Gmass', 'radius', 'j2rp2', 'j4rp4'] - if param['ROTATION'] == 'YES': + if param['ROTATION']: clab.append('Ip1') clab.append('Ip2') clab.append('Ip3') @@ -502,7 +594,7 @@ def make_swiftest_labels(param): plab.append('rotx') plab.append('roty') plab.append('rotz') - if param['TIDES'] == 'YES': + if param['TIDES']: clab.append('k2') clab.append('Q') plab.append('k2') @@ -589,14 +681,14 @@ def swiftest_stream(f, param): Rcb = f.read_reals(np.float64) J2cb = f.read_reals(np.float64) J4cb = f.read_reals(np.float64) - if param['ROTATION'] == 'YES': + if param['ROTATION']: Ipcbx = f.read_reals(np.float64) Ipcby = f.read_reals(np.float64) Ipcbz = f.read_reals(np.float64) rotcbx = f.read_reals(np.float64) rotcby = f.read_reals(np.float64) rotcbz = f.read_reals(np.float64) - if param['TIDES'] == 'YES': + if param['TIDES']: k2cb = f.read_reals(np.float64) Qcb = f.read_reals(np.float64) if npl[0] > 0: @@ -620,17 +712,17 @@ def swiftest_stream(f, param): p11 = f.read_reals(np.float64) p12 = f.read_reals(np.float64) GMpl = f.read_reals(np.float64) - if param['RHILL_PRESENT'] == 'YES': + if param['RHILL_PRESENT']: rhill = f.read_reals(np.float64) Rpl = f.read_reals(np.float64) - if param['ROTATION'] == 'YES': + if param['ROTATION']: Ipplx = f.read_reals(np.float64) Ipply = f.read_reals(np.float64) Ipplz = f.read_reals(np.float64) rotplx = f.read_reals(np.float64) rotply = f.read_reals(np.float64) rotplz = f.read_reals(np.float64) - if param['TIDES'] == 'YES': + if param['TIDES']: k2pl = f.read_reals(np.float64) Qpl = f.read_reals(np.float64) if ntp[0] > 0: @@ -680,14 +772,14 @@ def swiftest_stream(f, param): tpid = np.empty(0) tpnames = np.empty(0) cvec = np.array([Mcb, Rcb, J2cb, J4cb]) - if param['RHILL_PRESENT'] == 'YES': + if param['RHILL_PRESENT']: if npl > 0: pvec = np.vstack([pvec, rhill]) - if param['ROTATION'] == 'YES': + if param['ROTATION']: cvec = np.vstack([cvec, Ipcbx, Ipcby, Ipcbz, rotcbx, rotcby, rotcbz]) if npl > 0: pvec = np.vstack([pvec, Ipplx, Ipply, Ipplz, rotplx, rotply, rotplz]) - if param['TIDES'] == 'YES': + if param['TIDES']: cvec = np.vstack([cvec, k2cb, Qcb]) if npl > 0: pvec = np.vstack([pvec, k2pl, Qpl]) @@ -752,54 +844,18 @@ def swiftest2xr(param, verbose=True): ------- xarray dataset """ - if ((param['OUT_TYPE'] == 'REAL8') or (param['OUT_TYPE'] == 'REAL4')): - dims = ['time', 'id', 'vec'] - cb = [] - pl = [] - tp = [] - cbn = None - try: - with FortranFile(param['BIN_OUT'], 'r') as f: - for t, cbid, cbnames, cvec, clab, \ - npl, plid, plnames, pvec, plab, \ - ntp, tpid, tpnames, tvec, tlab in swiftest_stream(f, param): - # Prepare frames by adding an extra axis for the time coordinate - cbframe = np.expand_dims(cvec, axis=0) - plframe = np.expand_dims(pvec, axis=0) - tpframe = np.expand_dims(tvec, axis=0) - - - # Create xarray DataArrays out of each body type - cbxr = xr.DataArray(cbframe, dims=dims, coords={'time': t, 'id': cbid, 'vec': clab}) - cbxr = cbxr.assign_coords(name=("id", cbnames)) - plxr = xr.DataArray(plframe, dims=dims, coords={'time': t, 'id': plid, 'vec': plab}) - plxr = plxr.assign_coords(name=("id", plnames)) - tpxr = xr.DataArray(tpframe, dims=dims, coords={'time': t, 'id': tpid, 'vec': tlab}) - tpxr = tpxr.assign_coords(name=("id", tpnames)) - - cb.append(cbxr) - pl.append(plxr) - tp.append(tpxr) - - sys.stdout.write('\r' + f"Reading in time {t[0]:.3e}") - sys.stdout.flush() - except IOError: - print(f"Error encountered reading in {param['BIN_OUT']}") - - cbda = xr.concat(cb, dim='time') - plda = xr.concat(pl, dim='time') - tpda = xr.concat(tp, dim='time') - - cbds = cbda.to_dataset(dim='vec') - plds = plda.to_dataset(dim='vec') - tpds = tpda.to_dataset(dim='vec') - if verbose: print('\nCreating Dataset') - ds = xr.combine_by_coords([cbds, plds, tpds]) - elif ((param['OUT_TYPE'] == 'NETCDF_DOUBLE') or (param['OUT_TYPE'] == 'NETCDF_FLOAT')): + if ((param['OUT_TYPE'] == 'NETCDF_DOUBLE') or (param['OUT_TYPE'] == 'NETCDF_FLOAT')): if verbose: print('\nCreating Dataset from NetCDF file') ds = xr.open_dataset(param['BIN_OUT'], mask_and_scale=False) - ds = clean_string_values(ds) + if param['OUT_TYPE'] == "NETCDF_DOUBLE": + ds = fix_types(ds,ftype=np.float64) + elif param['OUT_TYPE'] == "NETCDF_FLOAT": + ds = fix_types(ds,ftype=np.float32) + # Check if the name variable contains unique values. If so, make name the dimension instead of id + if len(np.unique(ds['name'])) == len(ds['name']): + ds = ds.swap_dims({"id" : "name"}) + ds = ds.reset_coords("id") else: print(f"Error encountered. OUT_TYPE {param['OUT_TYPE']} not recognized.") return None @@ -836,7 +892,7 @@ def string_converter(da): """ if da.dtype == np.dtype(object): da = da.astype(' ") if intxt.upper() == 'Y': isSyMBA = True - swifter_param['RHILL_PRESENT'] = 'YES' + swifter_param['RHILL_PRESENT'] = True else: isSyMBA = False - swifter_param['RHILL_PRESENT'] = 'NO' + swifter_param['RHILL_PRESENT'] = False isDouble = conversion_questions.get('DOUBLE', None) if not isDouble: @@ -1213,9 +1292,9 @@ def swift2swifter(swift_param, plname="", tpname="", conversion_questions={}): swifter_param['OUT_FORM'] = 'XV' if swift_param['LCLOSE'] == "T": - swifter_param['CHK_CLOSE'] = "YES" + swifter_param['CHK_CLOSE'] = True else: - swifter_param['CHK_CLOSE'] = "NO" + swifter_param['CHK_CLOSE'] = False swifter_param['CHK_RMIN'] = swift_param['RMIN'] swifter_param['CHK_RMAX'] = swift_param['RMAX'] @@ -1254,17 +1333,17 @@ def swift2swifter(swift_param, plname="", tpname="", conversion_questions={}): if not intxt: intxt = input("EXTRA_FORCE: Use additional user-specified force routines? (y/N)> ") if intxt.upper() == 'Y': - swifter_param['EXTRA_FORCE'] = 'YES' + swifter_param['EXTRA_FORCE'] = True else: - swifter_param['EXTRA_FORCE'] = 'NO' + swifter_param['EXTRA_FORCE'] = False intxt = conversion_questions.get('BIG_DISCARD', None) if not intxt: intxt = input("BIG_DISCARD: include data for all bodies > GMTINY for each discard record? (y/N)> ") if intxt.upper() == 'Y': - swifter_param['BIG_DISCARD'] = 'YES' + swifter_param['BIG_DISCARD'] = True else: - swifter_param['BIG_DISCARD'] = 'NO' + swifter_param['BIG_DISCARD'] = False # Convert the PL file if plname == '': @@ -1310,11 +1389,11 @@ def swift2swifter(swift_param, plname="", tpname="", conversion_questions={}): else: if swift_param['LCLOSE'] == "T": plrad = real2float(i_list[1]) - if swifter_param['RHILL_PRESENT'] == 'YES': + if swifter_param['RHILL_PRESENT']: print(n + 1, GMpl, rhill, file=plnew) else: print(n + 1, GMpl, file=plnew) - if swifter_param['CHK_CLOSE'] == 'YES': + if swifter_param['CHK_CLOSE']: print(plrad, file=plnew) line = plold.readline() i_list = [i for i in re.split(' +|\t',line) if i.strip()] @@ -1434,12 +1513,12 @@ def swifter2swiftest(swifter_param, plname="", tpname="", cbname="", conversion_ i_list = [i for i in re.split(' +|\t',line) if i.strip()] idnum = int(i_list[0]) GMpl = real2float(i_list[1]) - if swifter_param['RHILL_PRESENT'] == 'YES': + if swifter_param['RHILL_PRESENT']: rhill = real2float(i_list[2]) print(idnum, GMpl, rhill, file=plnew) else: print(idnum, GMpl, file=plnew) - if swifter_param['CHK_CLOSE'] == 'YES': + if swifter_param['CHK_CLOSE']: line = plold.readline() i_list = [i for i in re.split(' +|\t',line) if i.strip()] plrad = real2float(i_list[0]) @@ -1609,7 +1688,7 @@ def swifter2swiftest(swifter_param, plname="", tpname="", cbname="", conversion_ # Remove the unneeded parameters if 'C' in swiftest_param: - swiftest_param['GR'] = 'YES' + swiftest_param['GR'] = True swiftest_param.pop('C', None) swiftest_param.pop('J2', None) swiftest_param.pop('J4', None) @@ -1698,7 +1777,7 @@ def swiftest2swifter_param(swiftest_param, J2=0.0, J4=0.0): TU2S = swifter_param.pop("TU2S", 1.0) GR = swifter_param.pop("GR", None) if GR is not None: - if GR == 'YES': + if GR: swifter_param['C'] = swiftest.einsteinC * np.longdouble(TU2S) / np.longdouble(DU2M) for key in newfeaturelist: tmp = swifter_param.pop(key, None) @@ -1770,7 +1849,7 @@ def swifter2swift_param(swifter_param, J2=0.0, J4=0.0): swift_param['BINARY_OUTPUTFILE'] = swifter_param['BIN_OUT'] swift_param['STATUS_FLAG_FOR_OPEN_STATEMENTS'] = swifter_param['OUT_STAT'] - if swifter_param['CHK_CLOSE'] == "YES": + if swifter_param['CHK_CLOSE']: swift_param['LCLOSE'] = "T" else: swift_param['LCLOSE'] = "F" diff --git a/python/swiftest/swiftest/simulation_class.py b/python/swiftest/swiftest/simulation_class.py index 5ad945d69..7c17c3d32 100644 --- a/python/swiftest/swiftest/simulation_class.py +++ b/python/swiftest/swiftest/simulation_class.py @@ -1,5 +1,4 @@ """ - self.param['BIN_OUT'] = binpath Copyright 2022 - David Minton, Carlisle Wishard, Jennifer Pouplin, Jake Elliott, & Dana Singh This file is part of Swiftest. Swiftest is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -9,80 +8,1512 @@ You should have received a copy of the GNU General Public License along with Swiftest. If not, see: https://www.gnu.org/licenses. """ +from __future__ import annotations from swiftest import io from swiftest import init_cond from swiftest import tool from swiftest import constants -from datetime import date +from swiftest import __file__ as _pyfile +import os +import datetime import xarray as xr import numpy as np -import os +import numpy.typing as npt import shutil +import subprocess +import shlex +import warnings +from typing import ( + Literal, + Dict, + List, + Any +) + + +class Simulation: + """ + This is a class that defines the basic Swift/Swifter/Swiftest simulation object + """ + + def __init__(self,read_param: bool = True, **kwargs: Any): + """ + + Parameters + ---------- + read_param : bool, default True + If true, read in a pre-existing parameter input file given by the argument `param_file` if it exists. + Otherwise, create a new parameter file using the arguments passed to Simulation or defaults + + Parameters for a given Simulation object can be set a number of different ways, including via a parameter input + file, arguments to Simulation, the general `set_parameter` method, or the specific setters for groups of + similar parameters (e.g. set_init_cond_files, set_simulation_time, etc.). Each parameter has a default value + that can be overridden by an argument to Simulation(). Some argument parameters have equivalent values that + are passed to the `swiftest_driver` Fortran program via a parameter input file. When declaring a new + Simulation object, parameters are chosen in the following way, from highest to lowest priority" + 1. Arguments to Simulation() + 2. The parameter input file given by `param_file` under the following conditions: + - `read_param` is set to True (default behavior). + - The file given by `param_file` exists. The default file is `param.in` located in the current working + directory, which can be changed by passing `param_file` as an argument. + - The argument has an equivalent parameter or set of parameters in the parameter input file. + 3. Default values (see below) + + **kwargs : See list of valid parameters and their defaults below + + codename : {"Swiftest", "Swifter", "Swift"}, default "Swiftest" + Name of the n-body code that will be used. + Parameter input file equivalent: None + integrator : {"symba","rmvs","whm","helio"}, default "symba" + Name of the n-body integrator that will be used when executing a run. + Parameter input file equivalent: None + param_file : str, path-like, or file-lke, default "param.in" + Name of the parameter input file that will be passed to the integrator. + Parameter input file equivalent: None + t0 : float, default 0.0 + The reference time for the start of the simulation. Defaults is 0.0. + Parameter input file equivalent: `T0` + tstart : float, default 0.0 + The start time for a restarted simulation. For a new simulation, tstart will be set to t0 automatically. + Parameter input file equivalent: `TSTART` + tstop : float, optional + The stopping time for a simulation. `tstop` must be greater than `tstart`. + Parameter input file equivalent: `TSTOP` + dt : float, optional + The step size of the simulation. `dt` must be less than or equal to `tstop-tstart`. + Parameter input file equivalent: `DT` + istep_out : int, optional + The number of time steps between outputs to file. *Note*: only `istep_out` or `toutput` can be set. + Parameter input file equivalent: `ISTEP_OUT` + tstep_out : float, optional + The approximate time between when outputs are written to file. Passing this computes + `istep_out = floor(tstep_out/dt)`. *Note*: only `istep_out` or `toutput` can be set. + Parameter input file equivalent: None + istep_dump : int, optional + The anumber of time steps between outputs to dump file. If not set, this will be set to the value of + `istep_out` (or the equivalent value determined by `tstep_out`). + Parameter input file equivalent: `ISTEP_DUMP` + init_cond_file_type : {"NETCDF_DOUBLE", "NETCDF_FLOAT", "ASCII"}, default "NETCDF_DOUBLE" + The file type containing initial conditions for the simulation: + * NETCDF_DOUBLE: A single initial conditions input file in NetCDF file format of type NETCDF_DOUBLE. + * NETCDF_FLOAT: A single initial conditions input file in NetCDF file format of type NETCDF_FLOAT. + * ASCII : Three initial conditions files in ASCII format. The individual files define the central body, + massive body, and test particle initial conditions. + Parameter input file equivalent: `IN_TYPE` + init_cond_file_name : str, path-like, or dict, optional + Name of the input initial condition file or files. Whether to pass a single file name or a dictionary + depends on the argument passed to `init_cond_file_type`: If `init_cond_file_type={"NETCDF_DOUBLE","NETCDF_FLOAT"}`, + then this will be a single file name. If `init_cond_file_type="ASCII"` then this must be a dictionary where: + ```init_cond_file_name = { + "CB" : *path to central body initial conditions file* (Swiftest only), + "PL" : *path to massive body initial conditions file*, + "TP" : *path to test particle initial conditions file* + }``` + If no file name is provided then the following default file names will be used. + * NETCDF_DOUBLE, NETCDF_FLOAT: `init_cond_file_name = "init_cond.nc"` + * ASCII: `init_cond_file_name = {"CB" : "cb.in", "PL" : "pl.in", "TP" : "tp.in"}` + Parameter input file equivalent: `NC_IN`, `CB_IN`, `PL_IN`, `TP_IN` + init_cond_format : {"EL", "XV"}, default "EL" + Indicates whether the input initial conditions are given as orbital elements or cartesian position and + velocity vectors. + > *Note:* If `codename` is "Swift" or "Swifter", EL initial conditions are converted to XV. + Parameter input file equivalent: `IN_FORM` + output_file_type : {"NETCDF_DOUBLE", "NETCDF_FLOAT","REAL4","REAL8","XDR4","XDR8"}, default "NETCDF_DOUBLE" + The file type for the outputs of the simulation. Compatible file types depend on the `codename` argument. + * Swiftest: Only "NETCDF_DOUBLE" or "NETCDF_FLOAT" supported. + * Swifter: Only "REAL4","REAL8","XDR4" or "XDR8" supported. + * Swift: Only "REAL4" supported. + Parameter input file equivalent: `OUT_TYPE` + output_file_name : str or path-like, optional + Name of output file to generate. If not supplied, then one of the default file names are used, depending on + the value passed to `output_file_type`. If one of the NetCDF types are used, the default is "bin.nc". + Otherwise, the default is "bin.dat". + Parameter input file equivalent: `BIN_OUT` + output_format : {"XV","XVEL"}, default "XVEL" + Specifies the format for the data saved to the output file. If "XV" then cartesian position and velocity + vectors for all bodies are stored. If "XVEL" then the orbital elements are also stored. + Parameter input file equivalent: `OUT_FORM` + read_old_output_file : bool, default False + If true, read in a pre-existing binary input file given by the argument `output_file_name` if it exists. + Parameter input file equivalent: None + MU : str, default "MSUN" + The mass unit system to use. Case-insensitive valid options are: + * "Msun" : Solar mass + * "Mearth" : Earth mass + * "kg" : kilograms + * "g" : grams + Parameter input file equivalent: None + DU : str, optional + The distance unit system to use. Case-insensitive valid options are: + * "AU" : Astronomical Unit + * "Rearth" : Earth radius + * "m" : meter + * "cm" : centimeter + Parameter input file equivalent: None + TU : str, optional + The time unit system to use. Case-insensitive valid options are: + * "YR" : Year + * "DAY" : Julian day + * "d" : Julian day + * "JD" : Julian day + * "s" : second + Parameter input file equivalent: None + MU2KG: float, optional + The conversion factor to multiply by the mass unit that would convert it to kilogram. + Setting this overrides MU + Parameter input file equivalent: `MU2KG` + DU2M : float, optional + The conversion factor to multiply by the distance unit that would convert it to meter. + Setting this overrides DU + Parameter input file equivalent: `DU2M` + TU2S : float, optional + The conversion factor to multiply by the time unit that would convert it to seconds. + Setting this overrides TU + Parameter input file equivalent: `TU2S` + MU_name : str, optional + The name of the mass unit. When setting one of the standard units via `MU` a name will be + automatically set for the unit, so this argument will override the automatic name. + Parameter input file equivalent: None + DU_name : str, optional + The name of the distance unit. When setting one of the standard units via `DU` a name will be + automatically set for the unit, so this argument will override the automatic name. + Parameter input file equivalent: None + TU_name : str, optional + The name of the time unit. When setting one of the standard units via `TU` a name will be + automatically set for the unit, so this argument will override the automatic name. + Parameter input file equivalent: None + rmin : float, default value is the radius of the Sun in the unit system defined by the unit input arguments. + Minimum distance of the simulation + Parameter input file equivalent: `CHK_QMIN`, `CHK_RMIN`, `CHK_QMIN_RANGE[0]` + rmax : float, default value is 10000 AU in the unit system defined by the unit input arguments. + Maximum distance of the simulation (CHK_RMAX, CHK_QMIN_RANGE[1]) + Parameter input file equivalent: `CHK_RMAX`, `CHK_QMIN_RANGE[1]` + qmin_coord : str, {"HELIO", "BARY"}, default "HELIO" + coordinate frame to use for checking the minimum periapsis distance + Parameter input file equivalent: `QMIN_COORD` + mtiny : float, optional + The minimum mass of fully interacting bodies. Bodies below this mass interact with the larger bodies, + but not each other (SyMBA only). *Note.* Only mtiny or gmtiny is accepted, not both. + Parameter input file equivalent: None + gmtiny : float, optional + The minimum G*mass of fully interacting bodies. Bodies below this mass interact with the larger bodies, + but not each other (SyMBA only). *Note.* Only mtiny or gmtiny is accepted, not both. + Parameter input file equivalent: `GMTINY` + close_encounter_check : bool, default True + Check for close encounters between bodies. If set to True, then the radii of massive bodies must be included + in initial conditions. + Parameter input file equivalent: `CHK_CLOSE` + general_relativity : bool, default True + Include the post-Newtonian correction in acceleration calculations. + Parameter input file equivalent: `GR` + fragmentation : bool, default True + If set to True, this turns on the Fraggle fragment generation code and `rotation` must also be True. + This argument only applies to Swiftest-SyMBA simulations. It will be ignored otherwise. + Parameter input file equivalent: `FRAGMENTATION` + minimum_fragment_gmass : float, optional + If fragmentation is turned on, this sets the mimimum G*mass of a collisional fragment that can be generated. + *Note.* Only set one of minimum_fragment_gmass or minimum_fragment_mass + Parameter input file equivalent: None + minimum_fragment_mass : float, optional + If fragmentation is turned on, this sets the mimimum mass of a collisional fragment that can be generated. + *Note.* Only set one of minimum_fragment_gmass or minimum_fragment_mass + Parameter input file equivalent: `MIN_GMFRAG` + rotation : bool, default False + If set to True, this turns on rotation tracking and radius, rotation vector, and moments of inertia values + must be included in the initial conditions. + This argument only applies to Swiftest-SyMBA simulations. It will be ignored otherwise. + Parameter input file equivalent: `ROTATION` + compute_conservation_values : bool, default False + Turns on the computation of energy, angular momentum, and mass conservation and reports the values + every output step of a running simulation. + Parameter input file equivalent: `ENERGY` + extra_force: bool, default False + Turns on user-defined force function. + Parameter input file equivalent: `EXTRA_FORCE` + big_discard: bool, default False + Includes big bodies when performing a discard (Swifter only) + Parameter input file equivalent: `BIG_DISCARD` + rhill_present: bool, default False + Include the Hill's radius with the input files . + Parameter input file equivalent: `RHILL_PRESENT` + restart : bool, default False + If true, will restart an old run. The file given by `output_file_name` must exist before the run can + execute. If false, will start a new run. If the file given by `output_file_name` exists, it will be replaced + when the run is executed. + Parameter input file equivalent: `OUT_STAT` + interaction_loops : {"TRIANGULAR","FLAT","ADAPTIVE"}, default "TRIANGULAR" + > *Swiftest Experimental feature* + Specifies which algorithm to use for the computation of body-body gravitational forces. + * "TRIANGULAR" : Upper-triangular double-loops . + * "FLAT" : Body-body interation pairs are flattened into a 1-D array. + * "ADAPTIVE" : Periodically times the TRIANGULAR and FLAT methods and determines which one to use based on + the wall time to complete the loop. *Note:* Using ADAPTIVE means that bit-identical repeatability cannot + be assured, as the choice of algorithm depends on possible external factors that can influence the wall + time calculation. The exact floating-point results of the interaction will be different between the two + algorithm types. + Parameter input file equivalent: `INTERACTION_LOOPS` + encounter_check_loops : {"TRIANGULAR","SORTSWEEP","ADAPTIVE"}, default "TRIANGULAR" + > *Swiftest Experimental feature* + Specifies which algorithm to use for checking whether bodies are in a close encounter state or not. + * "TRIANGULAR" : Upper-triangular double-loops. + * "SORTSWEEP" : A Sort-Sweep algorithm is used to reduce the population of potential close encounter bodies. + This algorithm is still in development, and does not necessarily speed up the encounter checking. + Use with caution. + * "ADAPTIVE" : Periodically times the TRIANGULAR and SORTSWEEP methods and determines which one to use based + on the wall time to complete the loop. *Note:* Using ADAPTIVE means that bit-identical repeatability cannot + be assured, as the choice of algorithm depends on possible external factors that can influence the wall + time calculation. The exact floating-point results of the interaction will be different between the two + algorithm types. + Parameter input file equivalent: `ENCOUNTER_CHECK` + verbose : bool, default True + If set to True, then more information is printed by Simulation methods as they are executed. Setting to + False suppresses most messages other than errors. + Parameter input file equivalent: None + """ + + # Width of the column in the printed name of the parameter in parameter getters + self._getter_column_width = '32' + + self.param = {} + self.data = xr.Dataset() + + # Parameters are set in reverse priority order. First the defaults, then values from a pre-existing input file, + # then using the arguments passed via **kwargs. + + #-------------------------- + # Lowest Priority: Defaults + #-------------------------- + + # Quietly set all parameters to their defaults. + self.verbose = kwargs.pop("verbose",True) + self.set_parameter(verbose=False) + + # Set the location of the parameter input file + param_file = kwargs.pop("param_file",self.param_file) + read_param = kwargs.pop("read_param",True) + self.set_parameter(verbose=False,param_file=param_file) + + #----------------------------------------------------------------- + # Higher Priority: Values from a file (if requested and it exists) + #----------------------------------------------------------------- + + # If the user asks to read in an old parameter file, override any default parameters with values from the file + # If the file doesn't exist, flag it for now so we know to create it + if read_param: + if os.path.exists(self.param_file): + self.read_param(self.param_file, codename=self.codename, verbose=self.verbose) + param_file_found = True + # We will add the parameter file to the kwarg list. This will keep the set_parameter method from + # overriding everything with defaults when there are no arguments passed to Simulation() + kwargs['param_file'] = self.param_file + else: + param_file_found = False + + # ----------------------------------------------------------------- + # Highest Priority: Values from arguments passed to Simulation() + # ----------------------------------------------------------------- + self.set_parameter(verbose=False, **kwargs) + + # Let the user know that there was a problem reading an old parameter file and we're going to create a new one + if read_param and not param_file_found: + warnings.warn(f"{self.param_file} not found. Creating a new file using default values for parameters not passed to Simulation().") + self.write_param() + + # Read in an old simulation file if requested + read_old_output_file = kwargs.pop("read_old_output_file",False) + if read_old_output_file: + binpath = os.path.join(self.sim_dir, self.param['BIN_OUT']) + if os.path.exists(binpath): + self.bin2xr() + else: + warnings.warn(f"BIN_OUT file {binpath} not found.") + return + + + def run(self,**kwargs): + """ + Runs a Swiftest integration. Uses the parameters set by the `param` dictionary unless overridden by keyword + arguments. Accepts any keyword arguments that can be passed to `set_parameter`. + + Parameters + ---------- + **kwargs : Any valid keyword arguments accepted by `set_parameter` + + Returns + ------- + None + + """ + + if len(kwargs) > 0: + self.set_parameter(**kwargs) + + # Write out the current parameter set before executing run + self.write_param() + + if self.codename != "Swiftest": + warnings.warn(f"Running an integration is not yet supported for {self.codename}") + return + + if self.driver_executable is None: + warnings.warn("Path to swiftest_driver has not been set!") + warnings.warn(f"Make sure swiftest_driver is compiled and the executable is in {self.binary_path}") + return + + print(f"Running a {self.codename} {self.integrator} run from tstart={self.param['TSTART']} {self.TU_name} to tstop={self.param['TSTOP']} {self.TU_name}") + + # Get current environment variables + env = os.environ.copy() + driver_script = os.path.join(self.binary_path,"swiftest_driver.sh") + shell = os.path.basename(env['SHELL']) + with open(driver_script,'w') as f: + f.write(f"#{env['SHELL']} -l {os.linesep}") + f.write(f"source ~/.{shell}rc {os.linesep}") + f.write(f"cd {self.sim_dir} {os.linesep}") + f.write(f"{self.driver_executable} {self.integrator} {self.param_file}") + + try: + cmd = f"{env['SHELL']} -l {driver_script}" + with subprocess.Popen(shlex.split(cmd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + universal_newlines=True) as p: + for line in p.stdout: + print(line, end='') + res = p.communicate() + if p.returncode != 0: + for line in res[1]: + print(line, end='') + raise Exception ("Failure in swiftest_driver") + except: + warnings.warn(f"Error executing main swiftest_driver program") + return + + # Read in new data + self.bin2xr() + + return + + def _get_valid_arg_list(self, arg_list: str | List[str] | None = None, valid_var: Dict | None = None): + """ + Internal function for getters that extracts subset of arguments that is contained in the dictionary of valid + argument/parameter variable pairs. + + Parameters + ---------- + arg_list : str | List[str], optional + A single string or list of strings containing the Simulation argument. If none are supplied, + then it will create the arg_list out of all keys in the valid_var dictionary. + valid_var : valid_var: Dict + A dictionary where the key is the argument name and the value is the equivalent key in the Simulation + parameter dictionary (i.e. the left-hand column of a param.in file) + + Returns + ------- + valid_arg : [str] + The list of valid arguments that match the keys in valid_var + param : dict + A dictionary that is the subset of the Simulation parameter dictionary corresponding to the arguments listed + in arg_list. + + """ + + if arg_list is None: + valid_arg = None + else: + valid_arg = arg_list.copy() + + if valid_arg is None: + valid_arg = list(valid_var.keys()) + elif type(arg_list) is str: + valid_arg = [arg_list] + else: + # Only allow arg_lists to be checked if they are valid. Otherwise ignore. + valid_arg = [k for k in arg_list if k in list(valid_var.keys())] + + # Extract the arg_list dictionary + param = {valid_var[arg]: self.param[valid_var[arg]] for arg in valid_arg if valid_var[arg] in self.param} + + return valid_arg, param + + def set_simulation_time(self, + t0: float | None = None, + tstart: float | None = None, + tstop: float | None = None, + dt: float | None = None, + istep_out: int | None = None, + tstep_out: float | None = None, + istep_dump: int | None = None, + verbose: bool | None = None, + **kwargs: Any + ): + """ + + Parameters + ---------- + t0 : float, optional + The reference time for the start of the simulation. Defaults is 0.0 + tstart : float, optional + The start time for a restarted simulation. For a new simulation, tstart will be set to t0 automatically. + tstop : float, optional + The stopping time for a simulation. `tstop` must be greater than `tstart`. + dt : float, optional + The step size of the simulation. `dt` must be less than or equal to `tstop-dstart`. + istep_out : int, optional + The number of time steps between outputs to file. *Note*: only `istep_out` or `toutput` can be set. + tstep_out : float, optional + The approximate time between when outputs are written to file. Passing this computes + `istep_out = floor(tstep_out/dt)`. *Note*: only `istep_out` or `toutput` can be set. + istep_dump : int, optional + The anumber of time steps between outputs to dump file. If not set, this will be set to the value of + `istep_out` (or the equivalent value determined by `tstep_out`) + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + time_dict : dict + A dictionary containing the requested parameters + + """ + if t0 is None and tstart is None and tstop is None and dt is None and istep_out is None and \ + tstep_out is None and istep_dump is None: + return {} + + update_list = [] + + if t0 is None: + t0 = self.param.pop("T0", None) + if t0 is None: + t0 = 0.0 + else: + update_list.append("t0") + + if tstart is None: + tstart = self.param.pop("TSTART", None) + if tstart is None: + tstart = t0 + else: + update_list.append("tstart") + + self.param['T0'] = t0 + self.param['TSTART'] = tstart + + if tstop is None: + tstop = self.param.pop("TSTOP", None) + else: + update_list.append("tstop") + + if tstop is not None: + if tstop <= tstart: + warnings.warn("tstop must be greater than tstart.") + return {} + + if tstop is not None: + self.param['TSTOP'] = tstop + + if dt is None: + dt = self.param.pop("DT", None) + else: + update_list.append("dt") + + if dt is not None and tstop is not None: + if dt > (tstop - tstart): + warnings.warn("dt must be smaller than tstop-tstart") + warnings.warn(f"Setting dt = {tstop - tstart} instead of {dt}") + dt = tstop - tstart + + if dt is not None: + self.param['DT'] = dt + + if istep_out is None and tstep_out is None: + istep_out = self.param.pop("ISTEP_OUT", None) + elif istep_out is not None and tstep_out is not None: + warnings.warn("istep_out and tstep_out cannot both be set") + return {} + else: + update_list.append("istep_out") + + if tstep_out is not None and dt is not None: + istep_out = int(np.floor(tstep_out / dt)) + + if istep_out is not None: + self.param['ISTEP_OUT'] = istep_out + + if istep_dump is None: + istep_dump = self.param.pop("ISTEP_DUMP", None) + if istep_dump is None: + istep_dump = istep_out + else: + update_list.append("istep_dump") + + if istep_dump is not None: + self.param['ISTEP_DUMP'] = istep_dump + + time_dict = self.get_simulation_time(update_list, verbose=verbose) + + return time_dict + + def get_simulation_time(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs): + """ + + Returns a subset of the parameter dictionary containing the current simulation time parameters. + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list : str | List[str], optional + A single string or list of strings containing the names of the simulation time parameters to extract. + Default is all of: + ["t0", "tstart", "tstop", "dt", "istep_out", "tstep_out", "istep_dump"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + + Returns + ------- + time_dict : dict + A dictionary containing the requested parameters + + """ + + valid_var = {"t0": "T0", + "tstart": "TSTART", + "tstop": "TSTOP", + "dt": "DT", + "istep_out": "ISTEP_OUT", + "istep_dump": "ISTEP_DUMP", + } + + units = {"t0": self.TU_name, + "tstart": self.TU_name, + "tstop": self.TU_name, + "dt": self.TU_name, + "tstep_out": self.TU_name, + "istep_out": "", + "istep_dump": ""} + + tstep_out = None + if arg_list is None or "tstep_out" in arg_list or "istep_out" in arg_list: + if "ISTEP_OUT" in self.param and "DT" in self.param: + istep_out = self.param['ISTEP_OUT'] + dt = self.param['DT'] + tstep_out = istep_out * dt + + valid_arg, time_dict = self._get_valid_arg_list(arg_list, valid_var) + + if verbose is None: + verbose = self.verbose + + if verbose: + for arg in valid_arg: + key = valid_var[arg] + if key in time_dict: + print(f"{arg:<{self._getter_column_width}} {time_dict[key]} {units[arg]}") + else: + print(f"{arg:<{self._getter_column_width}} NOT SET") + if tstep_out is not None: + print(f"{'tstep_out':<{self._getter_column_width}} {tstep_out} {units['tstep_out']}") + + return time_dict + + def set_parameter(self, verbose: bool = True, **kwargs): + """ + Setter for all possible parameters. This will call each of the specialized setters using keyword arguments. + If no arguments are passed, then default values will be used. + Parameters + ---------- + **kwargs : Any argument listed listed in the Simulation class definition. + + Returns + ------- + param : A dictionary of all Simulation parameters that changed + + """ + + default_arguments = { + "codename" : "Swiftest", + "integrator": "symba", + "param_file": "param.in", + "t0": 0.0, + "tstart": 0.0, + "tstop": None, + "dt": None, + "istep_out": None, + "tstep_out": None, + "istep_dump": None, + "init_cond_file_type": "NETCDF_DOUBLE", + "init_cond_file_name": None, + "init_cond_format": "EL", + "output_file_type": "NETCDF_DOUBLE", + "output_file_name": None, + "output_format": "XVEL", + "MU": "MSUN", + "DU": "AU", + "TU": "Y", + "MU2KG": None, + "DU2M": None, + "TU2S": None, + "MU_name": None, + "DU_name": None, + "TU_name": None, + "rmin": constants.RSun / constants.AU2M, + "rmax": 10000.0, + "qmin_coord": "HELIO", + "gmtiny": None, + "mtiny": None, + "close_encounter_check": True, + "general_relativity": True, + "fragmentation": False, + "minimum_fragment_mass": None, + "minimum_fragment_gmass": None, + "rotation": False, + "compute_conservation_values": False, + "extra_force": False, + "big_discard": False, + "rhill_present": False, + "interaction_loops": "TRIANGULAR", + "encounter_check_loops": "TRIANGULAR", + "ephemeris_date": "MBCL", + "restart": False, + } + + # If no arguments (other than, possibly, verbose) are requested, use defaults + if len(kwargs) == 0: + kwargs = default_arguments + + # Add the verbose flag to the kwargs for passing down to the individual setters + kwargs["verbose"] = verbose + + param_file = kwargs.pop("param_file",None) + + if param_file is not None: + self.param_file = os.path.realpath(param_file) + self.sim_dir = os.path.dirname(self.param_file) + + + # Setters returning parameter dictionary values + param_dict = {} + param_dict.update(self.set_unit_system(**kwargs)) + param_dict.update(self.set_integrator(**kwargs)) + param_dict.update(self.set_simulation_time(**kwargs)) + param_dict.update(self.set_init_cond_files(**kwargs)) + param_dict.update(self.set_output_files(**kwargs)) + param_dict.update(self.set_distance_range(**kwargs)) + param_dict.update(self.set_feature(**kwargs)) + + # Non-returning setters + self.set_ephemeris_date(**kwargs) + + return param_dict + + def get_parameter(self, **kwargs): + """ + Setter for all possible parameters. Calls each of the specialized setters using keyword arguments + Parameters + ---------- + **kwargs : Any of the arguments defined in Simulation. If none provided, it returns all arguments. + + Returns + ------- + param : A dictionary of all Simulation parameters requested + + """ + + # Getters returning parameter dictionary values + param_dict = {} + param_dict.update(self.get_integrator(**kwargs)) + param_dict.update(self.get_simulation_time(**kwargs)) + param_dict.update(self.get_init_cond_files(**kwargs)) + param_dict.update(self.get_output_files(**kwargs)) + param_dict.update(self.get_distance_range(**kwargs)) + param_dict.update(self.get_unit_system(**kwargs)) + param_dict.update(self.get_feature(**kwargs)) + + self.get_ephemeris_date(**kwargs) + + return param_dict + + def set_integrator(self, + codename: Literal["swiftest", "swifter", "swift"] | None = None, + integrator: Literal["symba","rmvs","whm","helio"] | None = None, + mtiny: float | None = None, + gmtiny: float | None = None, + verbose: bool | None = None, + **kwargs: Any + ): + """ + + Parameters + ---------- + codename : {"swiftest", "swifter", "swift"}, optional + integrator : {"symba","rmvs","whm","helio"}, optional + Name of the n-body integrator that will be used when executing a run. + mtiny : float, optional + The minimum mass of fully interacting bodies. Bodies below this mass interact with the larger bodies, + but not each other (SyMBA only). *Note.* Only mtiny or gmtiny is accepted, not both. + gmtiny : float, optional + The minimum G*mass of fully interacting bodies. Bodies below this mass interact with the larger bodies, + but not each other (SyMBA only). *Note.* Only mtiny or gmtiny is accepted, not both. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + integrator_dict: dict + A dictionary containing the subset of the parameter dictonary that was updated by this setter + + """ + # TODO: Improve how it finds the executable binary + + update_list = [] + + if codename is not None: + valid_codename = ["Swiftest", "Swifter", "Swift"] + if codename.title() not in valid_codename: + warnings.warn(f"{codename} is not a valid codename. Valid options are ",",".join(valid_codename)) + try: + self.codename + except: + self.codename = valid_codename[0] + else: + self.codename = codename.title() + + self.param['! VERSION'] = f"{self.codename} input file" + update_list.append("codename") + if self.codename == "Swiftest": + self.binary_path = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(_pyfile)),os.pardir,os.pardir,os.pardir,"bin")) + self.driver_executable = os.path.join(self.binary_path,"swiftest_driver") + if not os.path.exists(self.driver_executable): + warnings.warn(f"Cannot find the Swiftest driver in {self.binary_path}") + self.driver_executable = None + else: + self.binary_path = "NOT IMPLEMENTED FOR THIS CODE" + self.driver_executable = None + update_list.append("driver_executable") + + if integrator is not None: + valid_integrator = ["symba","rmvs","whm","helio"] + if integrator.lower() not in valid_integrator: + warnings.warn(f"{integrator} is not a valid integrator. Valid options are ",",".join(valid_integrator)) + try: + self.integrator + except: + self.integrator = valid_integrator[0] + else: + self.integrator = integrator.lower() + update_list.append("integrator") + + if mtiny is not None or gmtiny is not None: + if self.integrator != "symba": + warnings.warn("mtiny and gmtiny are only used by SyMBA.") + if mtiny is not None and gmtiny is not None: + warnings.warn("Only set mtiny or gmtiny, not both!") + elif gmtiny is not None: + self.param['GMTINY'] = gmtiny + update_list.append("gmtiny") + elif mtiny is not None: + self.param['GMTINY'] = self.GU * mtiny + update_list.append("gmtiny") + + integrator_dict = self.get_integrator(update_list, verbose) + + return integrator_dict + + def get_integrator(self,arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs: Any): + """ + + Returns a subset of the parameter dictionary containing the current values of the distance range parameters. + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list: str | List[str], optional + A single string or list of strings containing the names of the features to extract. Default is all of: + ["integrator"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + integrator_dict : dict + The subset of the dictionary containing the code name if codename is selected + """ + + valid_var = {"gmtiny" : "GMTINY"} + + valid_instance_vars = {"codename": self.codename, + "integrator": self.integrator, + "param_file": self.param_file, + "driver_executable": self.driver_executable} + + try: + self.integrator + except: + warnings.warn(f"integrator is not set") + return {} + + try: + self.codename + except: + warnings.warn(f"codename is not set") + return {} + + if verbose is None: + verbose = self.verbose + + if not bool(kwargs) and arg_list is None: + arg_list = list(valid_instance_vars.keys()) + arg_list.append(*[a for a in valid_var.keys() if a not in valid_instance_vars]) + + valid_arg, integrator_dict = self._get_valid_arg_list(arg_list, valid_var) + + if verbose: + for arg in arg_list: + if arg in valid_instance_vars: + print(f"{arg:<{self._getter_column_width}} {valid_instance_vars[arg]}") + for arg in valid_arg: + key = valid_var[arg] + if key in integrator_dict: + if arg == "gmtiny": + if self.integrator == "symba": + print(f"{arg:<{self._getter_column_width}} {integrator_dict[key]} {self.DU_name}^3 / {self.TU_name}^2 ") + print(f"{'mtiny':<{self._getter_column_width}} {integrator_dict[key] / self.GU} {self.MU_name}") + else: + print(f"{arg:<{self._getter_column_width}} {integrator_dict[key]}") + else: + print(f"{arg:<{self._getter_column_width}} NOT SET") + + + return integrator_dict + + def set_feature(self, + close_encounter_check: bool | None = None, + general_relativity: bool | None = None, + fragmentation: bool | None = None, + minimum_fragment_gmass: float | None = None, + minimum_fragment_mass: float | None = None, + rotation: bool | None = None, + compute_conservation_values: bool | None = None, + extra_force: bool | None = None, + big_discard: bool | None = None, + rhill_present: bool | None = None, + restart: bool | None = None, + tides: bool | None = None, + interaction_loops: Literal["TRIANGULAR", "FLAT", "ADAPTIVE"] | None = None, + encounter_check_loops: Literal["TRIANGULAR", "SORTSWEEP", "ADAPTIVE"] | None = None, + verbose: bool | None = None, + **kwargs: Any + ): + """ + Turns on or off various features of a simulation. + + Parameters + ---------- + close_encounter_check : bool, optional + Check for close encounters between bodies. If set to True, then the radii of massive bodies must be included + in initial conditions. + general_relativity : bool, optional + Include the post-Newtonian correction in acceleration calculations. + fragmentation : bool, optional + If set to True, this turns on the Fraggle fragment generation code and `rotation` must also be True. + This argument only applies to Swiftest-SyMBA simulations. It will be ignored otherwise. + minimum_fragment_gmass : float, optional + If fragmentation is turned on, this sets the mimimum G*mass of a collisional fragment that can be generated. + *Note.* Only set one of minimum_fragment_gmass or minimum_fragment_mass + minimum_fragment_mass : float, optional + If fragmentation is turned on, this sets the mimimum mass of a collisional fragment that can be generated. + *Note.* Only set one of minimum_fragment_gmass or minimum_fragment_mass + rotation : bool, optional + If set to True, this turns on rotation tracking and radius, rotation vector, and moments of inertia values + must be included in the initial conditions. + This argument only applies to Swiftest-SyMBA simulations. It will be ignored otherwise. + compute_conservation_values : bool, optional + Turns on the computation of energy, angular momentum, and mass conservation and reports the values + every output step of a running simulation. + extra_force: bool, optional + Turns on user-defined force function. + big_discard: bool, optional + Includes big bodies when performing a discard (Swifter only) + rhill_present: bool, optional + Include the Hill's radius with the input files. + interaction_loops : {"TRIANGULAR","FLAT","ADAPTIVE"}, default "TRIANGULAR" + *Swiftest Experimental feature* + Specifies which algorithm to use for the computation of body-body gravitational forces. + * "TRIANGULAR" : Upper-triangular double-loops . + * "FLAT" : Body-body interation pairs are flattened into a 1-D array. + * "ADAPTIVE" : Periodically times the TRIANGULAR and FLAT methods and determines which one to use based on + the wall time to complete the loop. *Note:* Using ADAPTIVE means that bit-identical repeatability cannot + be assured, as the choice of algorithm depends on possible external factors that can influence the wall + time calculation. The exact floating-point results of the interaction will be different between the two + algorithm types. + encounter_check_loops : {"TRIANGULAR","SORTSWEEP","ADAPTIVE"}, default "TRIANGULAR" + *Swiftest Experimental feature* + Specifies which algorithm to use for checking whether bodies are in a close encounter state or not. + * "TRIANGULAR" : Upper-triangular double-loops. + * "SORTSWEEP" : A Sort-Sweep algorithm is used to reduce the population of potential close encounter bodies. + This algorithm is still in development, and does not necessarily speed up the encounter checking. + Use with caution. + * "ADAPTIVE" : Periodically times the TRIANGULAR and SORTSWEEP methods and determines which one to use based + on the wall time to complete the loop. *Note:* Using ADAPTIVE means that bit-identical repeatability cannot + be assured, as the choice of algorithm depends on possible external factors that can influence the wall + time calculation. The exact floating-point results of the interaction will be different between the two + algorithm types. + tides: bool, optional + Turns on tidal model (IN DEVELOPMENT - IGNORED) + Yarkovsky: bool, optional + Turns on Yarkovsky model (IN DEVELOPMENT - IGNORED) + YORP: bool, optional + Turns on YORP model (IN DEVELOPMENT - IGNORED) + restart : bool, default False + If true, will restart an old run. The file given by `output_file_name` must exist before the run can + execute. If false, will start a new run. If the file given by `output_file_name` exists, it will be replaced + when the run is executed. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + feature_dict : dict + A dictionary containing the requested features. + + """ + + update_list = [] + if close_encounter_check is not None: + self.param["CHK_CLOSE"] = close_encounter_check + update_list.append("close_encounter_check") + + if general_relativity is not None: + self.param["GR"] = general_relativity + update_list.append("general_relativity") + + if fragmentation is not None: + if self.codename != "Swiftest" and self.integrator != "symba" and fragmentation: + warnings.warn("Fragmentation is only available on Swiftest SyMBA.") + self.param['FRAGMENTATION'] = False + else: + self.param['FRAGMENTATION'] = fragmentation + update_list.append("fragmentation") + if fragmentation: + if "MIN_GMFRAG" not in self.param and minimum_fragment_mass is None and minimum_fragment_gmass is None: + warnings.warn("Minimum fragment mass is not set. Set it using minimum_fragment_gmass or minimum_fragment_mass") + else: + update_list.append("minimum_fragment_gmass") + + if minimum_fragment_gmass is not None and minimum_fragment_mass is not None: + warnings.warn("Only set either minimum_fragment_mass or minimum_fragment_gmass, but not both!") + + if minimum_fragment_gmass is not None: + self.param["MIN_GMFRAG"] = minimum_fragment_gmass + if "minmum_fragment_gmass" not in update_list: + update_list.append("minimum_fragment_gmass") + elif minimum_fragment_mass is not None: + self.param["MIN_GMFRAG"] = minimum_fragment_mass * self.GU + if "minimum_fragment_gmass" not in update_list: + update_list.append("minimum_fragment_gmass") + + if rotation is not None: + self.param['ROTATION'] = rotation + update_list.append("rotation") + + if self.param['FRAGMENTATION'] and not self.param['ROTATION']: + self.param['ROTATION'] = True + update_list.append("rotation") + + if compute_conservation_values is not None: + self.param["ENERGY"] = compute_conservation_values + update_list.append("compute_conservation_values") + + if extra_force is not None: + self.param["EXTRA_FORCE"] = extra_force + update_list.append("extra_force") + + if big_discard is not None: + if self.codename != "Swifter": + self.param["BIG_DISCARD"] = False + else: + self.param["BIG_DISCARD"] = big_discard + update_list.append("big_discard") + + if rhill_present is not None: + self.param["RHILL_PRESENT"] = rhill_present + update_list.append("rhill_present") + + if restart is not None: + self.param["RESTART"] = restart + update_list.append("restart") + + if interaction_loops is not None: + valid_vals = ["TRIANGULAR", "FLAT", "ADAPTIVE"] + if interaction_loops not in valid_vals: + warnings.warn(f"{interaction_loops} is not a valid option for interaction loops.") + warnings.warn(f"Must be one of {valid_vals}") + if "INTERACTION_LOOPS" not in self.param: + self.param["INTERACTION_LOOPS"] = valid_vals[0] + else: + self.param["INTERACTION_LOOPS"] = interaction_loops + update_list.append("interaction_loops") + + if encounter_check_loops is not None: + valid_vals = ["TRIANGULAR", "SORTSWEEP", "ADAPTIVE"] + if encounter_check_loops not in valid_vals: + warnings.warn(f"{encounter_check_loops} is not a valid option for interaction loops.") + warnings.warn(f"Must be one of {valid_vals}") + if "ENCOUNTER_CHECK" not in self.param: + self.param["ENCOUNTER_CHECK"] = valid_vals[0] + else: + self.param["ENCOUNTER_CHECK"] = encounter_check_loops + update_list.append("encounter_check_loops") + + self.param["TIDES"] = False + + feature_dict = self.get_feature(update_list, verbose) + return feature_dict + + def get_feature(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs: Any): + """ + + Returns a subset of the parameter dictionary containing the current value of the feature boolean values. + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list: str | List[str], optional + A single string or list of strings containing the names of the features to extract. Default is all of: + ["close_encounter_check", "general_relativity", "fragmentation", "rotation", "compute_conservation_values"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + feature_dict : dict + A dictionary containing the requested features. + + """ + + valid_var = {"close_encounter_check": "CHK_CLOSE", + "fragmentation": "FRAGMENTATION", + "minimum_fragment_gmass": "MIN_GMFRAG", + "rotation": "ROTATION", + "general_relativity": "GR", + "compute_conservation_values": "ENERGY", + "rhill_present": "RHILL_PRESENT", + "extra_force": "EXTRA_FORCE", + "big_discard": "BIG_DISCARD", + "interaction_loops": "INTERACTION_LOOPS", + "encounter_check_loops": "ENCOUNTER_CHECK", + "restart": "RESTART" + } + + valid_arg, feature_dict = self._get_valid_arg_list(arg_list, valid_var) + + if verbose is None: + verbose = self.verbose + + if verbose: + for arg in valid_arg: + key = valid_var[arg] + if key in feature_dict: + if arg == "minimum_fragment_gmass": + if self.param['FRAGMENTATION']: + print(f"{arg:<{self._getter_column_width}} {feature_dict[key]} {self.DU_name}^3 / {self.TU_name}^2 ") + print(f"{'minimum_fragment_mass':<{self._getter_column_width}} {feature_dict[key] / self.GU} {self.MU_name}") + else: + print(f"{arg:<{self._getter_column_width}} {feature_dict[key]}") + else: + print(f"{arg:<{self._getter_column_width}} NOT SET") + + + return feature_dict + + def set_init_cond_files(self, + init_cond_file_type: Literal["NETCDF_DOUBLE", "NETCDF_FLOAT", "ASCII"] | None = None, + init_cond_file_name: str | os.PathLike | Dict[str, str] | + Dict[ str, os.PathLike] | None = None, + init_cond_format: Literal["EL", "XV"] | None = None, + verbose: bool | None = None, + **kwargs: Any + ): + """ + Sets the initial condition file parameters in the parameters dictionary. + + Parameters + ---------- + init_cond_file_type : {"NETCDF_DOUBLE", "NETCDF_FLOAT", "ASCII"}, optional + The file type containing initial conditions for the simulation: + * NETCDF_DOUBLE: A single initial conditions input file in NetCDF file format of type NETCDF_DOUBLE + * NETCDF_FLOAT: A single initial conditions input file in NetCDF file format of type NETCDF_FLOAT + * ASCII : Three initial conditions files in ASCII format. The individual files define the central body, + massive body, and test particle initial conditions. + init_cond_file_name : str, path-like, or dict, optional + Name of the input initial condition file or files. Whether to pass a single file name or a dictionary + depends on the argument passed to `init_cond_file_type`: If `init_cond_file_type={"NETCDF_DOUBLE","NETCDF_FLOAT"}`, + then this will be a single file name. If `init_cond_file_type="ASCII"` then this must be a dictionary where: + ```init_cond_file_name = { + "CB" : *path to central body initial conditions file* (Swiftest only), + "PL" : *path to massive body initial conditions file*, + "TP" : *path to test particle initial conditions file* + }``` + If no file name is provided then the following default file names will be used. + * NETCDF_DOUBLE, NETCDF_FLOAT: `init_cond_file_name = "init_cond.nc"` + * ASCII: `init_cond_file_name = {"CB" : "cb.in", "PL" : "pl.in", "TP" : "tp.in"}` + init_cond_format : {"EL", "XV"} + Indicates whether the input initial conditions are given as orbital elements or cartesian position and + velocity vectors. + > *Note:* If `codename` is "Swift" or "Swifter", EL initial conditions are converted to XV. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + init_cond_file_dict : dict + A dictionary containing the requested parameters + + """ + + update_list = [] + if init_cond_file_name is not None: + update_list.append("init_cond_file_name") + if init_cond_file_type is not None: + update_list.append("init_cond_file_type") + if init_cond_format is not None: + update_list.append("init_cond_format") + + if len(update_list) == 0: + return {} + + def ascii_file_input_error_msg(codename): + warnings.warn(f"in set_init_cond_files: init_cond_file_name must be a dictionary of the form: ") + warnings.warn('{') + if codename == "Swiftest": + warnings.warn('"CB" : *path to central body initial conditions file*,') + warnings.warn('"PL" : *path to massive body initial conditions file*,') + warnings.warn('"TP" : *path to test particle initial conditions file*') + warnings.warn('}') + return {} + + if init_cond_format is None: + if "IN_FORM" in self.param: + init_cond_format = self.param['IN_FORM'] + else: + init_cond_format = "EL" + + if init_cond_file_type is None: + if "IN_TYPE" in self.param: + init_cond_file_type = self.param['IN_TYPE'] + else: + init_cond_file_type = "NETCDF_DOUBLE" + + if self.codename.title() == "Swiftest": + init_cond_keys = ["CB", "PL", "TP"] + else: + init_cond_keys = ["PL", "TP"] + if init_cond_file_type != "ASCII": + warnings.warn(f"{init_cond_file_type} is not supported by {self.codename}. Using ASCII instead") + init_cond_file_type = "ASCII" + if init_cond_format != "XV": + warnings.warn(f"{init_cond_format} is not supported by {self.codename}. Using XV instead") + init_cond_format = "XV" + + valid_formats = {"EL", "XV"} + if init_cond_format not in valid_formats: + warnings.warn(f"{init_cond_format} is not a valid input format") + else: + self.param['IN_FORM'] = init_cond_format + + valid_types = {"NETCDF_DOUBLE", "NETCDF_FLOAT", "ASCII"} + if init_cond_file_type not in valid_types: + warnings.warn(f"{init_cond_file_type} is not a valid input type") + else: + self.param['IN_TYPE'] = init_cond_file_type + + if init_cond_file_type == "ASCII": + if init_cond_file_name is None: + # No file names passed, so we will just use the defaults. + for key in init_cond_keys: + self.param[f"{key}_IN"] = f"{key.lower()}.in" + elif type(init_cond_file_name) is not dict: + # Oops, accidentally passed a single string or path-like instead of the expected dictionary for ASCII + # input type. + ascii_file_input_error_msg(self.codename) + elif not all(key in init_cond_file_name for key in init_cond_keys): + # This is the case where the dictionary doesn't have all the keys we expect. Print an error message. + ascii_file_input_error_msg(self.codename) + else: + # A valid initial conditions file dictionary was passed. + for key in init_cond_keys: + self.param[f"{key}_IN"] = init_cond_file_name[key] + else: + if init_cond_file_name is None: + # No file names passed, so we will just use the default. + self.param["NC_IN"] = "init_cond.nc" + elif type(init_cond_file_name) is dict: + # Oops, accidentally passed a dictionary instead of the expected single string or path-like for NetCDF + # input type. + warnings.warn(f"Only a single input file is used for NetCDF files") + else: + self.param["NC_IN"] = init_cond_file_name + + init_cond_file_dict = self.get_init_cond_files(update_list, verbose) + + return init_cond_file_dict + + def get_init_cond_files(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs): + """ + + Returns a subset of the parameter dictionary containing the current initial condition file parameters + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list : str | List[str], optional + A single string or list of strings containing the names of the simulation time parameters to extract. + Default is all of: + ["init_cond_file_type", "init_cond_file_name", "init_cond_format"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + + Returns + ------- + init_cond_file_dict : dict + A dictionary containing the requested parameters + + """ + + valid_var = {"init_cond_file_type": "IN_TYPE", + "init_cond_format": "IN_FORM", + "init_cond_file_name": "NC_IN", + "init_cond_file_name['CB']": "CB_IN", + "init_cond_file_name['PL']": "PL_IN", + "init_cond_file_name['TP']": "TP_IN", + } + + three_file_args = ["init_cond_file_name['CB']", + "init_cond_file_name['PL']", + "init_cond_file_name['TP']"] + + if self.codename == "Swifter": + three_file_args.remove("init_cond_file_name['CB']") + valid_var.pop("init_cond_file_name['CB']",None) + + # We have to figure out which initial conditions file model we are using (1 vs. 3 files) + if arg_list is None: + valid_arg = list(valid_var.keys()) + elif type(arg_list) is str: + valid_arg = [arg_list] + else: + valid_arg = [k for k in arg_list if k in list(valid_var.keys())] + + # Figure out which input file model we need to use + if "init_cond_file_name" in valid_arg: + if self.param["IN_TYPE"] == "ASCII": + valid_arg.remove("init_cond_file_name") + for key in three_file_args: + if key not in valid_arg: + valid_arg.append(key) + else: + for key in three_file_args: + if key in valid_arg: + valid_arg.remove(key) + + valid_arg, init_cond_file_dict = self._get_valid_arg_list(valid_arg, valid_var) + + if verbose is None: + verbose = self.verbose + + if verbose: + for arg in valid_arg: + key = valid_var[arg] + print(f"{arg:<{self._getter_column_width}} {init_cond_file_dict[key]}") + + return init_cond_file_dict + + def set_output_files(self, + output_file_type: Literal[ + "NETCDF_DOUBLE", "NETCDF_FLOAT", "REAL4", "REAL8", "XDR4", "XDR8"] | None = None, + output_file_name: os.PathLike | str | None = None, + output_format: Literal["XV", "XVEL"] | None = None, + restart: bool | None = None, + verbose: bool | None = None, + **kwargs: Any + ): + """ + Sets the output file parameters in the parameter dictionary. + + Parameters + ---------- + output_file_type : {"NETCDF_DOUBLE", "NETCDF_FLOAT","REAL4","REAL8","XDR4","XDR8"}, optional + The file type for the outputs of the simulation. Compatible file types depend on the `codename` argument. + * Swiftest: Only "NETCDF_DOUBLE" or "NETCDF_FLOAT" supported. + * Swifter: Only "REAL4","REAL8","XDR4" or "XDR8" supported. + * Swift: Only "REAL4" supported. + output_file_name : str or path-like, optional + Name of output file to generate. If not supplied, then one of the default file names are used, depending on + the value passed to `output_file_type`. If one of the NetCDF types are used, the default is "bin.nc". + Otherwise, the default is "bin.dat". + output_format : {"XV","XVEL"}, optional + Specifies the format for the data saved to the output file. If "XV" then cartesian position and velocity + vectors for all bodies are stored. If "XVEL" then the orbital elements are also stored. + restart: bool, optional + Indicates whether this is a restart of an old run or a new run. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + output_file_dict : dict + A dictionary containing the requested parameters + + """ + update_list = [] + if output_file_type is not None: + update_list.append("output_file_type") + if output_file_name is not None: + update_list.append("output_file_name") + if output_format is not None: + update_list.append("output_format") + if restart is not None: + self.restart = restart + update_list.append("restart") + if len(update_list) == 0: + return {} -class Simulation: - """ - This is a class that defines the basic Swift/Swifter/Swiftest simulation object - """ - def __init__(self, codename="Swiftest", param_file="param.in", readbin=False, verbose=True): - self.ds = xr.Dataset() - self.param = { - '! VERSION': f"Swiftest parameter input", - 'T0': "0.0", - 'TSTOP': "0.0", - 'DT': "0.0", - 'IN_FORM': "XV", - 'IN_TYPE': "NETCDF_DOUBLE", - 'NC_IN' : "init_cond.nc", - 'CB_IN' : "cb.in", - 'PL_IN' : "pl.in", - 'TP_IN' : "tp.in", - 'ISTEP_OUT': "1", - 'ISTEP_DUMP': "1", - 'BIN_OUT': "bin.nc", - 'OUT_TYPE': 'NETCDF_DOUBLE', - 'OUT_FORM': "XVEL", - 'OUT_STAT': "REPLACE", - 'CHK_RMAX': "-1.0", - 'CHK_EJECT': "-1.0", - 'CHK_RMIN': "-1.0", - 'CHK_QMIN': "-1.0", - 'CHK_QMIN_COORD': "HELIO", - 'CHK_QMIN_RANGE': "-1.0 -1.0", - 'ENC_OUT': "", - 'EXTRA_FORCE': "NO", - 'DISCARD_OUT': "", - 'PARTICLE_OUT' : "", - 'BIG_DISCARD': "NO", - 'CHK_CLOSE': "YES", - 'RHILL_PRESENT': "YES", - 'FRAGMENTATION': "NO", - 'ROTATION': "NO", - 'TIDES': "NO", - 'ENERGY': "NO", - 'GR': "YES", - 'INTERACTION_LOOPS': "TRIANGULAR", - 'ENCOUNTER_CHECK': "TRIANGULAR" - } - self.codename = codename - self.verbose = verbose - self.set_unit_system() - - # If the parameter file is in a different location than the current working directory, we will need - # to use it to properly open bin files - self.sim_dir = os.path.dirname(os.path.realpath(param_file)) - if os.path.exists(param_file): - self.read_param(param_file, codename=codename, verbose=self.verbose) - if readbin: - binpath = os.path.join(self.sim_dir,self.param['BIN_OUT']) - if os.path.exists(binpath): - self.bin2xr() + if self.codename == "Swiftest": + if output_file_type is None: + output_file_type = self.param.pop("OUT_TYPE", None) + if output_file_type is None: + output_file_type = "NETCDF_DOUBLE" + elif output_file_type not in ["NETCDF_DOUBLE", "NETCDF_FLOAT"]: + warnings.warn(f"{output_file_type} is not compatible with Swiftest. Setting to NETCDF_DOUBLE") + output_file_type = "NETCDF_DOUBLE" + elif self.codename == "Swifter": + if output_file_type is None: + output_file_type = self.param.pop("OUT_TYPE", None) + if output_file_type is None: + output_file_type = "REAL8" + elif output_file_type not in ["REAL4", "REAL8", "XDR4", "XDR8"]: + warnings.warn(f"{output_file_type} is not compatible with Swifter. Setting to REAL8") + output_file_type = "REAL8" + elif self.codename == "Swift": + if output_file_type is None: + output_file_type = self.param.pop("OUT_TYPE", None) + if output_file_type is None: + output_file_type = "REAL4" + if output_file_type not in ["REAL4"]: + warnings.warn(f"{output_file_type} is not compatible with Swift. Setting to REAL4") + output_file_type = "REAL4" + + self.param['OUT_TYPE'] = output_file_type + if output_file_name is None: + if output_file_type in ["NETCDF_DOUBLE", "NETCDF_FLOAT"]: + self.param['BIN_OUT'] = "bin.nc" else: - print(f"BIN_OUT file {binpath} not found.") - return + self.param['BIN_OUT'] = "bin.dat" + else: + self.param['BIN_OUT'] = output_file_name + + if output_format != "XV" and self.codename != "Swiftest": + warnings.warn(f"{output_format} is not compatible with {self.codename}. Setting to XV") + output_format = "XV" + self.param["OUT_FORM"] = output_format + + if self.restart: + self.param["OUT_STAT"] = "APPEND" + else: + self.param["OUT_STAT"] = "REPLACE" + + output_file_dict = self.get_output_files(update_list, verbose=verbose) + + return output_file_dict + + def get_output_files(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs): + """ + + Returns a subset of the parameter dictionary containing the current output file parameters + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list : str | List[str], optional + A single string or list of strings containing the names of the simulation time parameters to extract. + Default is all of: + ["output_file_type", "output_file_name", "output_format"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + + Returns + ------- + output_file_dict : dict + A dictionary containing the requested parameters + + """ + + valid_var = {"output_file_type": "OUT_TYPE", + "output_file_name": "BIN_OUT", + "output_format": "OUT_FORM", + "restart": "OUT_STAT" + } + + valid_arg, output_file_dict = self._get_valid_arg_list(arg_list, valid_var) + if verbose is None: + verbose = self.verbose - def set_unit_system(self,MU="Msun",DU="AU",TU="YR",MU2KG=None,DU2M=None,TU2S=None): + if verbose: + for arg in valid_arg: + key = valid_var[arg] + print(f"{arg:<{self._getter_column_width}} {output_file_dict[key]}") + + return output_file_dict + + def set_unit_system(self, + MU: str | None = None, + DU: str | None = None, + TU: str | None = None, + MU2KG: float | None = None, + DU2M: float | None = None, + TU2S: float | None = None, + MU_name: str | None = None, + DU_name: str | None = None, + TU_name: str | None = None, + recompute_unit_values: bool = True, + verbose: bool | None = None, + **kwargs: Any): """ Setter for setting the unit conversion between one of the standard sets. @@ -96,200 +1527,836 @@ def set_unit_system(self,MU="Msun",DU="AU",TU="YR",MU2KG=None,DU2M=None,TU2S=Non Parameters ---------- - MU : str, default "Msun" + MU : str, optional The mass unit system to use. Case-insensitive valid options are: "Msun" : Solar mass "Mearth" : Earth mass "kg" : kilograms "g" : grams - - DU : str, default "AU" + DU : str, optional The distance unit system to use. Case-insensitive valid options are: "AU" : Astronomical Unit "Rearth" : Earth radius "m" : meter "cm" : centimeter - - TU : str, default "YR" + TU : str, optional The time unit system to use. Case-insensitive valid options are: "YR" : Year "DAY" : Julian day "d" : Julian day "JD" : Julian day "s" : second + MU2KG : float, optional + The conversion factor to multiply by the mass unit that would convert it to kilogram. + Setting this overrides MU + DU2M : float, optional + The conversion factor to multiply by the distance unit that would convert it to meter. + Setting this overrides DU + TU2S : float, optional + The conversion factor to multiply by the time unit that would convert it to seconds. + Setting this overrides TU + MU_name : str, optional + The name of the mass unit. When setting one of the standard units via `MU` a name will be + automatically set for the unit, so this argument will override the automatic name. + DU_name : str, optional + The name of the distance unit. When setting one of the standard units via `DU` a name will be + automatically set for the unit, so this argument will override the automatic name. + TU_name : str, optional + The name of the time unit. When setting one of the standard units via `TU` a name will be + automatically set for the unit, so this argument will override the automatic name. + recompute_unit_values : bool, default True + Recompute all values into the new unit system. + >*Note:* This is a destructive operation, however if not executed then the values contained in the parameter + > file and input/output data files computed previously may not be consistent with the new unit conversion + > factors. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ---------- + unit_dict : dict + A dictionary containing the requested unit conversion parameters + """ + + MU2KG_old = None + DU2M_old = None + TU2S_old = None + + if "MU_name" not in dir(self): + self.MU_name = None + if "DU_name" not in dir(self): + self.DU_name = None + if "TU_name" not in dir(self): + self.TU_name = None + + update_list = [] + if MU is not None or MU2KG is not None: + update_list.append("MU") + if DU is not None or DU2M is not None: + update_list.append("DU") + if TU is not None or TU2S is not None: + update_list.append("TU") + + if MU2KG is not None or MU is not None: + MU2KG_old = self.param.pop('MU2KG', None) + if MU2KG is not None: + self.param['MU2KG'] = MU2KG + self.MU_name = None + else: + if MU.upper() == "MSUN": + self.param['MU2KG'] = constants.MSun + self.MU_name = "MSun" + elif MU.upper() == "MEARTH": + self.param['MU2KG'] = constants.MEarth + self.MU_name = "MEarth" + elif MU.upper() == "KG": + self.param['MU2KG'] = 1.0 + self.MU_name = "kg" + elif MU.upper() == "G": + self.param['MU2KG'] = 1000.0 + self.MU_name = "g" + else: + warnings.warn(f"{MU} not a recognized unit system. Using MSun as a default.") + self.param['MU2KG'] = constants.MSun + self.MU_name = "MSun" + + if DU2M is not None or DU is not None: + DU2M_old = self.param.pop('DU2M', None) + if DU2M is not None: + self.param['DU2M'] = DU2M + self.DU_name = None + else: + if DU.upper() == "AU": + self.param['DU2M'] = constants.AU2M + self.DU_name = "AU" + elif DU.upper() == "REARTH": + self.param['DU2M'] = constants.REarth + self.DU_name = "REarth" + elif DU.upper() == "M": + self.param['DU2M'] = 1.0 + self.DU_name = "m" + elif DU.upper() == "CM": + self.param['DU2M'] = 100.0 + self.DU_name = "cm" + else: + warnings.warn(f"{DU} not a recognized unit system. Using AU as a default.") + self.param['DU2M'] = constants.AU2M + self.DU_name = "AU" + + if TU2S is not None or TU is not None: + TU2S_old = self.param.pop('TU2S', None) + if TU2S is not None: + self.param['TU2S'] = TU2S + self.TU_name = None + else: + if TU.upper() == "YR" or TU.upper() == "Y" or TU.upper() == "YEAR" or TU.upper() == "YEARS": + self.param['TU2S'] = constants.YR2S + self.TU_name = "y" + elif TU.upper() == "DAY" or TU.upper() == "D" or TU.upper() == "JD" or TU.upper() == "DAYS": + self.param['TU2S'] = constants.JD2S + self.TU_name = "d" + elif TU.upper() == "S" or TU.upper() == "SECONDS" or TU.upper() == "SEC": + self.param['TU2S'] = 1.0 + self.TU_name = "s" + else: + warnings.warn(f"{TU} not a recognized unit system. Using YR as a default.") + self.param['TU2S'] = constants.YR2S + self.TU_name = "y" + + if MU_name is not None: + self.MU_name = MU_name + if DU_name is not None: + self.DU_name = DU_name + if TU_name is not None: + self.TU_name = TU_name + + if "DU_name" in dir(self) and "TU_name" in dir(self): + self.VU_name = f"{self.DU_name}/{self.TU_name}" + if all(key in self.param for key in ["MU2KG","DU2M","TU2S"]): + self.GU = constants.GC * self.param["TU2S"] ** 2 * self.param["MU2KG"] / self.param["DU2M"] ** 3 + + if recompute_unit_values: + self.update_param_units(MU2KG_old, DU2M_old, TU2S_old) + + unit_dict = self.get_unit_system(update_list, verbose) + + return unit_dict + + def get_unit_system(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs): + """ + + Returns a subset of the parameter dictionary containing the current simulation unit system. + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list : str | List[str], optional + A single string or list of strings containing the names of the simulation unit system + Default is all of: + ["MU", "DU", "TU"] + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + unit_dict : dict + A dictionary containing the requested unit conversion parameters + + """ + + valid_var = { + "MU": "MU2KG", + "DU": "DU2M", + "TU": "TU2S", + } + + if "MU_name" not in dir(self) or self.MU_name is None: + MU_name = "mass unit" + else: + MU_name = self.MU_name + if "DU_name" not in dir(self) or self.DU_name is None: + DU_name = "distance unit" + else: + DU_name = self.DU_name + if "TU_name" not in dir(self) or self.TU_name is None: + TU_name = "time unit" + else: + TU_name = self.TU_name + + units1 = { + "MU": MU_name, + "DU": DU_name, + "TU": TU_name + } + units2 = { + "MU": f"kg / {MU_name}", + "DU": f"m / {DU_name}", + "TU": f"s / {TU_name}" + } + + valid_arg, unit_dict = self._get_valid_arg_list(arg_list, valid_var) + + if verbose is None: + verbose = self.verbose + + if verbose: + for arg in valid_arg: + key = valid_var[arg] + col_width = str(int(self._getter_column_width) - 4) + print(f"{arg}: {units1[arg]:<{col_width}} {unit_dict[key]} {units2[arg]}") - MU2KG : float, default `None` - The conversion factor to multiply by the mass unit that would convert it to kilogram. Setting this overrides MU + return unit_dict - DU2M : float, default `None` - The conversion factor to multiply by the distance unit that would convert it to meter. Setting this overrides DU + def update_param_units(self, MU2KG_old, DU2M_old, TU2S_old): + """ + Updates the values of parameters that have units when the units have changed. - TU2S : float, default `None` - The conversion factor to multiply by the time unit that would convert it to seconds. Setting this overrides TU + Parameters + ---------- + MU2KG_old : Old value of the mass unit conversion factor + DU2M_old : Old value of the distance unit conversion factor + TU2S_old : Old value of the time unit conversion factor Returns + ------- + Updates the set of param dictionary values for the new unit system + + """ + + mass_keys = ['GMTINY', 'MIN_GMFRAG'] + distance_keys = ['CHK_QMIN', 'CHK_RMIN', 'CHK_RMAX', 'CHK_EJECT'] + time_keys = ['T0', 'TSTOP', 'DT'] + + if MU2KG_old is not None: + for k in mass_keys: + if k in self.param: + print(f"param['{k}']: {self.param[k]}") + self.param[k] *= MU2KG_old / self.param['MU2KG'] + + if DU2M_old is not None: + for k in distance_keys: + if k in self.param: + self.param[k] *= DU2M_old / self.param['DU2M'] + + CHK_QMIN_RANGE = self.param.pop('CHK_QMIN_RANGE', None) + if CHK_QMIN_RANGE is not None: + CHK_QMIN_RANGE = CHK_QMIN_RANGE.split(" ") + for i, v in enumerate(CHK_QMIN_RANGE): + CHK_QMIN_RANGE[i] = float(CHK_QMIN_RANGE[i]) * self.param['DU2M'] / DU2M_old + self.param['CHK_QMIN_RANGE'] = f"{CHK_QMIN_RANGE[0]} {CHK_QMIN_RANGE[1]}" + + if TU2S_old is not None: + for k in time_keys: + if k in self.param: + self.param[k] *= TU2S_old / self.param['TU2S'] + + return + + def set_distance_range(self, + rmin: float | None = None, + rmax: float | None = None, + qmin_coord: Literal["HELIO","BARY"] | None = None, + verbose: bool | None = None, + **kwargs: Any): + """ + Sets the minimum and maximum distances of the simulation. + + Parameters ---------- - Sets the values of MU2KG, DU2M, and TU2S in the param dictionary to the appropriate units. Also computes the gravitational constant, GU + rmin : float + Minimum distance of the simulation (CHK_QMIN, CHK_RMIN, CHK_QMIN_RANGE[0]) + rmax : float + Maximum distance of the simulation (CHK_RMAX, CHK_QMIN_RANGE[1]) + qmin_coord : str, {"HELIO", "BARY"} + coordinate frame to use for CHK_QMIN + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + range_dict : dict + A dictionary containing the requested parameters. + """ + if rmax is None and rmin is None and qmin_coord is None: + return {} - if MU2KG is not None: - self.param['MU2KG'] = MU2KG - self.MU_name = None + update_list = [] + CHK_QMIN_RANGE = self.param.pop('CHK_QMIN_RANGE', None) + if CHK_QMIN_RANGE is None: + CHK_QMIN_RANGE = [-1, -1] else: - if MU.upper() == "MSUN": - self.param['MU2KG'] = constants.MSun - self.MU_name = "MSun" - elif MU.upper() == "MEARTH": - self.param['MU2KG'] = constants.MEarth - self.MU_name = "MEarth" - elif MU.upper() == "KG": - self.param['MU2KG'] = 1.0 - self.MU_name = "kg" - elif MU.upper() == "G": - self.param['MU2KG'] = 1000.0 - self.MU_name = "g" + CHK_QMIN_RANGE = CHK_QMIN_RANGE.split(" ") + if rmin is not None: + self.param['CHK_QMIN'] = rmin + self.param['CHK_RMIN'] = rmin + CHK_QMIN_RANGE[0] = rmin + update_list.append("rmin") + if rmax is not None: + self.param['CHK_RMAX'] = rmax + self.param['CHK_EJECT'] = rmax + CHK_QMIN_RANGE[1] = rmax + update_list.append("rmax") + if qmin_coord is not None: + valid_qmin_coord = ["HELIO","BARY"] + if qmin_coord.upper() not in valid_qmin_coord: + warnings.warn(f"qmin_coord = {qmin_coord} is not a valid option. Must be one of",','.join(valid_qmin_coord)) + self.param['CHK_QMIN_COORD'] = valid_qmin_coord[0] else: - print(f"{MU} not a recognized unit system. Using MSun as a default.") - self.param['MU2KG'] = constants.MSun - self.MU_name = "MSun" + self.param['CHK_QMIN_COORD'] = qmin_coord.upper() + update_list.append("qmin_coord") - if DU2M is not None: - self.param['DU2M'] = DU2M - self.DU_name = None + self.param['CHK_QMIN_RANGE'] = f"{CHK_QMIN_RANGE[0]} {CHK_QMIN_RANGE[1]}" + + range_dict = self.get_distance_range(update_list, verbose=verbose) + + return range_dict + + def get_distance_range(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs: Any): + """ + + Returns a subset of the parameter dictionary containing the current values of the distance range parameters. + If the verbose option is set in the Simulation object, then it will also print the values. + + Parameters + ---------- + arg_list: str | List[str], optional + A single string or list of strings containing the names of the features to extract. Default is all of: + ["rmin", "rmax"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + range_dict : dict + A dictionary containing the requested parameters. + + """ + + valid_var = {"rmin": "CHK_RMIN", + "rmax": "CHK_RMAX", + "qmin_coord": "CHK_QMIN_COORD", + "qmin": "CHK_QMIN", + "qminR": "CHK_QMIN_RANGE" + } + + units = {"rmin": self.DU_name, + "rmax": self.DU_name, + "qmin": self.DU_name, + "qminR": self.DU_name, + } + + if type(arg_list) is str: + arg_list = [arg_list] + if arg_list is not None: + if "rmin" in arg_list: + arg_list.append("qmin") + if "rmax" in arg_list or "rmin" in arg_list: + arg_list.append("qminR") + + valid_arg, range_dict = self._get_valid_arg_list(arg_list, valid_var) + + if verbose is None: + verbose = self.verbose + + if verbose: + if "rmin" in valid_arg: + key = valid_var["rmin"] + print(f"{'rmin':<{self._getter_column_width}} {range_dict[key]} {units['rmin']}") + if "rmax" in valid_arg: + key = valid_var["rmax"] + print(f"{'rmax':<{self._getter_column_width}} {range_dict[key]} {units['rmax']}") + if "qmin_coord" in valid_arg: + key = valid_var["qmin_coord"] + print(f"{'qmin_coord':<{self._getter_column_width}} {range_dict[key]}") + + return range_dict + + def add_solar_system_body(self, + name: str | List[str], + ephemeris_id: int | List[int] | None = None, + date: str | None = None, + source: str = "HORIZONS"): + """ + Adds a solar system body to an existing simulation Dataset from the JPL Horizons ephemeris service. + + The following are name/ephemeris_id pairs that are currently known to Swiftest, and therefore have + physical properties that can be used to make massive bodies. + + Sun : 0 + Mercury : 1 + Venus : 2 + Earth : 3 + Mars : 4 + Jupiter : 5 + Saturn : 6 + Uranus : 7 + Neptune : 8 + Pluto : 9 + + Parameters + ---------- + name : str | List[str] + Add solar system body by name. + Bodies not on this list will be added as test particles, but additional properties can be added later if + desired. + ephemeris_id : int | List[int], optional but must be the same length as `name` if passed. + Use id if the body you wish to add is recognized by Swiftest. In that case, the id is passed to the + ephemeris service and the name is used. The body specified by `id` supercedes that given by `name`. + date : str, optional + ISO-formatted date sto use when obtaining the ephemerides in the format YYYY-MM-DD. Defaults to value + set by `set_ephemeris_date`. + source : str, default "Horizons" + The source of the ephemerides. + >*Note.* Currently only the JPL Horizons ephemeris is implemented, so this is ignored. + Returns + ------- + data : Xarray dataset with body or bodies added. + """ + + if type(name) is str: + name = [name] + if ephemeris_id is not None: + if type(ephemeris_id) is int: + ephemeris_id = [ephemeris_id] + if len(ephemeris_id) != len(name): + warnings.warn(f"The length of ephemeris_id ({len(ephemeris_id)}) does not match the length of name ({len(name)})") + return None else: - if DU.upper() == "AU": - self.param['DU2M'] = constants.AU2M - self.DU_name = "AU" - elif DU.upper() == "REARTH": - self.param['DU2M'] = constants.REarth - self.DU_name = "REarth" - elif DU.upper() == "M": - self.param['DU2M'] = 1.0 - self.DU_name = "m" - elif DU.upper() == "CM": - self.param['DU2M'] = 100.0 - self.DU_name = "cm" - else: - print(f"{DU} not a recognized unit system. Using AU as a default.") - self.param['DU2M'] = constants.AU2M - self.DU_name = "AU" + ephemeris_id = [None] * len(name) - if TU2S is not None: - self.param['TU2S'] = TU2S - self.TU_name = None + if self.ephemeris_date is None: + self.set_ephemeris_date() + + if date is None: + date = self.ephemeris_date + try: + datetime.datetime.fromisoformat(date) + except: + warnings.warn(f"{date} is not a valid date format. Must be 'YYYY-MM-DD'. Setting to {self.ephemeris_date}") + date = self.ephemeris_date + + if source.upper() != "HORIZONS": + warnings.warn("Currently only the JPL Horizons ephemeris service is supported") + + body_list = [] + for i,n in enumerate(name): + body_list.append(init_cond.solar_system_horizons(n, self.param, date, idval=ephemeris_id[i])) + + #Convert the list receieved from the solar_system_horizons output and turn it into arguments to vec2xr + name,v1,v2,v3,v4,v5,v6,ephemeris_id,GMpl,Rpl,rhill,Ip1,Ip2,Ip3,rotx,roty,rotz,J2,J4 = tuple(np.squeeze(np.hsplit(np.array(body_list),19))) + + v1 = v1.astype(np.float64) + v2 = v2.astype(np.float64) + v3 = v3.astype(np.float64) + v4 = v4.astype(np.float64) + v5 = v5.astype(np.float64) + v6 = v6.astype(np.float64) + ephemeris_id = ephemeris_id.astype(int) + GMpl = GMpl.astype(np.float64) + Rpl = Rpl.astype(np.float64) + rhill = rhill.astype(np.float64) + Ip1 = Ip1.astype(np.float64) + Ip2 = Ip2.astype(np.float64) + Ip3 = Ip3.astype(np.float64) + rotx = rotx.astype(np.float64) + roty = roty.astype(np.float64) + rotz = rotz.astype(np.float64) + J2 = J2.astype(np.float64) + J4 = J4.astype(np.float64) + + if all(np.isnan(GMpl)): + GMpl = None + if all(np.isnan(Rpl)): + Rpl = None + if all(np.isnan(rhill)): + rhill = None + if all(np.isnan(Ip1)): + Ip1 = None + if all(np.isnan(Ip2)): + Ip2 = None + if all(np.isnan(Ip3)): + Ip3 = None + if all(np.isnan(rotx)): + rotx = None + if all(np.isnan(roty)): + roty = None + if all(np.isnan(rotz)): + rotz = None + if all(np.isnan(J2)): + J2 = None + if all(np.isnan(J4)): + J4 = None + + t = self.param['TSTART'] + + dsnew = init_cond.vec2xr(self.param,name,v1,v2,v3,v4,v5,v6,ephemeris_id, + GMpl=GMpl, Rpl=Rpl, rhill=rhill, + Ip1=Ip1, Ip2=Ip2, Ip3=Ip3, + rotx=rotx, roty=roty, rotz=rotz, + J2=J2, J4=J4, t=t) + + dsnew = self._combine_and_fix_dsnew(dsnew) + self.save() + + return dsnew + + + def set_ephemeris_date(self, + ephemeris_date: str | None = None, + verbose: bool | None = None, + **kwargs: Any): + """ + + Parameters + ---------- + ephemeris_date : str, optional + Date to use when obtaining the ephemerides. + Valid options are "today", "MBCL", or date in the format YYYY-MM-DD. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + set_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + Sets the `ephemeris_date` instance variable. + + """ + + if ephemeris_date is None: + return + + # The default value is Prof. Minton's Brimley/Cocoon line crossing date (aka MBCL) + minton_bday = datetime.date.fromisoformat('1976-08-05') + brimley_cocoon_line = datetime.timedelta(days=18530) + minton_bcl = (minton_bday + brimley_cocoon_line).isoformat() + + if ephemeris_date is None or ephemeris_date.upper() == "MBCL": + ephemeris_date = minton_bcl + elif ephemeris_date.upper() == "TODAY": + ephemeris_date = datetime.date.today().isoformat() else: - if TU.upper() == "YR": - self.param['TU2S'] = constants.YR2S - self.TU_name = "y" - elif TU.upper() == "DAY" or TU.upper() == "D" or TU.upper() == "JD": - self.param['TU2S'] = constants.JD2S - self.TU_name = "Day" - elif TU.upper() == "S": - self.param['TU2S'] = 1.0 - self.TU_name = "s" - else: - print(f"{TU} not a recognized unit system. Using YR as a default.") - self.param['TU2S'] = constants.YR2S - self.TU_name = "y" - - self.VU_name = f"{self.DU_name}/{self.TU_name}" - self.GC = constants.GC * self.param['TU2S']**2 * self.param['MU2KG'] / self.param['DU2M']**3 - - if self.verbose: - if self.MU_name is None or self.DU_name is None or self.TU_name is None: - print(f"Custom units set.") - print(f"MU2KG: {self.param['MU2KG']}") - print(f"DU2M : {self.param['DU2M']}") - print(f"TU2S : {self.param['TU2S']}") - else: - print(f"Units set to {self.MU_name}-{self.DU_name}-{self.TU_name}") + try: + datetime.datetime.fromisoformat(ephemeris_date) + except: + valid_date_args = ['"MBCL"', '"TODAY"', '"YYYY-MM-DD"'] + warnings.warn(f"{ephemeris_date} is not a valid format. Valid options include:", ', '.join(valid_date_args)) + warnings.warn("Using MBCL for date.") + ephemeris_date = minton_bcl - return - - def add(self, plname, date=date.today().isoformat(), idval=None): + self.ephemeris_date = ephemeris_date + + ephemeris_date = self.get_ephemeris_date(verbose=verbose) + + return ephemeris_date + + def get_ephemeris_date(self, arg_list: str | List[str] | None = None, verbose: bool | None = None, **kwargs: Any): """ - Adds a solar system body to an existing simulation DataSet. - + + Prints the current value of the ephemeris date + Parameters ---------- - plname : string - Name of planet to add (e.g. "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" - date : string - Date to use when obtaining the ephemerides in the format YYYY-MM-DD. Defaults to "today" + arg_list: str | List[str], optional + A single string or list of strings containing the names of the features to extract. Default is all of: + ["integrator"] + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + Returns ------- - self.ds : xarray dataset + ephemeris_date: str + The ISO-formatted date string for the ephemeris computation + """ - #self.ds = init_cond.solar_system_horizons(plname, idval, self.param, date, self.ds) - self.addp(*init_cond.solar_system_horizons(plname, idval, self.param, date, self.ds)) - return - - - def addp(self, idvals, namevals, v1, v2, v3, v4, v5, v6, GMpl=None, Rpl=None, rhill=None, Ip1=None, Ip2=None, Ip3=None, rotx=None, roty=None, rotz=None, J2=None,J4=None,t=None): + + try: + self.ephemeris_date + except: + warnings.warn(f"ephemeris_date is not set") + return + + valid_arg = {"ephemeris_date": self.ephemeris_date} + + ephemeris_date = self._get_instance_var(arg_list, valid_arg,verbose, **kwargs) + + return ephemeris_date + + def _get_instance_var(self, arg_list: str | List[str], valid_arg: Dict, verbose: bool | None = None, **kwargs: Any): + """ + + Prints the current value of an instance variable. + + Parameters + ---------- + arg_list: str | List[str] + A single string or list of strings containing the names of the the instance variable to get. + valid_arg: dict + A dictionary where the key is the parameter argument and the value is the equivalent instance variable value. + verbose: bool, optional + If passed, it will override the Simulation object's verbose flag + **kwargs + A dictionary of additional keyword argument. This allows this method to be called by the more general + get_parameter method, which takes all possible Simulation parameters as arguments, so these are ignored. + + Returns + ------- + Tuple of instance variable values given by the arg_list + + """ + + arg_vals = [] + if verbose is None: + verbose = self.verbose + if verbose: + if arg_list is None: + arg_list = list(valid_arg.keys()) + for arg in arg_list: + if arg in valid_arg: + print(f"{arg:<{self._getter_column_width}} {valid_arg[arg]}") + arg_vals.append(valid_arg[arg]) + + return tuple(arg_vals) + + def add_body(self, + name: str | List[str] | npt.NDArray[np.str_], + v1: float | List[float] | npt.NDArray[np.float_], + v2: float | List[float] | npt.NDArray[np.float_], + v3: float | List[float] | npt.NDArray[np.float_], + v4: float | List[float] | npt.NDArray[np.float_], + v5: float | List[float] | npt.NDArray[np.float_], + v6: float | List[float] | npt.NDArray[np.float_], + idvals: int | list[int] | npt.NDArray[np.int_] | None=None, + GMpl: float | List[float] | npt.NDArray[np.float_] | None=None, + Rpl: float | List[float] | npt.NDArray[np.float_] | None=None, + rhill: float | List[float] | npt.NDArray[np.float_] | None=None, + Ip1: float | List[float] | npt.NDArray[np.float_] | None=None, + Ip2: float | List[float] | npt.NDArray[np.float_] | None=None, + Ip3: float | List[float] | npt.NDArray[np.float_] | None=None, + rotx: float | List[float] | npt.NDArray[np.float_] | None=None, + roty: float | List[float] | npt.NDArray[np.float_] | None=None, + rotz: float | List[float] | npt.NDArray[np.float_] | None=None, + J2: float | List[float] | npt.NDArray[np.float_] | None=None, + J4: float | List[float] | npt.NDArray[np.float_] | None=None): """ Adds a body (test particle or massive body) to the internal DataSet given a set up 6 vectors (orbital elements - or cartesian state vectors, depending on the value of self.param). Input all angles in degress - - Parameters - ---------- - idvals : integer - Array of body index values. - v1 : float - xh for param['IN_FORM'] == "XV"; a for param['IN_FORM'] == "EL" - v2 : float - yh for param['IN_FORM'] == "XV"; e for param['IN_FORM'] == "EL" - v3 : float - zh for param['IN_FORM'] == "XV"; inc for param['IN_FORM'] == "EL" - v4 : float - vhxh for param['IN_FORM'] == "XV"; capom for param['IN_FORM'] == "EL" - v5 : float - vhyh for param['IN_FORM'] == "XV"; omega for param['IN_FORM'] == "EL" - v6 : float - vhzh for param['IN_FORM'] == "XV"; capm for param['IN_FORM'] == "EL" - Gmass : float - Optional: Array of G*mass values if these are massive bodies - radius : float - Optional: Array radius values if these are massive bodies - rhill : float - Optional: Array rhill values if these are massive bodies - Ip1,y,z : float - Optional: Principal axes moments of inertia - rotx,y,z: float - Optional: Rotation rate vector components - t : float - Optional: Time at start of simulation + or cartesian state vectors, depending on the value of self.param). Input all angles in degress. + + This method will update self.data with the new body or bodies added to the existing Dataset. + + Parameters + ---------- + name : str or array-like of str + Name or names of + v1 : float or array-like of float + xhx for param['IN_FORM'] == "XV"; a for param['IN_FORM'] == "EL" + v2 : float or array-like of float + xhy for param['IN_FORM'] == "XV"; e for param['IN_FORM'] == "EL" + v3 : float or array-like of float + xhz for param['IN_FORM'] == "XV"; inc for param['IN_FORM'] == "EL" + v4 : float or array-like of float + vhx for param['IN_FORM'] == "XV"; capom for param['IN_FORM'] == "EL" + v5 : float or array-like of float + vhy for param['IN_FORM'] == "XV"; omega for param['IN_FORM'] == "EL" + v6 : float or array-like of float + vhz for param['IN_FORM'] == "XV"; capm for param['IN_FORM'] == "EL" + idvals : int or array-like of int, optional + Unique id values. If not passed, this will be computed based on the pre-existing Dataset ids. + Gmass : float or array-like of float, optional + G*mass values if these are massive bodies + radius : float or array-like of float, optional + Radius values if these are massive bodies + rhill : float, optional + Hill's radius values if these are massive bodies + Ip1,y,z : float, optional + Principal axes moments of inertia these are massive bodies with rotation enabled + rotx,y,z: float, optional + Rotation rate vector components if these are massive bodies with rotation enabled + Returns ------- - self.ds : xarray dataset + data : Xarray Dataset + Dasaset containing the body or bodies that were added + """ - if t is None: - t = self.param['T0'] - dsnew = init_cond.vec2xr(self.param, idvals, namevals, v1, v2, v3, v4, v5, v6, GMpl, Rpl, rhill, Ip1, Ip2, Ip3, rotx, roty, rotz, J2, J4, t) - if dsnew is not None: - self.ds = xr.combine_by_coords([self.ds, dsnew]) - self.ds['ntp'] = self.ds['id'].where(np.isnan(self.ds['Gmass'])).count(dim="id") - self.ds['npl'] = self.ds['id'].where(np.invert(np.isnan(self.ds['Gmass']))).count(dim="id") - 1 + #convert all inputs to numpy arrays + def input_to_array(val,t,n=None): + if t == "f": + t = np.float64 + elif t == "i": + t = np.int64 + elif t == "s": + t = np.str + if val is None: + return None + elif np.isscalar(val): + val = np.array([val],dtype=t) + elif type(val) is list: + val = np.array(val,dtype=t) + + if n is None: + return val, len(val) + else: + if n != len(val): + raise ValueError(f"Error! Mismatched array lengths in add_body. Got {len(val)} when expecting {n}") + return val + + + name,nbodies = input_to_array(name,"s") + v1 = input_to_array(v1,"f",nbodies) + v2 = input_to_array(v2,"f",nbodies) + v3 = input_to_array(v3,"f",nbodies) + v4 = input_to_array(v4,"f",nbodies) + v5 = input_to_array(v5,"f",nbodies) + v6 = input_to_array(v6,"f",nbodies) + idvals = input_to_array(idvals,"i",nbodies) + GMpl = input_to_array(GMpl,"f",nbodies) + rhill = input_to_array(rhill,"f",nbodies) + Rpl = input_to_array(Rpl,"f",nbodies) + Ip1 = input_to_array(Ip1,"f",nbodies) + Ip2 = input_to_array(Ip2,"f",nbodies) + Ip3 = input_to_array(Ip3,"f",nbodies) + rotx = input_to_array(rotx,"f",nbodies) + roty = input_to_array(roty,"f",nbodies) + rotz = input_to_array(rotz,"f",nbodies) + J2 = input_to_array(J2,"f",nbodies) + J4 = input_to_array(J4,"f",nbodies) + + if len(self.data) == 0: + maxid = -1 + else: + maxid = self.data.id.max().values[()] + + if idvals is None: + idvals = np.arange(start=maxid+1,stop=maxid+1+nbodies,dtype=int) + + if len(self.data) > 0: + dup_id = np.in1d(idvals, self.data.id) + if any(dup_id): + raise ValueError(f"Duplicate ids detected: ", *idvals[dup_id]) + + t = self.param['TSTART'] + + dsnew = init_cond.vec2xr(self.param, name, v1, v2, v3, v4, v5, v6, idvals, + GMpl=GMpl, Rpl=Rpl, rhill=rhill, + Ip1=Ip1, Ip2=Ip2, Ip3=Ip3, + rotx=rotx, roty=roty, rotz=rotz, + J2=J2, J4=J4,t=t) + + dsnew = self._combine_and_fix_dsnew(dsnew) + self.save() + + return dsnew + + def _combine_and_fix_dsnew(self,dsnew): + """ + Combines the new Dataset with the old one. Also computes the values of ntp and npl and sets the proper types. + Parameters + ---------- + dsnew : xarray Dataset + Dataset with new bodies + + Returns + ------- + dsnew : xarray Dataset + Updated Dataset with ntp, npl values and types fixed. + + """ + + self.data = xr.combine_by_coords([self.data, dsnew]) + + def get_nvals(ds): + if "Gmass" in ds: + ds['ntp'] = ds['id'].where(np.isnan(ds['Gmass'])).count(dim="id") + ds['npl'] = ds['id'].where(~(np.isnan(ds['Gmass']))).count(dim="id") - 1 + else: + ds['ntp'] = ds['id'].count(dim="id") + ds['npl'] = xr.full_like(ds['ntp'],0) + return ds + + dsnew = get_nvals(dsnew) + self.data = get_nvals(self.data) + + if self.param['OUT_TYPE'] == "NETCDF_DOUBLE": + dsnew = io.fix_types(dsnew, ftype=np.float64) + self.data = io.fix_types(self.data, ftype=np.float64) + elif self.param['OUT_TYPE'] == "NETCDF_FLOAT": + dsnew = io.fix_types(dsnew, ftype=np.float32) + self.data = io.fix_types(self.data, ftype=np.float32) + + return dsnew - return - - def read_param(self, param_file, codename="Swiftest", verbose=True): """ - Reads in a param.in file and determines whether it is a Swift/Swifter/Swiftest parameter file. + Reads in an input parameter file and stores the values in the param dictionary. Parameters ---------- - param_file : string - File name of the input parameter file - codename : string - Type of parameter file, either "Swift", "Swifter", or "Swiftest" + param_file : string + File name of the input parameter file + codename : string + Type of parameter file, either "Swift", "Swifter", or "Swiftest" Returns ------- - self.ds : xarray dataset + """ if codename == "Swiftest": - self.param = io.read_swiftest_param(param_file, self.param, verbose=verbose) + param_old = self.param.copy() + self.param = io.read_swiftest_param(param_file, param_old, verbose=verbose) self.codename = "Swiftest" elif codename == "Swifter": self.param = io.read_swifter_param(param_file, verbose=verbose) @@ -298,43 +2365,61 @@ def read_param(self, param_file, codename="Swiftest", verbose=True): self.param = io.read_swift_param(param_file, verbose=verbose) self.codename = "Swift" else: - print(f'{codename} is not a recognized code name. Valid options are "Swiftest", "Swifter", or "Swift".') + warnings.warn(f'{codename} is not a recognized code name. Valid options are "Swiftest", "Swifter", or "Swift".') self.codename = "Unknown" return - - - def write_param(self, param_file, param=None): + + def write_param(self, + codename: Literal["Swiftest", "Swifter", "Swift"] | None = None, + param_file: str | os.PathLike | None = None, + param: Dict | None = None, + **kwargs: Any): """ Writes to a param.in file and determines whether the output format needs to be converted between Swift/Swifter/Swiftest. Parameters ---------- - param_file : string - File name of the input parameter file + codename : {"Swiftest", "Swifter", "Swift"}, optional + Alternative name of the n-body code that the parameter file will be formatted for. Defaults to current instance + variable codename + param_file : str or path-like, optional + Alternative file name of the input parameter file. Defaults to current instance variable self.param_file + param: Dict, optional + An alternative parameter dictionary to write out. Defaults to the current instance variable self.param + **kwargs + A dictionary of additional keyword argument. These are ignored. + Returns ------- - self.ds : xarray dataset + None """ + + if codename is None: + codename = self.codename + if param_file is None: + param_file = self.param_file if param is None: param = self.param + print(f"Writing parameter inputs to file {param_file}") + param['! VERSION'] = f"{codename} input file" + # Check to see if the parameter type matches the output type. If not, we need to convert - codename = param['! VERSION'].split()[0] if codename == "Swifter" or codename == "Swiftest": if param['IN_TYPE'] == "ASCII": param.pop("NC_IN", None) else: - param.pop("CB_IN",None) - param.pop("PL_IN",None) - param.pop("TP_IN",None) + param.pop("CB_IN", None) + param.pop("PL_IN", None) + param.pop("TP_IN", None) io.write_labeled_param(param, param_file) elif codename == "Swift": io.write_swift_param(param, param_file) else: - print('Cannot process unknown code type. Call the read_param method with a valid code name. Valid options are "Swiftest", "Swifter", or "Swift".') + warnings.warn('Cannot process unknown code type. Call the read_param method with a valid code name. Valid options are "Swiftest", "Swifter", or "Swift".') return - - - def convert(self, param_file, newcodename="Swiftest", plname="pl.swiftest.in", tpname="tp.swiftest.in", cbname="cb.swiftest.in", conversion_questions={}): + + def convert(self, param_file, newcodename="Swiftest", plname="pl.swiftest.in", tpname="tp.swiftest.in", + cbname="cb.swiftest.in", conversion_questions={}): """ Converts simulation input files from one format to another (Swift, Swifter, or Swiftest). @@ -360,10 +2445,10 @@ def convert(self, param_file, newcodename="Swiftest", plname="pl.swiftest.in", t """ oldparam = self.param if self.codename == newcodename: - print(f"This parameter configuration is already in {newcodename} format") + warnings.warn(f"This parameter configuration is already in {newcodename} format") return oldparam if newcodename != "Swift" and newcodename != "Swifter" and newcodename != "Swiftest": - print(f'{newcodename} is an invalid code type. Valid options are "Swiftest", "Swifter", or "Swift".') + warnings.warn(f'{newcodename} is an invalid code type. Valid options are "Swiftest", "Swifter", or "Swift".') return oldparam goodconversion = True if self.codename == "Swifter": @@ -380,14 +2465,13 @@ def convert(self, param_file, newcodename="Swiftest", plname="pl.swiftest.in", t goodconversion = False else: goodconversion = False - + if goodconversion: self.write_param(param_file) else: - print(f"Conversion from {self.codename} to {newcodename} is not supported.") + warnings.warn(f"Conversion from {self.codename} to {newcodename} is not supported.") return oldparam - - + def bin2xr(self): """ Converts simulation output files from a flat binary file to a xarray dataset. @@ -397,27 +2481,26 @@ def bin2xr(self): Returns ------- - self.ds : xarray dataset + self.data : xarray dataset """ # Make a temporary copy of the parameter dictionary so we can supply the absolute path of the binary file # This is done to handle cases where the method is called from a different working directory than the simulation # results param_tmp = self.param.copy() - param_tmp['BIN_OUT'] = os.path.join(self.dir_path,self.param['BIN_OUT']) + param_tmp['BIN_OUT'] = os.path.join(self.sim_dir, self.param['BIN_OUT']) if self.codename == "Swiftest": - self.ds = io.swiftest2xr(param_tmp, verbose=self.verbose) - if self.verbose: print('Swiftest simulation data stored as xarray DataSet .ds') + self.data = io.swiftest2xr(param_tmp, verbose=self.verbose) + if self.verbose: print('Swiftest simulation data stored as xarray DataSet .data') elif self.codename == "Swifter": - self.ds = io.swifter2xr(param_tmp, verbose=self.verbose) - if self.verbose: print('Swifter simulation data stored as xarray DataSet .ds') + self.data = io.swifter2xr(param_tmp, verbose=self.verbose) + if self.verbose: print('Swifter simulation data stored as xarray DataSet .data') elif self.codename == "Swift": - print("Reading Swift simulation data is not implemented yet") + warnings.warn("Reading Swift simulation data is not implemented yet") else: - print('Cannot process unknown code type. Call the read_param method with a valid code name. Valid options are "Swiftest", "Swifter", or "Swift".') + warnings.warn('Cannot process unknown code type. Call the read_param method with a valid code name. Valid options are "Swiftest", "Swifter", or "Swift".') return - - + def follow(self, codestyle="Swifter"): """ An implementation of the Swift tool_follow algorithm. Under development. Currently only for Swift simulations. @@ -431,66 +2514,82 @@ def follow(self, codestyle="Swifter"): ------- fol : xarray dataset """ - if self.ds is None: + if self.data is None: self.bin2xr() if codestyle == "Swift": try: with open('follow.in', 'r') as f: - line = f.readline() # Parameter file (ignored because bin2xr already takes care of it - line = f.readline() # PL file (ignored) - line = f.readline() # TP file (ignored) - line = f.readline() # ifol + line = f.readline() # Parameter file (ignored because bin2xr already takes care of it + line = f.readline() # PL file (ignored) + line = f.readline() # TP file (ignored) + line = f.readline() # ifol i_list = [i for i in line.split(" ") if i.strip()] ifol = int(i_list[0]) line = f.readline() # nskp i_list = [i for i in line.split(" ") if i.strip()] nskp = int(i_list[0]) except IOError: - print('No follow.in file found') + warnings.warn('No follow.in file found') ifol = None nskp = None - fol = tool.follow_swift(self.ds, ifol=ifol, nskp=nskp) + fol = tool.follow_swift(self.data, ifol=ifol, nskp=nskp) else: fol = None - + if self.verbose: print('follow.out written') return fol - - - def save(self, param_file, framenum=-1, codename="Swiftest"): + + def save(self, + codename: Literal["Swiftest", "Swifter", "Swift"] | None = None, + param_file: str | os.PathLike | None = None, + param: Dict | None = None, + framenum: int = -1, + **kwargs: Any): """ Saves an xarray dataset to a set of input files. Parameters ---------- - param_file : string - Name of the parameter input file - framenum : integer (default=-1) - Time frame to use to generate the initial conditions. If this argument is not passed, the default is to use the last frame in the dataset. - codename : string - Name of the desired format (Swift/Swifter/Swiftest) + codename : {"Swiftest", "Swifter", "Swift"}, optional + Alternative name of the n-body code that the parameter file will be formatted for. Defaults to current instance + variable self.codename + param_file : str or path-like, optional + Alternative file name of the input parameter file. Defaults to current instance variable self.param_file + param: Dict, optional + An alternative parameter dictionary to write out. Defaults to the current instance variable self.param + framenum : int Default=-1 + Time frame to use to generate the initial conditions. If this argument is not passed, the default is to use the last frame in the dataset. + **kwargs + A dictionary of additional keyword argument. These are ignored. Returns ------- - self.ds : xarray dataset + None """ + if codename is None: + codename = self.codename + if param_file is None: + param_file = self.param_file + if param is None: + param = self.param if codename == "Swiftest": - io.swiftest_xr2infile(ds=self.ds, param=self.param, in_type=self.param['IN_TYPE'], framenum=framenum,infile_name=self.param['NC_IN']) - self.write_param(param_file) + io.swiftest_xr2infile(ds=self.data, param=param, in_type=self.param['IN_TYPE'], framenum=framenum) + self.write_param(param_file=param_file) elif codename == "Swifter": - if self.codename == "Swiftest": - swifter_param = io.swiftest2swifter_param(self.param) + if codename == "Swiftest": + swifter_param = io.swiftest2swifter_param(param) else: - swifter_param = self.param - io.swifter_xr2infile(self.ds, swifter_param, framenum) + swifter_param = param + io.swifter_xr2infile(self.data, swifter_param, framenum) self.write_param(param_file, param=swifter_param) else: - print(f'Saving to {codename} not supported') + warnings.warn(f'Saving to {codename} not supported') return - def initial_conditions_from_bin(self, framenum=-1, new_param=None, new_param_file="param.new.in", new_initial_conditions_file="bin_in.nc", restart=False, codename="Swiftest"): + def initial_conditions_from_bin(self, framenum=-1, new_param=None, new_param_file="param.new.in", + new_initial_conditions_file="bin_in.nc", restart=False, codename="Swiftest"): """ Generates a set of input files from a old output file. @@ -511,39 +2610,43 @@ def initial_conditions_from_bin(self, framenum=-1, new_param=None, new_param_fil Returns ------- - frame : NetCDF dataset + frame : NetCDF dataset + A dataset containing the extracted initial condition data. """ - if codename != "Swiftest": self.save(new_param_file, framenum, codename) return + if new_param is None: new_param = self.param.copy() if codename == "Swiftest": if restart: - new_param['T0'] = self.ds.time.values[framenum] - if self.param['OUT_TYPE'] == 'NETCDF_DOUBLE' or self.param['OUT_TYPE'] == 'REAL8': + new_param['T0'] = self.data.time.values[framenum] + if self.param['OUT_TYPE'] == 'NETCDF_DOUBLE': new_param['IN_TYPE'] = 'NETCDF_DOUBLE' - elif self.param['OUT_TYPE'] == 'NETCDF_FLOAT' or self.param['OUT_TYPE'] == 'REAL4': + elif self.param['OUT_TYPE'] == 'NETCDF_FLOAT': new_param['IN_TYPE'] = 'NETCDF_FLOAT' else: - print(f"{self.param['OUT_TYPE']} is an invalid OUT_TYPE file") + warnings.warn(f"{self.param['OUT_TYPE']} is an invalid OUT_TYPE file") return + if self.param['BIN_OUT'] != new_param['BIN_OUT'] and restart: - print(f"Restart run with new output file. Copying {self.param['BIN_OUT']} to {new_param['BIN_OUT']}") - shutil.copy2(self.param['BIN_OUT'],new_param['BIN_OUT']) + print(f"Restart run with new output file. Copying {self.param['BIN_OUT']} to {new_param['BIN_OUT']}") + shutil.copy2(self.param['BIN_OUT'], new_param['BIN_OUT']) + new_param['IN_FORM'] = 'XV' if restart: new_param['OUT_STAT'] = 'APPEND' + new_param['FIRSTKICK'] = 'T' new_param['NC_IN'] = new_initial_conditions_file new_param.pop('PL_IN', None) new_param.pop('TP_IN', None) new_param.pop('CB_IN', None) print(f"Extracting data from dataset at time frame number {framenum} and saving it to {new_param['NC_IN']}") - frame = io.swiftest_xr2infile(self.ds, self.param, infile_name=new_param['NC_IN'],framenum=framenum) + frame = io.swiftest_xr2infile(self.data, self.param, infile_name=new_param['NC_IN'], framenum=framenum) print(f"Saving parameter configuration file to {new_param_file}") self.write_param(new_param_file, param=new_param) diff --git a/src/io/io.f90 b/src/io/io.f90 index aa797ddbe..86cf55728 100644 --- a/src/io/io.f90 +++ b/src/io/io.f90 @@ -566,6 +566,7 @@ module subroutine io_param_reader(self, unit, iotype, v_list, iostat, iomsg) character(len=*), intent(inout) :: iomsg !! Message to pass if iostat /= 0 ! Internals logical :: t0_set = .false. !! Is the initial time set in the input file? + logical :: tstart_set = .false. !! Is the final time set in the input file? logical :: tstop_set = .false. !! Is the final time set in the input file? logical :: dt_set = .false. !! Is the step size set in the input file? integer(I4B) :: ilength, ifirst, ilast, i !! Variables used to parse input file @@ -593,6 +594,9 @@ module subroutine io_param_reader(self, unit, iotype, v_list, iostat, iomsg) case ("T0") read(param_value, *, err = 667, iomsg = iomsg) param%t0 t0_set = .true. + case ("TSTART") + read(param_value, *, err = 667, iomsg = iomsg) param%t0 + tstart_set = .true. case ("TSTOP") read(param_value, *, err = 667, iomsg = iomsg) param%tstop tstop_set = .true. @@ -743,6 +747,12 @@ module subroutine io_param_reader(self, unit, iotype, v_list, iostat, iomsg) read(param_value, *, err = 667, iomsg = iomsg) param%maxid_collision case ("PARTICLE_OUT") param%particle_out = param_value + case ("RESTART") + if (param_value == "NO" .or. param_value == 'F') then + param%lrestart = .false. + else if (param_value == "YES" .or. param_value == 'T') then + param%lrestart = .true. + end if case ("NPLMAX", "NTPMAX", "GMTINY", "MIN_GMFRAG", "FRAGMENTATION", "SEED", "YARKOVSKY", "YORP") ! Ignore SyMBA-specific, not-yet-implemented, or obsolete input parameters case default write(*,*) "Ignoring unknown parameter -> ",param_name @@ -929,7 +939,7 @@ module subroutine io_param_reader(self, unit, iotype, v_list, iostat, iomsg) iostat = 0 ! Print the contents of the parameter file to standard output - call param%writer(unit = OUTPUT_UNIT, iotype = "none", v_list = [0], iostat = iostat, iomsg = iomsg) + ! call param%writer(unit = OUTPUT_UNIT, iotype = "none", v_list = [0], iostat = iostat, iomsg = iomsg) end associate diff --git a/src/modules/swiftest_globals.f90 b/src/modules/swiftest_globals.f90 index 47b852f5a..f6644a302 100644 --- a/src/modules/swiftest_globals.f90 +++ b/src/modules/swiftest_globals.f90 @@ -149,7 +149,7 @@ module swiftest_globals character(*), parameter :: NETCDF_OUTFILE = 'bin.nc' !! Default output file name character(*), parameter :: TIME_DIMNAME = "time" !! NetCDF name of the time dimension character(*), parameter :: ID_DIMNAME = "id" !! NetCDF name of the particle id dimension - character(*), parameter :: STR_DIMNAME = "str" !! NetCDF name of the particle id dimension + character(*), parameter :: STR_DIMNAME = "string32" !! NetCDF name of the character string dimension character(*), parameter :: PTYPE_VARNAME = "particle_type" !! NetCDF name of the particle type variable character(*), parameter :: NAME_VARNAME = "name" !! NetCDF name of the particle name variable character(*), parameter :: NPL_VARNAME = "npl" !! NetCDF name of the number of active massive bodies variable diff --git a/src/netcdf/netcdf.f90 b/src/netcdf/netcdf.f90 index 0cd27f737..4c037a291 100644 --- a/src/netcdf/netcdf.f90 +++ b/src/netcdf/netcdf.f90 @@ -198,8 +198,8 @@ module subroutine netcdf_initialize_output(self, param) ! Define the NetCDF dimensions with particle name as the record dimension call check( nf90_def_dim(self%ncid, ID_DIMNAME, NF90_UNLIMITED, self%id_dimid), "netcdf_initialize_output nf90_def_dim id_dimid" ) ! 'x' dimension - call check( nf90_def_dim(self%ncid, TIME_DIMNAME, NF90_UNLIMITED, self%time_dimid), "netcdf_initialize_output nf90_def_dim time_dimid" ) ! 'y' dimension call check( nf90_def_dim(self%ncid, STR_DIMNAME, NAMELEN, self%str_dimid), "netcdf_initialize_output nf90_def_dim str_dimid" ) ! Dimension for string variables (aka character arrays) + call check( nf90_def_dim(self%ncid, TIME_DIMNAME, NF90_UNLIMITED, self%time_dimid), "netcdf_initialize_output nf90_def_dim time_dimid" ) ! 'y' dimension select case (param%out_type) case(NETCDF_FLOAT_TYPE) @@ -351,27 +351,35 @@ module subroutine netcdf_open(self, param, readonly) ! Internals integer(I4B) :: mode, status character(len=NF90_MAX_NAME) :: str_dim_name + character(len=STRMAX) :: errmsg mode = NF90_WRITE if (present(readonly)) then if (readonly) mode = NF90_NOWRITE end if - call check( nf90_open(param%outfile, mode, self%ncid), "netcdf_open nf90_open" ) + write(errmsg,*) "netcdf_open nf90_open ",trim(adjustl(param%outfile)) + call check( nf90_open(param%outfile, mode, self%ncid), errmsg) call check( nf90_inq_dimid(self%ncid, TIME_DIMNAME, self%time_dimid), "netcdf_open nf90_inq_dimid time_dimid" ) call check( nf90_inq_dimid(self%ncid, ID_DIMNAME, self%id_dimid), "netcdf_open nf90_inq_dimid id_dimid" ) - call check( nf90_inquire_dimension(self%ncid, max(self%time_dimid,self%id_dimid)+1, name=str_dim_name), "netcdf_open nf90_inquire_dimension str_dim_name" ) + if (max(self%time_dimid,self%id_dimid) == 2) then + self%str_dimid = 3 + else if (min(self%time_dimid,self%id_dimid) == 0) then + self%str_dimid = 1 + else + self%str_dimid = 2 + end if + call check( nf90_inquire_dimension(self%ncid, self%str_dimid, name=str_dim_name), "netcdf_open nf90_inquire_dimension str_dim_name" ) call check( nf90_inq_dimid(self%ncid, str_dim_name, self%str_dimid), "netcdf_open nf90_inq_dimid str_dimid" ) + ! Required Variables + call check( nf90_inq_varid(self%ncid, TIME_DIMNAME, self%time_varid), "netcdf_open nf90_inq_varid time_varid" ) call check( nf90_inq_varid(self%ncid, ID_DIMNAME, self%id_varid), "netcdf_open nf90_inq_varid id_varid" ) - call check( nf90_inq_varid(self%ncid, NPL_VARNAME, self%npl_varid), "netcdf_open nf90_inq_varid npl_varid" ) - call check( nf90_inq_varid(self%ncid, NTP_VARNAME, self%ntp_varid), "netcdf_open nf90_inq_varid ntp_varid" ) - if (param%integrator == SYMBA) call check( nf90_inq_varid(self%ncid, NPLM_VARNAME, self%nplm_varid), "netcdf_open nf90_inq_varid nplm_varid" ) call check( nf90_inq_varid(self%ncid, NAME_VARNAME, self%name_varid), "netcdf_open nf90_inq_varid name_varid" ) call check( nf90_inq_varid(self%ncid, PTYPE_VARNAME, self%ptype_varid), "netcdf_open nf90_inq_varid ptype_varid" ) - call check( nf90_inq_varid(self%ncid, STATUS_VARNAME, self%status_varid), "netcdf_open nf90_inq_varid status_varid" ) + call check( nf90_inq_varid(self%ncid, GMASS_VARNAME, self%Gmass_varid), "netcdf_open nf90_inq_varid Gmass_varid" ) if ((param%out_form == XV) .or. (param%out_form == XVEL)) then call check( nf90_inq_varid(self%ncid, XHX_VARNAME, self%xhx_varid), "netcdf_open nf90_inq_varid xhx_varid" ) @@ -409,31 +417,10 @@ module subroutine netcdf_open(self, param, readonly) call check( nf90_inq_varid(self%ncid, OMEGA_VARNAME, self%omega_varid), "netcdf_open nf90_inq_varid omega_varid" ) call check( nf90_inq_varid(self%ncid, CAPM_VARNAME, self%capm_varid), "netcdf_open nf90_inq_varid capm_varid" ) end if - call check( nf90_inq_varid(self%ncid, GMASS_VARNAME, self%Gmass_varid), "netcdf_open nf90_inq_varid Gmass_varid" ) - - if (param%lrhill_present) call check( nf90_inq_varid(self%ncid, RHILL_VARNAME, self%rhill_varid), "netcdf_open nf90_inq_varid rhill_varid" ) if (param%lclose) then call check( nf90_inq_varid(self%ncid, RADIUS_VARNAME, self%radius_varid), "netcdf_open nf90_inq_varid radius_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_TYPE_VARNAME, self%origin_type_varid), "netcdf_open nf90_inq_varid origin_type_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_TIME_VARNAME, self%origin_time_varid), "netcdf_open nf90_inq_varid origin_time_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_XHX_VARNAME, self%origin_xhx_varid), "netcdf_open nf90_inq_varid origin_xhx_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_XHY_VARNAME, self%origin_xhy_varid), "netcdf_open nf90_inq_varid origin_xhy_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_XHZ_VARNAME, self%origin_xhz_varid), "netcdf_open nf90_inq_varid origin_xhz_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_VHX_VARNAME, self%origin_vhx_varid), "netcdf_open nf90_inq_varid origin_vhx_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_VHY_VARNAME, self%origin_vhy_varid), "netcdf_open nf90_inq_varid origin_vhy_varid" ) - call check( nf90_inq_varid(self%ncid, ORIGIN_VHZ_VARNAME, self%origin_vhz_varid), "netcdf_open nf90_inq_varid origin_vhz_varid" ) - - call check( nf90_inq_varid(self%ncid, COLLISION_ID_VARNAME, self%collision_id_varid), "netcdf_open nf90_inq_varid collision_id_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_TIME_VARNAME, self%discard_time_varid), "netcdf_open nf90_inq_varid discard_time_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_XHX_VARNAME, self%discard_xhx_varid), "netcdf_open nf90_inq_varid discard_xhx_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_XHY_VARNAME, self%discard_xhy_varid), "netcdf_open nf90_inq_varid discard_xhy_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_XHZ_VARNAME, self%discard_xhz_varid), "netcdf_open nf90_inq_varid discard_xhz_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_VHX_VARNAME, self%discard_vhx_varid), "netcdf_open nf90_inq_varid discard_vhx_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_VHY_VARNAME, self%discard_vhy_varid), "netcdf_open nf90_inq_varid discard_vhy_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_VHZ_VARNAME, self%discard_vhz_varid), "netcdf_open nf90_inq_varid discard_vhz_varid" ) - call check( nf90_inq_varid(self%ncid, DISCARD_BODY_ID_VARNAME, self%discard_body_id_varid), "netcdf_open nf90_inq_varid discard_body_id_varid" ) - end if + end if if (param%lrotation) then call check( nf90_inq_varid(self%ncid, IP1_VARNAME, self%Ip1_varid), "netcdf_open nf90_inq_varid Ip1_varid" ) @@ -449,27 +436,67 @@ module subroutine netcdf_open(self, param, readonly) ! call check( nf90_inq_varid(self%ncid, Q_VARNAME, self%Q_varid), "netcdf_open nf90_inq_varid Q_varid" ) ! end if - if (param%lenergy) then - call check( nf90_inq_varid(self%ncid, KE_ORB_VARNAME, self%KE_orb_varid), "netcdf_open nf90_inq_varid KE_orb_varid" ) - call check( nf90_inq_varid(self%ncid, KE_SPIN_VARNAME, self%KE_spin_varid), "netcdf_open nf90_inq_varid KE_spin_varid" ) - call check( nf90_inq_varid(self%ncid, PE_VARNAME, self%PE_varid), "netcdf_open nf90_inq_varid PE_varid" ) - call check( nf90_inq_varid(self%ncid, L_ORBX_VARNAME, self%L_orbx_varid), "netcdf_open nf90_inq_varid L_orbx_varid" ) - call check( nf90_inq_varid(self%ncid, L_ORBY_VARNAME, self%L_orby_varid), "netcdf_open nf90_inq_varid L_orby_varid" ) - call check( nf90_inq_varid(self%ncid, L_ORBZ_VARNAME, self%L_orbz_varid), "netcdf_open nf90_inq_varid L_orbz_varid" ) - call check( nf90_inq_varid(self%ncid, L_SPINX_VARNAME, self%L_spinx_varid), "netcdf_open nf90_inq_varid L_spinx_varid" ) - call check( nf90_inq_varid(self%ncid, L_SPINY_VARNAME, self%L_spiny_varid), "netcdf_open nf90_inq_varid L_spiny_varid" ) - call check( nf90_inq_varid(self%ncid, L_SPINZ_VARNAME, self%L_spinz_varid), "netcdf_open nf90_inq_varid L_spinz_varid" ) - call check( nf90_inq_varid(self%ncid, L_ESCAPEX_VARNAME, self%L_escapex_varid), "netcdf_open nf90_inq_varid L_escapex_varid" ) - call check( nf90_inq_varid(self%ncid, L_ESCAPEY_VARNAME, self%L_escapey_varid), "netcdf_open nf90_inq_varid L_escapey_varid" ) - call check( nf90_inq_varid(self%ncid, L_ESCAPEZ_VARNAME, self%L_escapez_varid), "netcdf_open nf90_inq_varid L_escapez_varid" ) - call check( nf90_inq_varid(self%ncid, ECOLLISIONS_VARNAME, self%Ecollisions_varid), "netcdf_open nf90_inq_varid Ecollisions_varid" ) - call check( nf90_inq_varid(self%ncid, EUNTRACKED_VARNAME, self%Euntracked_varid), "netcdf_open nf90_inq_varid Euntracked_varid" ) - call check( nf90_inq_varid(self%ncid, GMESCAPE_VARNAME, self%GMescape_varid), "netcdf_open nf90_inq_varid GMescape_varid" ) + ! Optional Variables + + status = nf90_inq_varid(self%ncid, NPL_VARNAME, self%npl_varid) + if (status /= nf90_noerr) write(*,*) "Warning! NPL variable not set in input file. Calculating." + + status = nf90_inq_varid(self%ncid, NTP_VARNAME, self%ntp_varid) + if (status /= nf90_noerr) write(*,*) "Warning! NTP variable not set in input file. Calculating." + + if (param%integrator == SYMBA) then + status = nf90_inq_varid(self%ncid, NPLM_VARNAME, self%nplm_varid) + if (status /= nf90_noerr) write(*,*) "Warning! NPLM variable not set in input file. Calculating." end if - call check( nf90_inq_varid(self%ncid, J2RP2_VARNAME, self%j2rp2_varid), "netcdf_open nf90_inq_varid j2rp2_varid" ) - call check( nf90_inq_varid(self%ncid, J4RP4_VARNAME, self%j4rp4_varid), "netcdf_open nf90_inq_varid j4rp4_varid" ) + if (param%lrhill_present) then + status = nf90_inq_varid(self%ncid, RHILL_VARNAME, self%rhill_varid) + if (status /= nf90_noerr) write(*,*) "Warning! RHILL variable not set in input file. Calculating." + end if + + ! Variables The User Doesn't Need to Know About + + status = nf90_inq_varid(self%ncid, STATUS_VARNAME, self%status_varid) + status = nf90_inq_varid(self%ncid, J2RP2_VARNAME, self%j2rp2_varid) + status = nf90_inq_varid(self%ncid, J4RP4_VARNAME, self%j4rp4_varid) + if (param%lclose) then + status = nf90_inq_varid(self%ncid, ORIGIN_TYPE_VARNAME, self%origin_type_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_TIME_VARNAME, self%origin_time_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_XHX_VARNAME, self%origin_xhx_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_XHY_VARNAME, self%origin_xhy_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_XHZ_VARNAME, self%origin_xhz_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_VHX_VARNAME, self%origin_vhx_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_VHY_VARNAME, self%origin_vhy_varid) + status = nf90_inq_varid(self%ncid, ORIGIN_VHZ_VARNAME, self%origin_vhz_varid) + status = nf90_inq_varid(self%ncid, COLLISION_ID_VARNAME, self%collision_id_varid) + status = nf90_inq_varid(self%ncid, DISCARD_TIME_VARNAME, self%discard_time_varid) + status = nf90_inq_varid(self%ncid, DISCARD_XHX_VARNAME, self%discard_xhx_varid) + status = nf90_inq_varid(self%ncid, DISCARD_XHY_VARNAME, self%discard_xhy_varid) + status = nf90_inq_varid(self%ncid, DISCARD_XHZ_VARNAME, self%discard_xhz_varid) + status = nf90_inq_varid(self%ncid, DISCARD_VHX_VARNAME, self%discard_vhx_varid) + status = nf90_inq_varid(self%ncid, DISCARD_VHY_VARNAME, self%discard_vhy_varid) + status = nf90_inq_varid(self%ncid, DISCARD_VHZ_VARNAME, self%discard_vhz_varid) + status = nf90_inq_varid(self%ncid, DISCARD_BODY_ID_VARNAME, self%discard_body_id_varid) + end if + + if (param%lenergy) then + status = nf90_inq_varid(self%ncid, KE_ORB_VARNAME, self%KE_orb_varid) + status = nf90_inq_varid(self%ncid, KE_SPIN_VARNAME, self%KE_spin_varid) + status = nf90_inq_varid(self%ncid, PE_VARNAME, self%PE_varid) + status = nf90_inq_varid(self%ncid, L_ORBX_VARNAME, self%L_orbx_varid) + status = nf90_inq_varid(self%ncid, L_ORBY_VARNAME, self%L_orby_varid) + status = nf90_inq_varid(self%ncid, L_ORBZ_VARNAME, self%L_orbz_varid) + status = nf90_inq_varid(self%ncid, L_SPINX_VARNAME, self%L_spinx_varid) + status = nf90_inq_varid(self%ncid, L_SPINY_VARNAME, self%L_spiny_varid) + status = nf90_inq_varid(self%ncid, L_SPINZ_VARNAME, self%L_spinz_varid) + status = nf90_inq_varid(self%ncid, L_ESCAPEX_VARNAME, self%L_escapex_varid) + status = nf90_inq_varid(self%ncid, L_ESCAPEY_VARNAME, self%L_escapey_varid) + status = nf90_inq_varid(self%ncid, L_ESCAPEZ_VARNAME, self%L_escapez_varid) + status = nf90_inq_varid(self%ncid, ECOLLISIONS_VARNAME, self%Ecollisions_varid) + status = nf90_inq_varid(self%ncid, EUNTRACKED_VARNAME, self%Euntracked_varid) + status = nf90_inq_varid(self%ncid, GMESCAPE_VARNAME, self%GMescape_varid) + end if return end subroutine netcdf_open @@ -487,7 +514,7 @@ module function netcdf_read_frame_system(self, iu, param) result(ierr) ! Return integer(I4B) :: ierr !! Error code: returns 0 if the read is successful ! Internals - integer(I4B) :: tslot, idmax, npl_check, ntp_check, nplm_check, t_max, str_max + integer(I4B) :: tslot, idmax, npl_check, ntp_check, nplm_check, t_max, str_max, status real(DP), dimension(:), allocatable :: rtemp integer(I4B), dimension(:), allocatable :: itemp logical, dimension(:), allocatable :: validmask, tpmask, plmask @@ -544,7 +571,7 @@ module function netcdf_read_frame_system(self, iu, param) result(ierr) class is (symba_pl) select type (param) class is (symba_parameters) - nplm_check = count(rtemp(:) > param%GMTINY .and. plmask(:)) + nplm_check = count(pack(rtemp,plmask) > param%GMTINY ) if (nplm_check /= pl%nplm) then write(*,*) "Error reading in NetCDF file: The recorded value of nplm does not match the number of active fully interacting massive bodies" call util_exit(failure) @@ -594,28 +621,39 @@ module function netcdf_read_frame_system(self, iu, param) result(ierr) end if if ((param%in_form == EL) .or. (param%in_form == XVEL)) then - call check( nf90_get_var(iu%ncid, iu%a_varid, rtemp, start=[1, tslot]), "netcdf_read_frame_system nf90_getvar a_varid" ) + if (.not.allocated(pl%a)) allocate(pl%a(npl)) + if (.not.allocated(tp%a)) allocate(tp%a(ntp)) if (npl > 0) pl%a(:) = pack(rtemp, plmask) if (ntp > 0) tp%a(:) = pack(rtemp, tpmask) call check( nf90_get_var(iu%ncid, iu%e_varid, rtemp, start=[1, tslot]), "netcdf_read_frame_system nf90_getvar e_varid" ) + if (.not.allocated(pl%e)) allocate(pl%e(npl)) + if (.not.allocated(tp%e)) allocate(tp%e(ntp)) if (npl > 0) pl%e(:) = pack(rtemp, plmask) if (ntp > 0) tp%e(:) = pack(rtemp, tpmask) call check( nf90_get_var(iu%ncid, iu%inc_varid, rtemp, start=[1, tslot]), "netcdf_read_frame_system nf90_getvar inc_varid" ) + if (.not.allocated(pl%inc)) allocate(pl%inc(npl)) + if (.not.allocated(tp%inc)) allocate(tp%inc(ntp)) if (npl > 0) pl%inc(:) = pack(rtemp, plmask) if (ntp > 0) tp%inc(:) = pack(rtemp, tpmask) call check( nf90_get_var(iu%ncid, iu%capom_varid, rtemp, start=[1, tslot]), "netcdf_read_frame_system nf90_getvar capom_varid" ) + if (.not.allocated(pl%capom)) allocate(pl%capom(npl)) + if (.not.allocated(tp%capom)) allocate(tp%capom(ntp)) if (npl > 0) pl%capom(:) = pack(rtemp, plmask) if (ntp > 0) tp%capom(:) = pack(rtemp, tpmask) call check( nf90_get_var(iu%ncid, iu%omega_varid, rtemp, start=[1, tslot]), "netcdf_read_frame_system nf90_getvar omega_varid" ) + if (.not.allocated(pl%omega)) allocate(pl%omega(npl)) + if (.not.allocated(tp%omega)) allocate(tp%omega(ntp)) if (npl > 0) pl%omega(:) = pack(rtemp, plmask) if (ntp > 0) tp%omega(:) = pack(rtemp, tpmask) call check( nf90_get_var(iu%ncid, iu%capm_varid, rtemp, start=[1, tslot]), "netcdf_read_frame_system nf90_getvar capm_varid" ) + if (.not.allocated(pl%capm)) allocate(pl%capm(npl)) + if (.not.allocated(tp%capm)) allocate(tp%capm(ntp)) if (npl > 0) pl%capm(:) = pack(rtemp, plmask) if (ntp > 0) tp%capm(:) = pack(rtemp, tpmask) @@ -699,11 +737,26 @@ module function netcdf_read_frame_system(self, iu, param) result(ierr) ! if (npl > 0) pl%Q(:) = pack(rtemp, plmask) ! end if - call check( nf90_get_var(iu%ncid, iu%j2rp2_varid, cb%j2rp2, start=[tslot]), "netcdf_read_frame_system nf90_getvar j2rp2_varid" ) - call check( nf90_get_var(iu%ncid, iu%j4rp4_varid, cb%j4rp4, start=[tslot]), "netcdf_read_frame_system nf90_getvar j4rp4_varid" ) + status = nf90_inq_varid(iu%ncid, J2RP2_VARNAME, iu%j2rp2_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%j2rp2_varid, cb%j2rp2, start=[tslot]), "netcdf_read_frame_system nf90_getvar j2rp2_varid" ) + else + cb%j2rp2 = 0.0_DP + end if + + status = nf90_inq_varid(iu%ncid, J4RP4_VARNAME, iu%j4rp4_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%j4rp4_varid, cb%j4rp4, start=[tslot]), "netcdf_read_frame_system nf90_getvar j4rp4_varid" ) + else + cb%j4rp4 = 0.0_DP + end if call self%read_particle_info(iu, param, plmask, tpmask) + if (param%in_form == "EL") then + call pl%el2xv(cb) + call tp%el2xv(cb) + end if ! if this is a GR-enabled run, check to see if we got the pseudovelocities in. Otherwise, we'll need to generate them. if (param%lgr .and. .not.(iu%lpseudo_vel_exists)) then call pl%set_mu(cb) @@ -737,34 +790,90 @@ module subroutine netcdf_read_hdr_system(self, iu, param) class(netcdf_parameters), intent(inout) :: iu !! Parameters used to for writing a NetCDF dataset to file class(swiftest_parameters), intent(inout) :: param !! Current run configuration parameters ! Internals - integer(I4B) :: tslot + integer(I4B) :: tslot, status, idmax + real(DP), dimension(:), allocatable :: gmtemp + logical, dimension(:), allocatable :: plmask, tpmask, plmmask - tslot = int(param%ioutput, kind=I4B) + 1 + tslot = int(param%ioutput, kind=I4B) + 1 + call check( nf90_inquire_dimension(iu%ncid, iu%id_dimid, len=idmax), "netcdf_read_frame_system nf90_inquire_dimension id_dimid" ) call check( nf90_get_var(iu%ncid, iu%time_varid, param%t, start=[tslot]), "netcdf_read_hdr_system nf90_getvar time_varid" ) - call check( nf90_get_var(iu%ncid, iu%npl_varid, self%pl%nbody, start=[tslot]), "netcdf_read_hdr_system nf90_getvar npl_varid" ) - call check( nf90_get_var(iu%ncid, iu%ntp_varid, self%tp%nbody, start=[tslot]), "netcdf_read_hdr_system nf90_getvar ntp_varid" ) - select type(pl => self%pl) - class is (symba_pl) - call check( nf90_get_var(iu%ncid, iu%nplm_varid, pl%nplm, start=[tslot]), "netcdf_read_hdr_system nf90_getvar nplm_varid" ) + + allocate(gmtemp(idmax)) + allocate(tpmask(idmax)) + allocate(plmask(idmax)) + allocate(plmmask(idmax)) + + call check( nf90_get_var(iu%ncid, iu%Gmass_varid, gmtemp, start=[1,1]), "netcdf_read_frame_system nf90_getvar Gmass_varid" ) + + plmask(:) = gmtemp(:) == gmtemp(:) + tpmask(:) = .not. plmask(:) + plmask(1) = .false. ! This is the central body + select type (param) + class is (symba_parameters) + plmmask(:) = plmask(:) + where(plmask(:)) + plmmask(:) = gmtemp(:) > param%GMTINY + endwhere end select + status = nf90_inq_varid(iu%ncid, NPL_VARNAME, iu%npl_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%npl_varid, self%pl%nbody, start=[tslot]), "netcdf_read_hdr_system nf90_getvar npl_varid" ) + else + self%pl%nbody = count(plmask(:)) + end if + + status = nf90_inq_varid(iu%ncid, NTP_VARNAME, iu%ntp_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%ntp_varid, self%tp%nbody, start=[tslot]), "netcdf_read_hdr_system nf90_getvar ntp_varid" ) + else + self%tp%nbody = count(tpmask(:)) + end if + + if (param%integrator == SYMBA) then + status = nf90_inq_varid(iu%ncid, NPLM_VARNAME, iu%nplm_varid) + select type(pl => self%pl) + class is (symba_pl) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%nplm_varid, pl%nplm, start=[tslot]), "netcdf_read_hdr_system nf90_getvar nplm_varid" ) + else + pl%nplm = count(plmmask(:)) + end if + end select + end if + if (param%lenergy) then - call check( nf90_get_var(iu%ncid, iu%KE_orb_varid, self%ke_orbit, start=[tslot]), "netcdf_read_hdr_system nf90_getvar KE_orb_varid" ) - call check( nf90_get_var(iu%ncid, iu%KE_spin_varid, self%ke_spin, start=[tslot]), "netcdf_read_hdr_system nf90_getvar KE_spin_varid" ) - call check( nf90_get_var(iu%ncid, iu%PE_varid, self%pe, start=[tslot]), "netcdf_read_hdr_system nf90_getvar PE_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_orbx_varid, self%Lorbit(1), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_orbx_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_orby_varid, self%Lorbit(2), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_orby_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_orbz_varid, self%Lorbit(3), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_orbz_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_spinx_varid, self%Lspin(1), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_spinx_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_spiny_varid, self%Lspin(2), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_spiny_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_spinz_varid, self%Lspin(3), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_spinz_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_escapex_varid, self%Lescape(1), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_escapex_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_escapey_varid, self%Lescape(2), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_escapey_varid" ) - call check( nf90_get_var(iu%ncid, iu%L_escapez_varid, self%Lescape(3), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_escapez_varid" ) - call check( nf90_get_var(iu%ncid, iu%Ecollisions_varid, self%Ecollisions, start=[tslot]), "netcdf_read_hdr_system nf90_getvar Ecollisions_varid" ) - call check( nf90_get_var(iu%ncid, iu%Euntracked_varid, self%Euntracked, start=[tslot]), "netcdf_read_hdr_system nf90_getvar Euntracked_varid" ) - call check( nf90_get_var(iu%ncid, iu%GMescape_varid, self%GMescape, start=[tslot]), "netcdf_read_hdr_system nf90_getvar GMescape_varid" ) + status = nf90_inq_varid(iu%ncid, KE_ORB_VARNAME, iu%KE_orb_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%KE_orb_varid, self%ke_orbit, start=[tslot]), "netcdf_read_hdr_system nf90_getvar KE_orb_varid" ) + status = nf90_inq_varid(iu%ncid, KE_SPIN_VARNAME, iu%KE_spin_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%KE_spin_varid, self%ke_spin, start=[tslot]), "netcdf_read_hdr_system nf90_getvar KE_spin_varid" ) + status = nf90_inq_varid(iu%ncid, PE_VARNAME, iu%PE_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%PE_varid, self%pe, start=[tslot]), "netcdf_read_hdr_system nf90_getvar PE_varid" ) + status = nf90_inq_varid(iu%ncid, L_ORBX_VARNAME, iu%L_orbx_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_orbx_varid, self%Lorbit(1), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_orbx_varid" ) + status = nf90_inq_varid(iu%ncid, L_ORBY_VARNAME, iu%L_orby_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_orby_varid, self%Lorbit(2), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_orby_varid" ) + status = nf90_inq_varid(iu%ncid, L_ORBZ_VARNAME, iu%L_orbz_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_orbz_varid, self%Lorbit(3), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_orbz_varid" ) + status = nf90_inq_varid(iu%ncid, L_SPINX_VARNAME, iu%L_spinx_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_spinx_varid, self%Lspin(1), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_spinx_varid" ) + status = nf90_inq_varid(iu%ncid, L_SPINY_VARNAME, iu%L_spiny_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_spiny_varid, self%Lspin(2), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_spiny_varid" ) + status = nf90_inq_varid(iu%ncid, L_SPINZ_VARNAME, iu%L_spinz_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_spinz_varid, self%Lspin(3), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_spinz_varid" ) + status = nf90_inq_varid(iu%ncid, L_ESCAPEX_VARNAME, iu%L_escapex_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_escapex_varid, self%Lescape(1), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_escapex_varid" ) + status = nf90_inq_varid(iu%ncid, L_ESCAPEY_VARNAME, iu%L_escapey_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_escapey_varid, self%Lescape(2), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_escapey_varid" ) + status = nf90_inq_varid(iu%ncid, L_ESCAPEZ_VARNAME, iu%L_escapez_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%L_escapez_varid, self%Lescape(3), start=[tslot]), "netcdf_read_hdr_system nf90_getvar L_escapez_varid" ) + status = nf90_inq_varid(iu%ncid, ECOLLISIONS_VARNAME, iu%Ecollisions_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%Ecollisions_varid, self%Ecollisions, start=[tslot]), "netcdf_read_hdr_system nf90_getvar Ecollisions_varid" ) + status = nf90_inq_varid(iu%ncid, EUNTRACKED_VARNAME, iu%Euntracked_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%Euntracked_varid, self%Euntracked, start=[tslot]), "netcdf_read_hdr_system nf90_getvar Euntracked_varid" ) + status = nf90_inq_varid(iu%ncid, GMESCAPE_VARNAME, iu%GMescape_varid) + if (status == nf90_noerr) call check( nf90_get_var(iu%ncid, iu%GMescape_varid, self%GMescape, start=[tslot]), "netcdf_read_hdr_system nf90_getvar GMescape_varid" ) end if return @@ -783,12 +892,12 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma logical, dimension(:), intent(in) :: plmask !! Logical array indicating which index values belong to massive bodies logical, dimension(:), intent(in) :: tpmask !! Logical array indicating which index values belong to test particles ! Internals - integer(I4B) :: i, idmax - real(DP), dimension(:), allocatable :: rtemp + integer(I4B) :: i, idmax, status + real(DP), dimension(:), allocatable :: rtemp real(DP), dimension(:,:), allocatable :: rtemp_arr - integer(I4B), dimension(:), allocatable :: itemp + integer(I4B), dimension(:), allocatable :: itemp character(len=NAMELEN), dimension(:), allocatable :: ctemp - integer(I4B), dimension(:), allocatable :: plind, tpind + integer(I4B), dimension(:), allocatable :: plind, tpind ! This string of spaces of length NAMELEN is used to clear out any old data left behind inside the string variables idmax = size(plmask) @@ -841,8 +950,13 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(particle_type=ctemp(tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%status_varid, ctemp, count=[NAMELEN, idmax]), "netcdf_read_particle_info_system nf90_getvar status_varid" ) - call cb%info%set_value(status=ctemp(1)) + status = nf90_inq_varid(iu%ncid, STATUS_VARNAME, iu%status_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%status_varid, ctemp, count=[NAMELEN, idmax]), "netcdf_read_particle_info_system nf90_getvar status_varid") + call cb%info%set_value(status=ctemp(1)) + else + call cb%info%set_value(status="ACTIVE") + end if do i = 1, npl call pl%info(i)%set_value(status=ctemp(plind(i))) end do @@ -851,7 +965,14 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma end do if (param%lclose) then - call check( nf90_get_var(iu%ncid, iu%origin_type_varid, ctemp, count=[NAMELEN, idmax]), "netcdf_read_particle_info_system nf90_getvar origin_type_varid" ) + + status = nf90_inq_varid(iu%ncid, ORIGIN_TYPE_VARNAME, iu%origin_type_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_type_varid, ctemp, count=[NAMELEN, idmax]), "netcdf_read_particle_info_system nf90_getvar origin_type_varid" ) + else + ctemp = "Initial Conditions" + end if + call cb%info%set_value(origin_type=ctemp(1)) do i = 1, npl call pl%info(i)%set_value(origin_type=ctemp(plind(i))) @@ -860,7 +981,13 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(origin_type=ctemp(tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%origin_time_varid, rtemp), "netcdf_read_particle_info_system nf90_getvar origin_time_varid" ) + status = nf90_inq_varid(iu%ncid, ORIGIN_TIME_VARNAME, iu%origin_time_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_time_varid, rtemp), "netcdf_read_particle_info_system nf90_getvar origin_time_varid" ) + else + rtemp = 0.0_DP + end if + call cb%info%set_value(origin_time=rtemp(1)) do i = 1, npl call pl%info(i)%set_value(origin_time=rtemp(plind(i))) @@ -869,9 +996,31 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(origin_time=rtemp(tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%origin_xhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar origin_xhx_varid" ) - call check( nf90_get_var(iu%ncid, iu%origin_xhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar origin_xhy_varid" ) - call check( nf90_get_var(iu%ncid, iu%origin_xhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar origin_xhz_varid" ) + + status = nf90_inq_varid(iu%ncid, ORIGIN_XHX_VARNAME, iu%origin_xhx_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_xhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar origin_xhx_varid" ) + else + ! [TODO]: This doesn't work when the input mode is EL. This needs to get filled in later after xv2el has been called + ! call check( nf90_get_var(iu%ncid, iu%xhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar xhx_varid" ) + end if + + status = nf90_inq_varid(iu%ncid, ORIGIN_XHY_VARNAME, iu%origin_xhy_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_xhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar origin_xhy_varid" ) + else + ! [TODO]: This doesn't work when the input mode is EL. This needs to get filled in later after xv2el has been called + ! call check( nf90_get_var(iu%ncid, iu%xhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar xhy_varid" ) + end if + + status = nf90_inq_varid(iu%ncid, ORIGIN_XHZ_VARNAME, iu%origin_xhz_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_xhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar origin_xhz_varid" ) + else + ! [TODO]: This doesn't work when the input mode is EL. This needs to get filled in later after xv2el has been called + ! call check( nf90_get_var(iu%ncid, iu%xhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar xhz_varid" ) + end if + do i = 1, npl call pl%info(i)%set_value(origin_xh=rtemp_arr(:,plind(i))) end do @@ -879,9 +1028,30 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(origin_xh=rtemp_arr(:,tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%origin_vhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar origin_vhx_varid" ) - call check( nf90_get_var(iu%ncid, iu%origin_vhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar origin_vhy_varid" ) - call check( nf90_get_var(iu%ncid, iu%origin_vhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar origin_vhz_varid" ) + status = nf90_inq_varid(iu%ncid, ORIGIN_VHX_VARNAME, iu%origin_vhx_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_vhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar origin_vhx_varid" ) + else + ! [TODO]: This doesn't work when the input mode is EL. This needs to get filled in later after xv2el has been called + ! call check( nf90_get_var(iu%ncid, iu%vhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar vhx_varid" ) + end if + + status = nf90_inq_varid(iu%ncid, ORIGIN_VHY_VARNAME, iu%origin_vhy_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_vhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar origin_vhy_varid" ) + else + ! [TODO]: This doesn't work when the input mode is EL. This needs to get filled in later after xv2el has been called + ! call check( nf90_get_var(iu%ncid, iu%vhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar vhy_varid" ) + end if + + status = nf90_inq_varid(iu%ncid, ORIGIN_VHZ_VARNAME, iu%origin_vhz_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%origin_vhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar origin_vhz_varid" ) + else + ! [TODO]: This doesn't work when the input mode is EL. This needs to get filled in later after xv2el has been called + ! call check( nf90_get_var(iu%ncid, iu%vhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar vhz_varid" ) + end if + do i = 1, npl call pl%info(i)%set_value(origin_vh=rtemp_arr(:,plind(i))) end do @@ -889,7 +1059,13 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(origin_vh=rtemp_arr(:,tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%collision_id_varid, itemp), "netcdf_read_particle_info_system nf90_getvar collision_id_varid" ) + status = nf90_inq_varid(iu%ncid, COLLISION_ID_VARNAME, iu%collision_id_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%collision_id_varid, itemp), "netcdf_read_particle_info_system nf90_getvar collision_id_varid" ) + else + itemp = 0.0_DP + end if + do i = 1, npl call pl%info(i)%set_value(collision_id=itemp(plind(i))) end do @@ -897,7 +1073,13 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(collision_id=itemp(tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%discard_time_varid, rtemp), "netcdf_read_particle_info_system nf90_getvar discard_time_varid" ) + status = nf90_inq_varid(iu%ncid, DISCARD_TIME_VARNAME, iu%discard_time_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_time_varid, rtemp), "netcdf_read_particle_info_system nf90_getvar discard_time_varid" ) + else + rtemp = 0.0_DP + end if + call cb%info%set_value(discard_time=rtemp(1)) do i = 1, npl call pl%info(i)%set_value(discard_time=rtemp(plind(i))) @@ -906,9 +1088,27 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(discard_time=rtemp(tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%discard_xhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar discard_xhx_varid" ) - call check( nf90_get_var(iu%ncid, iu%discard_xhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar discard_xhy_varid" ) - call check( nf90_get_var(iu%ncid, iu%discard_xhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar discard_xhz_varid" ) + status = nf90_inq_varid(iu%ncid, DISCARD_XHX_VARNAME, iu%discard_xhx_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_xhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar discard_xhx_varid" ) + else + rtemp_arr(1,:) = 0.0_DP + end if + + status = nf90_inq_varid(iu%ncid, DISCARD_XHY_VARNAME, iu%discard_xhy_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_xhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar discard_xhy_varid" ) + else + rtemp_arr(2,:) = 0.0_DP + end if + + status = nf90_inq_varid(iu%ncid, DISCARD_XHZ_VARNAME, iu%discard_xhz_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_xhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar discard_xhz_varid" ) + else + rtemp_arr(3,:) = 0.0_DP + end if + do i = 1, npl call pl%info(i)%set_value(discard_xh=rtemp_arr(:,plind(i))) end do @@ -916,9 +1116,27 @@ module subroutine netcdf_read_particle_info_system(self, iu, param, plmask, tpma call tp%info(i)%set_value(discard_xh=rtemp_arr(:,tpind(i))) end do - call check( nf90_get_var(iu%ncid, iu%discard_vhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar discard_vhx_varid" ) - call check( nf90_get_var(iu%ncid, iu%discard_vhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar discard_vhy_varid" ) - call check( nf90_get_var(iu%ncid, iu%discard_vhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar discard_vhz_varid" ) + status = nf90_inq_varid(iu%ncid, DISCARD_VHX_VARNAME, iu%discard_vhx_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_vhx_varid, rtemp_arr(1,:)), "netcdf_read_particle_info_system nf90_getvar discard_vhx_varid" ) + else + rtemp_arr(1,:) = 0.0_DP + end if + + status = nf90_inq_varid(iu%ncid, DISCARD_VHY_VARNAME, iu%discard_vhy_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_vhy_varid, rtemp_arr(2,:)), "netcdf_read_particle_info_system nf90_getvar discard_vhy_varid" ) + else + rtemp_arr(2,:) = 0.0_DP + end if + + status = nf90_inq_varid(iu%ncid, DISCARD_VHZ_VARNAME, iu%discard_vhz_varid) + if (status == nf90_noerr) then + call check( nf90_get_var(iu%ncid, iu%discard_vhz_varid, rtemp_arr(3,:)), "netcdf_read_particle_info_system nf90_getvar discard_vhz_varid" ) + else + rtemp_arr(3,:) = 0.0_DP + end if + do i = 1, npl call pl%info(i)%set_value(discard_vh=rtemp_arr(:,plind(i))) end do diff --git a/src/orbel/orbel.f90 b/src/orbel/orbel.f90 index 9dc68c397..5e7c4a989 100644 --- a/src/orbel/orbel.f90 +++ b/src/orbel/orbel.f90 @@ -30,7 +30,6 @@ module subroutine orbel_el2xv_vec(self, cb) call orbel_el2xv(self%mu(i), self%a(i), self%e(i), self%inc(i), self%capom(i), & self%omega(i), self%capm(i), self%xh(:, i), self%vh(:, i)) end do - deallocate(self%a, self%e, self%inc, self%capom, self%omega, self%capm) return end subroutine orbel_el2xv_vec