feat(validation): added more of the scripts to make paper figures
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
import os.path
|
||||
import shutil
|
||||
|
||||
from gridfire.policy import MainSequencePolicy, NetworkPolicy
|
||||
from gridfire.engine import DynamicEngine, GraphEngine, EngineTypes, MultiscalePartitioningEngineView
|
||||
from gridfire.solver import PointSolverContext
|
||||
from gridfire.type import NetIn
|
||||
from gridfire.policy import ConstructionResults
|
||||
|
||||
from typing import Dict
|
||||
from fourdst.composition import Composition
|
||||
|
||||
from testsuite import TestSuite
|
||||
from utils import init_netIn, init_composition, years_to_seconds
|
||||
|
||||
from enum import Enum
|
||||
|
||||
EngineNameToType: Dict[str, EngineTypes] = {
|
||||
"graphengine": EngineTypes.GRAPH_ENGINE,
|
||||
"multiscalepartitioningengineview": EngineTypes.MULTISCALE_PARTITIONING_ENGINE_VIEW,
|
||||
}
|
||||
|
||||
class SolarLikeStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="SolarLikeStar_QSE",
|
||||
description="GridFire simulation of a roughly solar like star over 10Gyr with QSE enabled.",
|
||||
temp=1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class MetalEnhancedSolarLikeStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition(ZZs=1)
|
||||
super().__init__(
|
||||
name="MetalEnhancedSolarLikeStar_QSE",
|
||||
description="GridFire simulation of a star with solar core temp and density but enhanced by 1 dex in Z.",
|
||||
temp=0.8 * 1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View, Z enhanced by 1 dex, temperature reduced to 80% of solar core"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class MetalEnhancedSolarLikeStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition(ZZs=1)
|
||||
super().__init__(
|
||||
name="MetalEnhancedSolarLikeStar_No_QSE",
|
||||
description="GridFire simulation of a star with solar core temp and density but enhanced by 1 dex in Z.",
|
||||
temp=0.8 * 1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, Z enhanced by 1 dex, temperature reduced to 80% of solar core"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
engine: GraphEngine = GraphEngine(self.composition, 4)
|
||||
blob = engine.constructStateBlob()
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
|
||||
class MetalDepletedSolarLikeStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition(ZZs=-1)
|
||||
super().__init__(
|
||||
name="MetalDepletedSolarLikeStar_QSE",
|
||||
description="GridFire simulation of a star with solar core temp and density but depleted by 1 dex in Z.",
|
||||
temp=1.2 * 1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View, Z depleted by 1 dex, temperature increased to 120% of solar core"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class MetalDepletedSolarLikeStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition(ZZs=-1)
|
||||
super().__init__(
|
||||
name="MetalDepletedSolarLikeStar_No_QSE",
|
||||
description="GridFire simulation of a star with solar core temp and density but depleted by 1 dex in Z.",
|
||||
temp=1.2 * 1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, Z depleted by 1 dex, temperature increased to 120% of solar core"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
blob = engine.constructStateBlob()
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class SolarLikeStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="SolarLikeStar_No_QSE",
|
||||
description="GridFire simulation of a roughly solar like star over 10Gyr with QSE disabled.",
|
||||
temp=1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, No MultiscalePartitioning Engine View"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
engine : GraphEngine = GraphEngine(self.composition, 3)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
context : PointSolverContext = PointSolverContext(engine.constructStateBlob())
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class SolarLikeStar_No_QSE_Depth_Suite(TestSuite):
|
||||
def __init__(self, depth: int = 1):
|
||||
initialComposition : Composition = init_composition()
|
||||
self.depth : int = depth
|
||||
super().__init__(
|
||||
name=f"SolarLikeStar_No_QSE_Depth_{depth}_Suite",
|
||||
description="GridFire simulation of a roughly solar like star over 10Gyr with QSE disabled.",
|
||||
temp=1.5e7,
|
||||
density=1.5e2,
|
||||
tMax=years_to_seconds(1e10),
|
||||
composition=initialComposition,
|
||||
notes=f"Thermodynamically Static, No MultiscalePartitioning Engine View, configurable depth {depth}"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
engine : GraphEngine = GraphEngine(self.composition, self.depth)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
context : PointSolverContext = PointSolverContext(engine.constructStateBlob())
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class HotStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="HotStar_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE enabled.",
|
||||
temp=2.5e7,
|
||||
density=15,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View, B(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class CoolStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="CoolStar_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE enabled.",
|
||||
temp=6e6,
|
||||
density=750,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View, M(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
class HotStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="HotStar_No_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE disabled.",
|
||||
temp=2.5e7,
|
||||
density=15,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, B(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class CoolStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="CoolStar_No_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE disabled.",
|
||||
temp=6e6,
|
||||
density=750,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, M(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class VeryCoolStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="VeryCoolStar_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE enabled.",
|
||||
temp=4e6,
|
||||
density=1000,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View, M(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class VeryCoolStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="VeryCoolStar_No_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE disabled.",
|
||||
temp=4e6,
|
||||
density=1000,
|
||||
tMax=1e19,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, M(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
engine: GraphEngine = GraphEngine(self.composition, 3)
|
||||
blob = engine.constructStateBlob()
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class VeryHotStar_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="VeryHotStar_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE enabled.",
|
||||
temp=4e7,
|
||||
density=1,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, MultiscalePartitioning Engine View, M(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
base_engine: GraphEngine = GraphEngine(self.composition, 4)
|
||||
engine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(base_engine)
|
||||
blob = base_engine.constructStateBlob()
|
||||
blob = engine.constructStateBlob(blob)
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
class VeryHotStar_No_QSE_Suite(TestSuite):
|
||||
def __init__(self):
|
||||
initialComposition : Composition = init_composition()
|
||||
super().__init__(
|
||||
name="VeryHotStar_No_QSE",
|
||||
description="GridFire simulation of a hot star over 1Gyr with QSE disabled.",
|
||||
temp=4e7,
|
||||
density=1,
|
||||
tMax=1e15,
|
||||
composition=initialComposition,
|
||||
notes="Thermodynamically Static, B(ish) star conditions"
|
||||
)
|
||||
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
engine: GraphEngine = GraphEngine(self.composition, 4)
|
||||
blob = engine.constructStateBlob()
|
||||
context : PointSolverContext = PointSolverContext(blob)
|
||||
netIn : NetIn = init_netIn(self.temperature, self.density, self.tMax, self.composition)
|
||||
self.evolve(engine, context, netIn, pynucastro_compare = pynucastro_compare, engine_type=EngineNameToType[pync_engine.lower()], output=output)
|
||||
|
||||
|
||||
class ValidationSuites(Enum):
|
||||
SolarLikeStar_QSE = SolarLikeStar_QSE_Suite
|
||||
SolarLikeStar_No_QSE = SolarLikeStar_No_QSE_Suite
|
||||
SolarLikeStar_No_QSE_Depth = SolarLikeStar_No_QSE_Depth_Suite
|
||||
MetalDepletedSolarLikeStar_QSE = MetalDepletedSolarLikeStar_QSE_Suite
|
||||
MetalEnhancedSolarLikeStar_QSE = MetalEnhancedSolarLikeStar_QSE_Suite
|
||||
MetalDepletedSolarLikeStar_No_QSE = MetalDepletedSolarLikeStar_No_QSE_Suite
|
||||
MetalEnhancedSolarLikeStar_No_QSE = MetalEnhancedSolarLikeStar_No_QSE_Suite
|
||||
HotStar_QSE = HotStar_QSE_Suite
|
||||
CoolStar_QSE = CoolStar_QSE_Suite
|
||||
HotStar_No_QSE = HotStar_No_QSE_Suite
|
||||
CoolStar_No_QSE = CoolStar_No_QSE_Suite
|
||||
VeryCoolStar_QSE = VeryCoolStar_QSE_Suite
|
||||
VeryHotStar_QSE = VeryHotStar_QSE_Suite
|
||||
VeryCoolStar_No_QSE = VeryCoolStar_No_QSE_Suite
|
||||
VeryHotStar_No_QSE = VeryHotStar_No_QSE_Suite
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Run some subset of the GridFire validation suite.")
|
||||
parser.add_argument('--suite', type=str, choices=[suite.name for suite in ValidationSuites], nargs="+", help="The validation suite to run.")
|
||||
parser.add_argument("--all", action="store_true", help="Run all validation suites.")
|
||||
parser.add_argument("--pynucastro-compare", action="store_true", help="Generate pynucastro comparison data.")
|
||||
parser.add_argument("--pync-engine", type=str, choices=["GraphEngine", "MultiscalePartitioningEngineView", "AdaptiveEngineView"], default="AdaptiveEngineView", help="The GridFire engine to use to select the reactions for pynucastro comparison.")
|
||||
parser.add_argument("-o", "--output", type=str, help="Directory to save OKAY results too", default="GF_Validation_Output")
|
||||
parser.add_argument("--depths", type=int, nargs="+", default=[1, 2, 3, 4, 5, 6, 7], help="Construction depths to test. Must be positive non zero")
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(args.output, exist_ok=True)
|
||||
|
||||
if args.all:
|
||||
for suite in ValidationSuites:
|
||||
if suite.name == "SolarLikeStar_No_QSE_Depth":
|
||||
for depth in args.depths:
|
||||
instance : TestSuite = suite.value(depth=depth)
|
||||
instance(args.pynucastro_compare, args.pync_engine, args.output)
|
||||
else:
|
||||
instance : TestSuite = suite.value()
|
||||
instance(args.pynucastro_compare, args.pync_engine, args.output)
|
||||
|
||||
else:
|
||||
for suite_name in args.suite:
|
||||
suite = ValidationSuites[suite_name]
|
||||
if suite.name == "SolarLikeStar_No_QSE_Depth":
|
||||
for depth in args.depths:
|
||||
instance : TestSuite = suite.value(depth=depth)
|
||||
instance(args.pynucastro_compare, args.pync_engine, args.output)
|
||||
else:
|
||||
instance : TestSuite = suite.value()
|
||||
instance(args.pynucastro_compare, args.pync_engine, args.output)
|
||||
|
||||
262
validation/ManuscriptFigures/pynucastro/figures.ipynb
Normal file
262
validation/ManuscriptFigures/pynucastro/figures.ipynb
Normal file
File diff suppressed because one or more lines are too long
80
validation/ManuscriptFigures/pynucastro/logger.py
Normal file
80
validation/ManuscriptFigures/pynucastro/logger.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from enum import Enum
|
||||
|
||||
from typing import Dict, List, Any, SupportsFloat
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gridfire.solver import PointSolverTimestepContext
|
||||
from gridfire._gridfire.engine.scratchpads import StateBlob
|
||||
import gridfire
|
||||
|
||||
class LogEntries(Enum):
|
||||
Step = "Step"
|
||||
t = "t"
|
||||
dt = "dt"
|
||||
eps = "eps"
|
||||
Composition = "Composition"
|
||||
ReactionContributions = "ReactionContributions"
|
||||
|
||||
|
||||
class StepLogger:
|
||||
def __init__(self):
|
||||
self.num_steps : int = 0
|
||||
self.steps : List[Dict[LogEntries, Any]] = []
|
||||
|
||||
def log_step(self, ctx: PointSolverTimestepContext):
|
||||
comp_data: Dict[str, SupportsFloat] = {}
|
||||
for species in ctx.engine.getNetworkSpecies(ctx.state_ctx):
|
||||
sid = ctx.engine.getSpeciesIndex(ctx.state_ctx, species)
|
||||
comp_data[species.name()] = ctx.state[sid]
|
||||
entry : Dict[LogEntries, Any] = {
|
||||
LogEntries.Step: ctx.num_steps,
|
||||
LogEntries.t: ctx.t,
|
||||
LogEntries.dt: ctx.dt,
|
||||
LogEntries.eps: ctx.state[-1],
|
||||
LogEntries.Composition: comp_data,
|
||||
}
|
||||
self.steps.append(entry)
|
||||
self.num_steps += 1
|
||||
|
||||
def to_json(self, filename: str, **kwargs):
|
||||
serializable_steps : List[Dict[str, Any]] = [
|
||||
{
|
||||
LogEntries.Step.value: step[LogEntries.Step],
|
||||
LogEntries.t.value: step[LogEntries.t],
|
||||
LogEntries.dt.value: step[LogEntries.dt],
|
||||
LogEntries.eps.value: step[LogEntries.eps],
|
||||
LogEntries.Composition.value: step[LogEntries.Composition],
|
||||
}
|
||||
for step in self.steps
|
||||
]
|
||||
out_data : Dict[str, Any] = {
|
||||
"Metadata": {
|
||||
"NumSteps": self.num_steps,
|
||||
**kwargs,
|
||||
"DateCreated": datetime.now().isoformat(),
|
||||
"GridFireVersion": gridfire.__version__,
|
||||
"Author": "Emily M. Boudreaux",
|
||||
"OS": os.uname().sysname,
|
||||
"ClangVersion": os.popen("clang --version").read().strip(),
|
||||
"GccVersion": os.popen("gcc --version").read().strip(),
|
||||
"PythonVersion": sys.version,
|
||||
},
|
||||
"Steps": serializable_steps
|
||||
}
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(out_data, f, indent=4)
|
||||
|
||||
|
||||
def summary(self) -> Dict[str, Any]:
|
||||
if not self.steps:
|
||||
return {}
|
||||
final_step = self.steps[-1]
|
||||
summary_data : Dict[str, Any] = {
|
||||
"TotalSteps": self.num_steps,
|
||||
"FinalTime": final_step[LogEntries.t],
|
||||
"FinalComposition": final_step[LogEntries.Composition],
|
||||
}
|
||||
return summary_data
|
||||
351
validation/ManuscriptFigures/pynucastro/plot.py
Normal file
351
validation/ManuscriptFigures/pynucastro/plot.py
Normal file
@@ -0,0 +1,351 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from scipy.interpolate import interp1d
|
||||
from scipy.integrate import cumulative_trapezoid
|
||||
from astropy import constants as const
|
||||
from astropy import units as u
|
||||
from enum import Enum
|
||||
from typing import List, Dict, Any, Tuple
|
||||
from fourdst.atomic import species as spdict
|
||||
|
||||
EXTERNAL_STYLE_PATH = "../ManuscriptFigures/utils/pub.mplstyle"
|
||||
|
||||
class PlotVariable(Enum):
|
||||
COMPOSITION = "composition"
|
||||
EPS = "eps"
|
||||
DT = "dt"
|
||||
|
||||
class OutputFormat(Enum):
|
||||
INTERACTIVE = "interactive"
|
||||
PDF = "pdf"
|
||||
PNG = "png"
|
||||
JPEG = "jpeg"
|
||||
|
||||
def discover_runs(base_dir: str) -> List[str]:
|
||||
runs = set()
|
||||
gf_ok_dir = os.path.join(base_dir, "GridFire", "Ok")
|
||||
|
||||
if os.path.exists(gf_ok_dir):
|
||||
for fname in os.listdir(gf_ok_dir):
|
||||
if fname.endswith("_OKAY.json"):
|
||||
runs.add(fname.replace("_OKAY.json", ""))
|
||||
|
||||
return sorted(list(runs))
|
||||
|
||||
def load_run_data(base_dir: str, run_name: str) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||||
gf_data = {}
|
||||
pynuc_data = {}
|
||||
|
||||
gf_ok_path = os.path.join(base_dir, "GridFire", "Ok", f"{run_name}_OKAY.json")
|
||||
gf_err_path = os.path.join(base_dir, "GridFire", "Err", f"{run_name}_FAIL.json")
|
||||
pynuc_path = os.path.join(base_dir, "pynucastro", f"{run_name}_pynucastro.json")
|
||||
|
||||
if os.path.exists(gf_ok_path):
|
||||
with open(gf_ok_path, 'r') as f:
|
||||
gf_data = json.load(f)
|
||||
elif os.path.exists(gf_err_path):
|
||||
with open(gf_err_path, 'r') as f:
|
||||
gf_data = json.load(f)
|
||||
|
||||
if os.path.exists(pynuc_path):
|
||||
with open(pynuc_path, 'r') as f:
|
||||
pynuc_data = json.load(f)
|
||||
|
||||
return gf_data, pynuc_data
|
||||
|
||||
|
||||
def get_pynuc_eps(steps: List[Dict[str, Any]]) -> Tuple[np.ndarray, np.ndarray]:
|
||||
c_sq = (const.c.cgs.value)**2
|
||||
Na = const.N_A.cgs.value
|
||||
amu_to_g = const.u.cgs.value
|
||||
|
||||
eps_history = []
|
||||
time_history = []
|
||||
last_Y = {}
|
||||
last_t = None
|
||||
|
||||
for step in steps:
|
||||
t = step["t"]
|
||||
current_Y = step["Composition"]
|
||||
|
||||
if last_t is None:
|
||||
last_t = t
|
||||
last_Y = current_Y.copy()
|
||||
eps_history.append(0.0)
|
||||
time_history.append(t)
|
||||
continue
|
||||
|
||||
dt = t - last_t
|
||||
|
||||
if dt > 0:
|
||||
dm_dt = 0.0
|
||||
|
||||
all_species = set(current_Y.keys()) | set(last_Y.keys())
|
||||
|
||||
for sp in all_species:
|
||||
y_curr = current_Y.get(sp, 0.0)
|
||||
y_prev = last_Y.get(sp, 0.0)
|
||||
dy = y_curr - y_prev
|
||||
if sp in spdict:
|
||||
mass_g = spdict[sp].mass() * amu_to_g
|
||||
|
||||
dm_dt += mass_g * (dy / dt)
|
||||
rate = -dm_dt * Na * c_sq
|
||||
eps_history.append(rate)
|
||||
time_history.append(t)
|
||||
|
||||
last_t = t
|
||||
last_Y = current_Y.copy()
|
||||
|
||||
return np.array(time_history), np.array(eps_history)
|
||||
|
||||
def _setup_axes(ax_main: plt.Axes, ax_diff: plt.Axes, var: PlotVariable, fig_opts: dict):
|
||||
ax_diff.set_xlabel("Time (s)")
|
||||
ax_main.set_xscale(fig_opts['x_scale'])
|
||||
ax_diff.set_xscale(fig_opts['x_scale'])
|
||||
|
||||
ax_main.set_yscale(fig_opts['y_scale'])
|
||||
|
||||
if var == PlotVariable.EPS:
|
||||
ax_main.set_ylabel("Cumulative Energy (eps)")
|
||||
elif var == PlotVariable.DT:
|
||||
ax_main.set_ylabel("Timestep Size (dt)")
|
||||
elif var == PlotVariable.COMPOSITION:
|
||||
ax_main.set_ylabel("Mass Fraction (X_i)")
|
||||
|
||||
ax_diff.set_ylabel(r"$\Delta \log_{10}$")
|
||||
|
||||
def _plot_single_run(ax_main: plt.Axes, ax_diff: plt.Axes, run_name: str, var: PlotVariable,
|
||||
base_dir: str, compare_pynuc: bool):
|
||||
gf_data, pynuc_data = load_run_data(base_dir, run_name)
|
||||
|
||||
if not gf_data or gf_data.get("Metadata", {}).get("Status") == "Error":
|
||||
return
|
||||
|
||||
gf_steps = gf_data.get("Steps", [])
|
||||
if not gf_steps:
|
||||
return
|
||||
|
||||
if compare_pynuc and not pynuc_data:
|
||||
print(f"Warning: PyNucastro comparison requested but data not found for '{run_name}'.")
|
||||
|
||||
t_gf = np.array([s["t"] for s in gf_steps])
|
||||
|
||||
if var == PlotVariable.COMPOSITION:
|
||||
final_comp = gf_steps[-1]["Composition"]
|
||||
top_species = sorted(final_comp, key=final_comp.get, reverse=True)[:3]
|
||||
|
||||
for spec in top_species:
|
||||
y_gf = np.array([s["Composition"].get(spec, 1e-30) for s in gf_steps])
|
||||
line, = ax_main.plot(t_gf, y_gf, label=f"{run_name} {spec} (GF)")
|
||||
|
||||
if compare_pynuc and pynuc_data:
|
||||
pynuc_steps = pynuc_data.get("Steps", [])
|
||||
if not pynuc_steps:
|
||||
continue
|
||||
|
||||
t_pynuc = np.array([s["t"] for s in pynuc_steps])
|
||||
y_pynuc = np.array([s["Composition"].get(spec, 1e-30) for s in pynuc_steps])
|
||||
|
||||
ax_main.plot(t_pynuc, y_pynuc, '--', color=line.get_color(), label=f"{run_name} {spec} (PyNuc)")
|
||||
|
||||
if len(t_pynuc) > 1:
|
||||
f_interp = interp1d(t_pynuc, y_pynuc, kind='linear', bounds_error=False, fill_value=(y_pynuc[0], y_pynuc[-1]))
|
||||
y_pynuc_interp = f_interp(t_gf)
|
||||
|
||||
log_diff = np.abs(np.log10(np.maximum(y_gf, 1e-30)) - np.log10(np.maximum(y_pynuc_interp, 1e-30)))
|
||||
ax_diff.plot(t_gf, log_diff, color=line.get_color(), linestyle=':', label=f"Δ {spec}")
|
||||
|
||||
elif var == PlotVariable.EPS:
|
||||
y_gf = np.array([s["eps"] for s in gf_steps])
|
||||
line, = ax_main.plot(t_gf, y_gf, label=f"{run_name} (GF)")
|
||||
|
||||
if compare_pynuc and pynuc_data:
|
||||
pynuc_steps = pynuc_data.get("Steps", [])
|
||||
if pynuc_steps:
|
||||
s_t, s_e = get_pynuc_eps(pynuc_steps)
|
||||
|
||||
if len(s_t) > 1:
|
||||
s_cumE = cumulative_trapezoid(s_e, s_t, initial=0)
|
||||
|
||||
ax_main.plot(s_t, s_cumE, '--', color=line.get_color(), label=f"{run_name} (PyNuc)")
|
||||
|
||||
f_pynuc_interp = interp1d(s_t[np.isfinite(s_cumE)], s_cumE[np.isfinite(s_cumE)])
|
||||
f_gf_interp = interp1d(t_gf, y_gf)
|
||||
|
||||
|
||||
t_safe = np.logspace(
|
||||
8,
|
||||
np.log10(min(s_t.max(), t_gf.max())),
|
||||
1000
|
||||
)
|
||||
y_pynuc_interp = f_pynuc_interp(t_safe)
|
||||
y_gf_interp = f_gf_interp(t_safe)
|
||||
|
||||
pynuc_safe = np.maximum(np.abs(y_pynuc_interp), 1e-30)
|
||||
gf_safe = np.maximum(np.abs(y_gf_interp), 1e-30)
|
||||
log_diff = np.log10(gf_safe) - np.log10(pynuc_safe)
|
||||
ax_diff.plot(t_safe, log_diff, color=line.get_color(), linestyle=':', label=f"Δ eps")
|
||||
ax_main.set_xlim(1e8)
|
||||
|
||||
elif var == PlotVariable.DT:
|
||||
y_gf = np.array([s["dt"] for s in gf_steps])
|
||||
ax_main.plot(t_gf, y_gf, label=f"{run_name} (GF)")
|
||||
|
||||
def _finalize_plot(fig: plt.Figure, ax_main: plt.Axes, ax_diff: plt.Axes, format_opt: OutputFormat, filename_base: str, is_subfigure: bool = False):
|
||||
ax_main.legend(loc='best', fontsize='small')
|
||||
|
||||
if len(ax_diff.lines) > 0:
|
||||
ax_diff.legend(loc='best', fontsize='x-small')
|
||||
|
||||
if is_subfigure:
|
||||
return
|
||||
|
||||
fig.tight_layout()
|
||||
|
||||
if format_opt != OutputFormat.INTERACTIVE:
|
||||
out_name = f"{filename_base}.{format_opt.value}"
|
||||
fig.savefig(out_name, format=format_opt.value, bbox_inches='tight')
|
||||
print(f"Saved figure: {out_name}")
|
||||
plt.close(fig)
|
||||
|
||||
def plot_data(runs: List[str], plot_vars: List[PlotVariable], base_dir: str,
|
||||
compare_pynuc: bool, format_opt: OutputFormat, fig_opts: dict):
|
||||
if not runs:
|
||||
print("No valid runs to plot.")
|
||||
return
|
||||
|
||||
if fig_opts['use_ext_style']:
|
||||
try:
|
||||
plt.style.use(EXTERNAL_STYLE_PATH)
|
||||
print(f"Using external style sheet: {EXTERNAL_STYLE_PATH}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load external style sheet. Error: {e}")
|
||||
elif fig_opts['style']:
|
||||
try:
|
||||
plt.style.use(fig_opts['style'])
|
||||
except OSError:
|
||||
print(f"Warning: Style '{fig_opts['style']}' not found. Using default.")
|
||||
|
||||
plt.rcParams["figure.figsize"] = fig_opts['figsize']
|
||||
plt.rcParams["figure.dpi"] = fig_opts['dpi']
|
||||
|
||||
for var in plot_vars:
|
||||
if fig_opts['merge_runs']:
|
||||
fig, (ax_main, ax_diff) = plt.subplots(2, 1, sharex=True, gridspec_kw={'height_ratios': [3, 1]})
|
||||
ax_main.set_title(f"Comparison of {var.value.upper()} (Merged Runs)")
|
||||
_setup_axes(ax_main, ax_diff, var, fig_opts)
|
||||
|
||||
for run_name in runs:
|
||||
_plot_single_run(ax_main, ax_diff, run_name, var, base_dir, compare_pynuc)
|
||||
|
||||
_finalize_plot(fig, ax_main, ax_diff, format_opt, f"ValidationPlot_Merged_{var.value}")
|
||||
|
||||
else:
|
||||
num_runs = len(runs)
|
||||
cols = math.ceil(math.sqrt(num_runs))
|
||||
rows = math.ceil(num_runs / cols)
|
||||
|
||||
base_w, base_h = fig_opts['figsize']
|
||||
fig = plt.figure(figsize=(base_w * cols, base_h * rows), layout='constrained')
|
||||
fig.suptitle(f"{var.value.upper()} Comparison", fontsize=16, fontweight='bold')
|
||||
|
||||
subfigs_raw = fig.subfigures(rows, cols)
|
||||
if hasattr(subfigs_raw, 'flatten'):
|
||||
subfigs = subfigs_raw.flatten()
|
||||
else:
|
||||
subfigs = [subfigs_raw]
|
||||
|
||||
for i, run_name in enumerate(runs):
|
||||
subfig = subfigs[i]
|
||||
subfig.suptitle(f"{run_name}", fontsize=12)
|
||||
|
||||
axes = subfig.subplots(2, 1, sharex=True, gridspec_kw={'height_ratios': [3, 1]})
|
||||
ax_main, ax_diff = axes[0], axes[1]
|
||||
|
||||
_setup_axes(ax_main, ax_diff, var, fig_opts)
|
||||
_plot_single_run(ax_main, ax_diff, run_name, var, base_dir, compare_pynuc)
|
||||
|
||||
_finalize_plot(fig, ax_main, ax_diff, format_opt, "", is_subfigure=True)
|
||||
|
||||
for j in range(num_runs, len(subfigs)):
|
||||
subfigs[j].set_visible(False)
|
||||
|
||||
if format_opt != OutputFormat.INTERACTIVE:
|
||||
out_name = f"ValidationPlot_Grid_{var.value}.{format_opt.value}"
|
||||
fig.savefig(out_name, format=format_opt.value, bbox_inches='tight')
|
||||
print(f"Saved grid figure: {out_name}")
|
||||
plt.close(fig)
|
||||
|
||||
if format_opt == OutputFormat.INTERACTIVE:
|
||||
plt.show()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="GridFire Validation Suite Output Parser and Plotter")
|
||||
|
||||
parser.add_argument("-d", "--data-dir", type=str, default="GF_Validation_Output",
|
||||
help="Path to the directory containing the JSON output folders.")
|
||||
parser.add_argument("--runs", nargs="+", type=str, required=False,
|
||||
help="Which validation runs to analyze. Use 'all' to process all available runs.")
|
||||
parser.add_argument("--plot", nargs="*", type=lambda x: PlotVariable[x.upper()], choices=list(PlotVariable), default=[],
|
||||
help="Variables to plot. Leave empty to skip plotting.")
|
||||
parser.add_argument("--compare-pynucastro", action="store_true",
|
||||
help="Include pynucastro data and calculate log residuals.")
|
||||
parser.add_argument("--merge-runs", action="store_true",
|
||||
help="Merge all specified runs onto a single figure per variable. (Default: Grid layout of subfigures)")
|
||||
parser.add_argument("--x-scale", type=str, choices=["log", "linear"], default="log",
|
||||
help="Scale for the x-axis (time). Default is 'log'.")
|
||||
parser.add_argument("--y-scale", type=str, choices=["log", "linear"], default="log",
|
||||
help="Scale for the y-axis (main plots). Default is 'log'.")
|
||||
parser.add_argument("--format", type=lambda x: OutputFormat[x.upper()], choices=list(OutputFormat), default=OutputFormat.INTERACTIVE,
|
||||
help="Output format for the plots. Default is interactive window.")
|
||||
parser.add_argument("--use-external-style", action="store_true",
|
||||
help="Load the custom style sheet defined in EXTERNAL_STYLE_PATH.")
|
||||
parser.add_argument("--style", type=str, default=None,
|
||||
help="Built-in Matplotlib stylesheet name (e.g., 'seaborn-v0_8-whitegrid'). Ignored if --use-external-style is set.")
|
||||
parser.add_argument("--figsize", nargs=2, type=float, default=[8.0, 6.0],
|
||||
metavar=("WIDTH", "HEIGHT"), help="Base figure size in inches per subfigure (e.g., --figsize 10 8).")
|
||||
parser.add_argument("--dpi", type=int, default=150, help="DPI resolution for saved figures.")
|
||||
parser.add_argument("--list", action="store_true", default=False, help="list available runs")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
available_runs = discover_runs(args.data_dir)
|
||||
if not available_runs:
|
||||
print(f"Error: No successful run data found in {args.data_dir} (Checked GridFire/Ok/).")
|
||||
sys.exit(1)
|
||||
|
||||
if args.list:
|
||||
for run in available_runs:
|
||||
print(f"==> {run}")
|
||||
exit()
|
||||
|
||||
if "all" in [r.lower() for r in args.runs]:
|
||||
target_runs = available_runs
|
||||
else:
|
||||
target_runs = [r for r in args.runs if r in available_runs]
|
||||
missing = [r for r in args.runs if r.lower() != "all" and r not in target_runs]
|
||||
if missing:
|
||||
print(f"Warning: The following runs were skipped because they failed or weren't found: {', '.join(missing)}")
|
||||
|
||||
if args.plot and target_runs:
|
||||
fig_opts = {
|
||||
"figsize": tuple(args.figsize),
|
||||
"dpi": args.dpi,
|
||||
"style": args.style,
|
||||
"use_ext_style": args.use_external_style,
|
||||
"merge_runs": args.merge_runs,
|
||||
"x_scale": args.x_scale,
|
||||
"y_scale": args.y_scale
|
||||
}
|
||||
plot_data(target_runs, args.plot, args.data_dir, args.compare_pynucastro, args.format, fig_opts)
|
||||
elif args.plot:
|
||||
print("Error: No valid runs matched your selection.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
validation/ManuscriptFigures/pynucastro/readme.txt
Normal file
22
validation/ManuscriptFigures/pynucastro/readme.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
# Pynucastro test suite
|
||||
|
||||
Test suite to auto generate pynucastro comparisons to GridFire by copying the GridFire topology to pynucastro.
|
||||
|
||||
Note that this may take a while to run as each pynucastro network must run through JIT compilation. The JIT time is not counted to
|
||||
pynucastro evaluation time
|
||||
|
||||
|
||||
To run use
|
||||
|
||||
```bash
|
||||
python GridFireValidationSuite.py --suite HotStar_No_QSE CoolStar_No_QSE --pynucastro-compare --pync-engine="GraphEngine"
|
||||
```
|
||||
|
||||
to see all options
|
||||
|
||||
|
||||
```bash
|
||||
python GridFireValidationSuite.py --help
|
||||
```
|
||||
|
||||
Results will be saved in a directory as json files which you may parse to analyze
|
||||
299
validation/ManuscriptFigures/pynucastro/testsuite.py
Normal file
299
validation/ManuscriptFigures/pynucastro/testsuite.py
Normal file
@@ -0,0 +1,299 @@
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import fourdst.atomic
|
||||
import scipy.integrate
|
||||
import gridfire
|
||||
from fourdst.composition import Composition
|
||||
from gridfire.engine import DynamicEngine, GraphEngine, AdaptiveEngineView, MultiscalePartitioningEngineView
|
||||
from gridfire.engine import EngineTypes
|
||||
from gridfire.policy import MainSequencePolicy
|
||||
from gridfire.type import NetIn, NetOut
|
||||
from gridfire.exceptions import GridFireError
|
||||
from gridfire.solver import PointSolver, PointSolverContext
|
||||
from logger import StepLogger
|
||||
from typing import List
|
||||
import re
|
||||
from typing import Dict, Tuple, Any, Union
|
||||
from datetime import datetime
|
||||
|
||||
import pynucastro as pyna
|
||||
import os
|
||||
import importlib.util
|
||||
import sys
|
||||
import numpy as np
|
||||
import json
|
||||
import time
|
||||
|
||||
EngineTypeLookup : Dict[EngineTypes, Any] = {
|
||||
EngineTypes.ADAPTIVE_ENGINE_VIEW: AdaptiveEngineView,
|
||||
EngineTypes.MULTISCALE_PARTITIONING_ENGINE_VIEW: MultiscalePartitioningEngineView,
|
||||
EngineTypes.GRAPH_ENGINE: GraphEngine
|
||||
}
|
||||
|
||||
def load_network_module(filepath):
|
||||
module_name = os.path.basename(filepath).replace(".py", "")
|
||||
if module_name in sys.modules: # clear any existing module with the same name
|
||||
del sys.modules[module_name]
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_name, filepath)
|
||||
if spec is None:
|
||||
raise FileNotFoundError(f"Could not find module at {filepath}")
|
||||
|
||||
network_module = importlib.util.module_from_spec(spec)
|
||||
|
||||
sys.modules[module_name] = network_module
|
||||
|
||||
spec.loader.exec_module(network_module)
|
||||
|
||||
return network_module
|
||||
|
||||
def get_pyna_rate(my_rate_str, library):
|
||||
match = re.match(r"([a-zA-Z0-9]+)\(([^,]+),([^)]*)\)(.*)", my_rate_str)
|
||||
|
||||
if not match:
|
||||
print(f"Could not parse string format: {my_rate_str}")
|
||||
return None
|
||||
|
||||
target = match.group(1)
|
||||
projectile = match.group(2)
|
||||
ejectiles = match.group(3)
|
||||
product = match.group(4)
|
||||
|
||||
def expand_species(s_str):
|
||||
if not s_str or s_str.strip() == "":
|
||||
return []
|
||||
|
||||
# Split by space (handling "p a" or "2p a")
|
||||
parts = s_str.split()
|
||||
expanded = []
|
||||
|
||||
for p in parts:
|
||||
# Check for multipliers like 2p, 3a
|
||||
mult_match = re.match(r"(\d+)([a-zA-Z0-9]+)", p)
|
||||
if mult_match:
|
||||
count = int(mult_match.group(1))
|
||||
spec = mult_match.group(2)
|
||||
# Map common aliases if necessary (though pyna handles most)
|
||||
if spec == 'a': spec = 'he4'
|
||||
expanded.extend([spec] * count)
|
||||
else:
|
||||
spec = p
|
||||
if spec == 'a': spec = 'he4'
|
||||
expanded.append(spec)
|
||||
return expanded
|
||||
|
||||
reactants_str = [target] + expand_species(projectile)
|
||||
products_str = expand_species(ejectiles) + [product]
|
||||
|
||||
# Convert strings to pyna.Nucleus objects
|
||||
try:
|
||||
r_nuc = [pyna.Nucleus(r) for r in reactants_str]
|
||||
p_nuc = [pyna.Nucleus(p) for p in products_str]
|
||||
except Exception as e:
|
||||
print(f"Error converting nuclei for {my_rate_str}: {e}")
|
||||
return None
|
||||
|
||||
rates = library.get_rate_by_nuclei(r_nuc, p_nuc)
|
||||
|
||||
if rates:
|
||||
if isinstance(rates, list):
|
||||
return rates[0] # Return the first match
|
||||
return rates
|
||||
else:
|
||||
return None
|
||||
|
||||
class TestSuite(ABC):
|
||||
def __init__(self, name: str, description: str, temp: float, density: float, tMax: float, composition: Composition, notes: str = ""):
|
||||
self.name : str = name
|
||||
self.description : str = description
|
||||
self.temperature : float = temp
|
||||
self.density : float = density
|
||||
self.tMax : float = tMax
|
||||
self.composition : Composition = composition
|
||||
self.notes : str = notes
|
||||
|
||||
def evolve_pynucastro(self, engine: DynamicEngine, ctx: PointSolverContext, output: str = "pynucastro"):
|
||||
print("Evolution complete. Now building equivalent pynucastro network...")
|
||||
# Build equivalent pynucastro network for comparison
|
||||
reaclib_library : pyna.ReacLibLibrary = pyna.ReacLibLibrary()
|
||||
rate_names = [r.id().replace("e+","").replace("e-","").replace(", ", ",") for r in engine.getNetworkReactions(ctx.engine_ctx)]
|
||||
|
||||
with open(f"{self.name}_rate_names_pynuc.txt", "w") as f:
|
||||
for r_name in rate_names:
|
||||
f.write(f"{r_name}\n")
|
||||
|
||||
goodRates : List[pyna.rates.reaclib_rate.ReacLibRate] = []
|
||||
missingRates = []
|
||||
|
||||
for r_str in rate_names:
|
||||
# Try the exact name match first (fastest)
|
||||
try:
|
||||
pyna_rate = reaclib_library.get_rate_by_name(r_str)
|
||||
if isinstance(pyna_rate, list):
|
||||
goodRates.append(pyna_rate[0])
|
||||
else:
|
||||
goodRates.append(pyna_rate)
|
||||
except:
|
||||
# Fallback to the smart parser
|
||||
pyna_rate = get_pyna_rate(r_str, reaclib_library)
|
||||
if pyna_rate:
|
||||
goodRates.append(pyna_rate)
|
||||
else:
|
||||
missingRates.append(r_str)
|
||||
|
||||
|
||||
pynet : pyna.PythonNetwork = pyna.PythonNetwork(rates=goodRates)
|
||||
pynet.write_network(f"{self.name}_pynucastro_network.py")
|
||||
|
||||
net = load_network_module(f"{self.name}_pynucastro_network.py")
|
||||
Y0 = np.zeros(net.nnuc)
|
||||
Y0[net.jp] = self.composition.getMolarAbundance("H-1")
|
||||
Y0[net.jhe3] = self.composition.getMolarAbundance("He-3")
|
||||
Y0[net.jhe4] = self.composition.getMolarAbundance("He-4")
|
||||
Y0[net.jc12] = self.composition.getMolarAbundance("C-12")
|
||||
Y0[net.jn14] = self.composition.getMolarAbundance("N-14")
|
||||
Y0[net.jo16] = self.composition.getMolarAbundance("O-16")
|
||||
Y0[net.jne20] = self.composition.getMolarAbundance("Ne-20")
|
||||
Y0[net.jmg24] = self.composition.getMolarAbundance("Mg-24")
|
||||
|
||||
print("Starting pynucastro integration...")
|
||||
startTime = time.time()
|
||||
sol = scipy.integrate.solve_ivp(
|
||||
net.rhs,
|
||||
[0, self.tMax],
|
||||
Y0,
|
||||
args=(self.density, self.temperature),
|
||||
method="BDF",
|
||||
jac=net.jacobian,
|
||||
rtol=1e-5,
|
||||
atol=1e-8
|
||||
)
|
||||
endTime = time.time()
|
||||
initial_duration = endTime - startTime
|
||||
print("Pynucastro integration complete. Writing results to JSON...")
|
||||
print("Running pynucastro a second time to account for any JIT compilation overhead...")
|
||||
startTime = time.time()
|
||||
sol = scipy.integrate.solve_ivp(
|
||||
net.rhs,
|
||||
[0, self.tMax],
|
||||
Y0,
|
||||
args=(self.density, self.temperature),
|
||||
method="BDF",
|
||||
jac=net.jacobian,
|
||||
rtol=1e-5,
|
||||
atol=1e-8
|
||||
)
|
||||
endTime = time.time()
|
||||
final_duration = endTime - startTime
|
||||
print(f"Pynucastro second integration complete. Initial run time: {initial_duration: .4f} s, Second run time: {final_duration: .4f} s")
|
||||
|
||||
data: List[Dict[str, Union[float, Dict[str, float]]]] = []
|
||||
|
||||
for time_step, t in enumerate(sol.t):
|
||||
data.append({"t": t, "Composition": {}})
|
||||
for j in range(net.nnuc):
|
||||
A = net.A[j]
|
||||
Z = net.Z[j]
|
||||
species: str
|
||||
try:
|
||||
species = fourdst.atomic.az_to_species(A, Z).name()
|
||||
except:
|
||||
species = f"SP-A_{A}_Z_{Z}"
|
||||
|
||||
data[-1]["Composition"][species] = sol.y[j, time_step]
|
||||
|
||||
pynucastro_json : Dict[str, Any] = {
|
||||
"Metadata": {
|
||||
"Name": f"{self.name}_pynucastro",
|
||||
"Description": f"pynucastro simulation equivalent to GridFire validation suite: {self.description}",
|
||||
"Status": "Success",
|
||||
"Notes": self.notes,
|
||||
"Temperature": self.temperature,
|
||||
"Density": self.density,
|
||||
"tMax": self.tMax,
|
||||
"RunTime0": initial_duration,
|
||||
"RunTime1": final_duration,
|
||||
"DateCreated": datetime.now().isoformat(),
|
||||
"NumSpecies": net.nnuc
|
||||
},
|
||||
"Steps": data
|
||||
}
|
||||
|
||||
filename: str = f"{self.name}_pynucastro.json"
|
||||
filepath: str = os.path.join(output, filename)
|
||||
with open(filepath, "w") as f:
|
||||
json.dump(pynucastro_json, f, indent=4)
|
||||
|
||||
def evolve(self, engine: DynamicEngine, solver_ctx: PointSolverContext, netIn: NetIn, pynucastro_compare: bool = True, engine_type: EngineTypes | None = None, output: str = "output"):
|
||||
solver : PointSolver = PointSolver(engine)
|
||||
|
||||
stepLogger : StepLogger = StepLogger()
|
||||
solver_ctx.callback = lambda ctx: stepLogger.log_step(ctx)
|
||||
|
||||
startTime = time.time()
|
||||
subdir: str = os.path.join(output, "GridFire")
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
try:
|
||||
startTime = time.time()
|
||||
netOut : NetOut = solver.evaluate(solver_ctx, netIn)
|
||||
endTime = time.time()
|
||||
filename: str = f"{self.name}_OKAY.json"
|
||||
subdir2: str = os.path.join(subdir, "Ok")
|
||||
os.makedirs(subdir2, exist_ok=True)
|
||||
filepath: str = os.path.join(subdir2, filename)
|
||||
stepLogger.to_json(
|
||||
filepath,
|
||||
Name = f"{self.name}_Success",
|
||||
Description=self.description,
|
||||
Status="Success",
|
||||
Notes=self.notes,
|
||||
Temperature=netIn.temperature,
|
||||
Density=netIn.density,
|
||||
tMax=netIn.tMax,
|
||||
FinalEps = netOut.energy,
|
||||
FinaldEpsdT = netOut.dEps_dT,
|
||||
FinaldEpsdRho = netOut.dEps_dRho,
|
||||
ElapsedTime = endTime - startTime,
|
||||
NumSpecies = engine.getNetworkSpecies(solver_ctx.engine_ctx).__len__(),
|
||||
NumReactions = engine.getNetworkReactions(solver_ctx.engine_ctx).__len__()
|
||||
)
|
||||
except GridFireError as e:
|
||||
endTime = time.time()
|
||||
filename : str = f"{self.name}_FAIL.json"
|
||||
subdir2 : str = os.path.join(subdir, "Err")
|
||||
os.makedirs(subdir2, exist_ok=True)
|
||||
filepath = os.path.join(subdir2, filename)
|
||||
stepLogger.to_json(
|
||||
filepath,
|
||||
Name = f"{self.name}_Failure",
|
||||
Description=self.description,
|
||||
Status=f"Error",
|
||||
ErrorMessage=str(e),
|
||||
Notes=self.notes,
|
||||
Temperature=netIn.temperature,
|
||||
Density=netIn.density,
|
||||
tMax=netIn.tMax,
|
||||
ElapsedTime = endTime - startTime
|
||||
)
|
||||
|
||||
if pynucastro_compare:
|
||||
pynuc_dir = os.path.join(output, "pynucastro")
|
||||
os.makedirs(pynuc_dir, exist_ok=True)
|
||||
if engine_type is not None:
|
||||
if engine_type == EngineTypes.MULTISCALE_PARTITIONING_ENGINE_VIEW:
|
||||
print("Pynucastro comparison using MultiscalePartitioningEngineView...")
|
||||
graphEngine : GraphEngine = GraphEngine(self.composition, depth=3)
|
||||
multiScaleEngine : MultiscalePartitioningEngineView = MultiscalePartitioningEngineView(graphEngine)
|
||||
self.evolve_pynucastro(multiScaleEngine, solver_ctx, pynuc_dir)
|
||||
elif engine_type == EngineTypes.GRAPH_ENGINE:
|
||||
print("Pynucastro comparison using GraphEngine...")
|
||||
graphEngine : GraphEngine = GraphEngine(self.composition, depth=3)
|
||||
self.evolve_pynucastro(graphEngine, solver_ctx, pynuc_dir)
|
||||
else:
|
||||
print(f"Pynucastro comparison not implemented for engine type: {engine_type}")
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def __call__(self, pynucastro_compare: bool = False, pync_engine: str = "GraphEngine", output: str = "output"):
|
||||
pass
|
||||
56
validation/ManuscriptFigures/pynucastro/utils.py
Normal file
56
validation/ManuscriptFigures/pynucastro/utils.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from fourdst.composition import Composition
|
||||
from fourdst.composition import CanonicalComposition
|
||||
from fourdst.atomic import Species
|
||||
from gridfire.type import NetIn
|
||||
|
||||
def rescale_composition(comp_ref : Composition, ZZs : float, Y_primordial : float = 0.248) -> Composition:
|
||||
CC : CanonicalComposition = comp_ref.getCanonicalComposition()
|
||||
|
||||
dY_dZ = (CC.Y - Y_primordial) / CC.Z
|
||||
|
||||
Z_new = CC.Z * (10**ZZs)
|
||||
Y_bulk_new = Y_primordial + (dY_dZ * Z_new)
|
||||
X_new = 1.0 - Z_new - Y_bulk_new
|
||||
|
||||
if X_new < 0: raise ValueError(f"ZZs={ZZs} yields unphysical composition (X < 0)")
|
||||
|
||||
ratio_H = X_new / CC.X if CC.X > 0 else 0
|
||||
ratio_He = Y_bulk_new / CC.Y if CC.Y > 0 else 0
|
||||
ratio_Z = Z_new / CC.Z if CC.Z > 0 else 0
|
||||
|
||||
Y_new_list = []
|
||||
newComp : Composition = Composition()
|
||||
s: Species
|
||||
for s in comp_ref.getRegisteredSpecies():
|
||||
Xi_ref = comp_ref.getMassFraction(s)
|
||||
|
||||
if s.el() == "H":
|
||||
Xi_new = Xi_ref * ratio_H
|
||||
elif s.el() == "He":
|
||||
Xi_new = Xi_ref * ratio_He
|
||||
else:
|
||||
Xi_new = Xi_ref * ratio_Z
|
||||
|
||||
Y = Xi_new / s.mass()
|
||||
newComp.registerSpecies(s)
|
||||
newComp.setMolarAbundance(s, Y)
|
||||
|
||||
return newComp
|
||||
|
||||
def init_composition(ZZs : float = 0) -> Composition:
|
||||
Y_solar = [7.0262E-01, 9.7479E-06, 6.8955E-02, 2.5000E-04, 7.8554E-05, 6.0144E-04, 8.1031E-05, 2.1513E-05]
|
||||
S = ["H-1", "He-3", "He-4", "C-12", "N-14", "O-16", "Ne-20", "Mg-24"]
|
||||
return rescale_composition(Composition(S, Y_solar), ZZs)
|
||||
|
||||
|
||||
def init_netIn(temp: float, rho: float, time: float, comp: Composition) -> NetIn:
|
||||
n : NetIn = NetIn()
|
||||
n.temperature = temp
|
||||
n.density = rho
|
||||
n.tMax = time
|
||||
n.dt0 = 1e-12
|
||||
n.composition = comp
|
||||
return n
|
||||
|
||||
def years_to_seconds(years: float) -> float:
|
||||
return years * 3.1536e7
|
||||
Reference in New Issue
Block a user