mirror of
https://github.com/JHUAPL/kaipy.git
synced 2026-01-08 23:38:11 -05:00
First version of dashboard, and updates to comparison plotting code
This commit is contained in:
@@ -25,18 +25,18 @@ mpiCol = "deepskyblue"
|
||||
|
||||
jMax = 10.0 #Max current for contours
|
||||
|
||||
eMax = 5.0 #Max current for contours
|
||||
eMax = 5.0 #Max current for contours
|
||||
|
||||
#Default pressure colorbar
|
||||
vP = kv.genNorm(vMin=1.0e-2,vMax=10.0,doLog=True)
|
||||
szStrs = ['small','std','big','bigger','fullD','dm']
|
||||
szBds = {}
|
||||
szBds["std"] = [-40.0 ,20.0,2.0]
|
||||
szBds["big"] = [-100.0,20.0,2.0]
|
||||
szBds["std"] = [-40.0 ,20.0,2.0]
|
||||
szBds["big"] = [-100.0,20.0,2.0]
|
||||
szBds["bigger"] = [-200.0,25.0,2.0]
|
||||
szBds["fullD"] = [-300.0,30.0,3.0] # full domain for double res
|
||||
szBds["small"] = [-10.0 , 5.0,2.0]
|
||||
szBds["dm"] = [-30.0 ,10.0,40.0/15.0]
|
||||
szBds["fullD"] = [-300.0,30.0,3.0] # full domain for double res
|
||||
szBds["small"] = [-10.0 , 5.0,2.0]
|
||||
szBds["dm"] = [-30.0 ,10.0,40.0/15.0]
|
||||
|
||||
#Add different size options to argument
|
||||
def AddSizeArgs(parser):
|
||||
@@ -74,10 +74,10 @@ def GetSizeBds(args):
|
||||
|
||||
return xyBds
|
||||
|
||||
#Plot absolute error in the requested, or given, equatorial field
|
||||
def PlotEqErrAbs(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=True, doDeco=True, vMin=1e-9, vMax=1e-4, doLog=True, doVerb=True):
|
||||
#Plot absolute error in the requested, or given, equatorial or meridional field
|
||||
def PlotErrAbs(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=True, doDeco=True, vMin=1e-9, vMax=1e-4, doLog=True, doVerb=True, doEq=True):
|
||||
"""
|
||||
PlotEqErrAbs function plots the absolute error between two gsph objects.
|
||||
PlotErrAbs function plots the absolute error between two gsph objects.
|
||||
|
||||
Args:
|
||||
gsphP (gsph): The gsph object representing the predicted values.
|
||||
@@ -93,6 +93,7 @@ def PlotEqErrAbs(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=T
|
||||
vMax (float, optional): The maximum value for the colorbar. (default: 1e-4)
|
||||
doLog (bool, optional): Whether to use logarithmic scale for the colorbar. (default: True)
|
||||
doVerb (bool, optional): Whether to print verbose output. (default: True)
|
||||
doEq (bool, optional): Whether to plot equatorial or meridional (default: True)
|
||||
|
||||
Returns:
|
||||
dataAbs (ndarray): The absolute error data.
|
||||
@@ -118,8 +119,8 @@ def PlotEqErrAbs(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=T
|
||||
Ax.clear()
|
||||
dataAbs = None
|
||||
for fn in fieldNames:
|
||||
dataP = gsphP.EggSlice(fn, nStp, doEq=True, doVerb=doVerb)
|
||||
dataO = gsphO.EggSlice(fn, nStp, doEq=True, doVerb=doVerb)
|
||||
dataP = gsphP.EggSlice(fn, nStp, doEq=doEq, doVerb=doVerb)
|
||||
dataO = gsphO.EggSlice(fn, nStp, doEq=doEq, doVerb=doVerb)
|
||||
if dataAbs is None:
|
||||
dataAbs = np.square(dataO - dataP)
|
||||
else:
|
||||
@@ -132,13 +133,16 @@ def PlotEqErrAbs(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=T
|
||||
if doDeco:
|
||||
kv.addEarth2D(ax=Ax)
|
||||
Ax.set_xlabel('SM-X [Re]')
|
||||
Ax.set_ylabel('SM-Y [Re]')
|
||||
if doEq:
|
||||
Ax.set_ylabel('SM-Y [Re]')
|
||||
else:
|
||||
Ax.set_ylabel('SM-Z [Re]')
|
||||
return dataAbs
|
||||
|
||||
#Plot relative error in the requested, or given, equatorial field
|
||||
def PlotEqErrRel(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=True, doDeco=True, vMin=1e-16, vMax=1, doLog=True, doVerb=True):
|
||||
#Plot relative error in the requested, or given, equatorial or meridional field
|
||||
def PlotErrRel(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=True, doDeco=True, vMin=1e-16, vMax=1, doLog=True, doVerb=True, doEq=True):
|
||||
"""
|
||||
PlotEqErrRel function plots the relative error between two gsph objects.
|
||||
PlotErrRel function plots the relative error between two gsph objects.
|
||||
|
||||
Args:
|
||||
gsphP (gsph): The gsph object representing the predicted values.
|
||||
@@ -154,6 +158,7 @@ def PlotEqErrRel(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=T
|
||||
vMax (float, optional): The maximum value for the colorbar. (default: 1)
|
||||
doLog (bool, optional): Whether to use logarithmic scale for the colorbar. (default: True)
|
||||
doVerb (bool, optional): Whether to print verbose output. (default: True)
|
||||
doEq (bool, optional): Whether to plot equatorial or meridional (default: True)
|
||||
|
||||
Returns:
|
||||
dataRel (ndarray): The relative error data.
|
||||
@@ -180,8 +185,8 @@ def PlotEqErrRel(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=T
|
||||
dataAbs = None
|
||||
dataBase = None
|
||||
for fn in fieldNames:
|
||||
dataP = gsphP.EggSlice(fn, nStp, doEq=True, doVerb=doVerb)
|
||||
dataO = gsphO.EggSlice(fn, nStp, doEq=True, doVerb=doVerb)
|
||||
dataP = gsphP.EggSlice(fn, nStp, doEq=doEq, doVerb=doVerb)
|
||||
dataO = gsphO.EggSlice(fn, nStp, doEq=doEq, doVerb=doVerb)
|
||||
if dataAbs is None:
|
||||
dataBase = np.square(dataP)
|
||||
dataAbs = np.square(dataO - dataP)
|
||||
@@ -200,7 +205,10 @@ def PlotEqErrRel(gsphP, gsphO, nStp, xyBds, Ax, fieldNames, AxCB=None, doClear=T
|
||||
if doDeco:
|
||||
kv.addEarth2D(ax=Ax)
|
||||
Ax.set_xlabel('SM-X [Re]')
|
||||
Ax.set_ylabel('SM-Y [Re]')
|
||||
if doEq:
|
||||
Ax.set_ylabel('SM-Y [Re]')
|
||||
else:
|
||||
Ax.set_ylabel('SM-Z [Re]')
|
||||
return dataRel
|
||||
|
||||
#Plot absolute error along the requested logical axis
|
||||
|
||||
@@ -43,14 +43,14 @@ def makeMovie(frame_dir,movie_name):
|
||||
return
|
||||
|
||||
cmd = [
|
||||
ffmpegExe, "-nostdin", "-i", frame_pattern,
|
||||
"-vcodec", "libx264", "-crf", "14", "-profile:v", "high", "-pix_fmt", "yuv420p",
|
||||
movie_file,"-y"
|
||||
ffmpegExe, "-nostdin", "-i", frame_pattern,
|
||||
"-vcodec", "libx264", "-crf", "14", "-profile:v", "high", "-pix_fmt", "yuv420p",
|
||||
movie_file,"-y"
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
# python allows changes by reference to the errTimes,errListRel, errListAbs lists
|
||||
def makeImage(i,gsph1,gsph2,tOut,doVerb,xyBds,fnList,oDir,errTimes,errListRel,errListAbs,cv,dataCounter, vO, figSz, noMPI, noLog, fieldNames):
|
||||
def makeImage(i,gsph1,gsph2,tOut,doVerb,doEq,logAx,xyBds,fnList,oDir,errTimes,errListRel,errListAbs,cv,dataCounter, vO, figSz, noMPI, noLog, fieldNames):
|
||||
if doVerb:
|
||||
print("Making image %d"%(i))
|
||||
#Convert time (in seconds) to Step #
|
||||
@@ -77,23 +77,45 @@ def makeImage(i,gsph1,gsph2,tOut,doVerb,xyBds,fnList,oDir,errTimes,errListRel,er
|
||||
AxB2.clear()
|
||||
|
||||
#plot upper left msph error
|
||||
mviz.PlotEqErrRel(gsph1,gsph2,nStp,xyBds,AxTL,fnList,AxCB=AxCT,doVerb=doVerb)
|
||||
AxTL.set_title("Equatorial Slice of Relative Error")
|
||||
mviz.PlotErrRel(gsph1,gsph2,nStp,xyBds,AxTL,fnList,AxCB=AxCT,doVerb=doVerb,doEq=doEq)
|
||||
if doEq:
|
||||
AxTL.set_title("Equatorial Slice of Relative Error")
|
||||
else:
|
||||
AxTL.set_title("Meridional Slice of Relative Error")
|
||||
|
||||
#plot upper right k-axis error
|
||||
mviz.PlotLogicalErrRel(gsph1,gsph2,nStp,AxTR,fnList,2,doVerb=doVerb)
|
||||
AxTR.set_title("Per-Cell Relative Error along K-Axis")
|
||||
#plot upper right cumulative logical error
|
||||
mviz.PlotLogicalErrRel(gsph1,gsph2,nStp,AxTR,fnList,logAx,doVerb=doVerb)
|
||||
if logAx == 0:
|
||||
AxTR.set_title("Per-Cell Relative Error along I-Axis")
|
||||
elif logAx == 1:
|
||||
AxTR.set_title("Per-Cell Relative Error along J-Axis")
|
||||
else:
|
||||
AxTR.set_title("Per-Cell Relative Error along K-Axis")
|
||||
if (not noMPI):
|
||||
#plot I-MPI decomp on logical plot
|
||||
if(gsph2.Ri > 1):
|
||||
if(gsph2.Ri > 1 and logAx != 0):
|
||||
for im in range(gsph2.Ri):
|
||||
i0 = im*gsph2.dNi
|
||||
AxTR.plot([i0, i0],[0, gsph2.Nj],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
if logAx == 1:
|
||||
AxTR.plot([i0, i0],[0, gsph2.Nk],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
else:
|
||||
AxTR.plot([i0, i0],[0, gsph2.Nj],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
#plot J-MPI decomp on logical plot
|
||||
if (gsph2.Rj>1):
|
||||
if (gsph2.Rj>1 and logAx != 1):
|
||||
for jm in range(1,gsph2.Rj):
|
||||
j0 = jm*gsph2.dNj
|
||||
AxTR.plot([0, gsph2.Ni],[j0, j0],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
if logAx == 0:
|
||||
AxTR.plot([j0, j0],[0, gsph2.Nk],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
else:
|
||||
AxTR.plot([0, gsph2.Ni],[j0, j0],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
#plot K-MPI decomp on logical plot
|
||||
if (gsph2.Rk>1 and logAx != 2):
|
||||
for km in range(1,gsph2.Rk):
|
||||
k0 = km*gsph2.dNk
|
||||
if logAx == 0:
|
||||
AxTR.plot([0, gsph2.Nj],[k0, k0],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
else:
|
||||
AxTR.plot([0, gsph2.Ni],[k0, k0],"deepskyblue",linewidth=0.25,alpha=0.5)
|
||||
|
||||
#plot bottom line plot
|
||||
etval = tOut[i]/60.0
|
||||
@@ -168,15 +190,17 @@ def create_command_line_parser():
|
||||
fdir2 = os.getcwd()
|
||||
ftag2 = "msphere"
|
||||
oDir = "vid2D"
|
||||
ts = 0 #[min]
|
||||
te = 200 #[min]
|
||||
ts = 0.0 #[min]
|
||||
te = 200.0 #[min]
|
||||
dt = 0.0 #[sec] 0 default means every timestep
|
||||
logAx = 2 # axis to accumulate logical error along
|
||||
Nth = 1 #Number of threads
|
||||
noMPI = False # Don't add MPI tiling
|
||||
noLog = False
|
||||
fieldNames = "Bx, By, Bz"
|
||||
doVerb = False
|
||||
skipMovie = False
|
||||
merid = False
|
||||
|
||||
MainS = """Creates simple multi-panel figure for Gamera magnetosphere run
|
||||
Left Panel - Residual vertical magnetic field
|
||||
@@ -189,14 +213,16 @@ def create_command_line_parser():
|
||||
parser.add_argument('-d2',type=str,metavar="directory",default=fdir2,help="Directory to read second dataset from (default: %(default)s)")
|
||||
parser.add_argument('-id2',type=str,metavar="runid",default=ftag2,help="RunID of second dataset (default: %(default)s)")
|
||||
parser.add_argument('-o',type=str,metavar="directory",default=oDir,help="Subdirectory to write to (default: %(default)s)")
|
||||
parser.add_argument('-ts' ,type=int,metavar="tStart",default=ts,help="Starting time [min] (default: %(default)s)")
|
||||
parser.add_argument('-te' ,type=int,metavar="tEnd" ,default=te,help="Ending time [min] (default: %(default)s)")
|
||||
parser.add_argument('-dt' ,type=int,metavar="dt" ,default=dt,help="Cadence [sec] (default: %(default)s)")
|
||||
parser.add_argument('-ts' ,type=float,metavar="tStart",default=ts,help="Starting time [min] (default: %(default)s)")
|
||||
parser.add_argument('-te' ,type=float,metavar="tEnd" ,default=te,help="Ending time [min] (default: %(default)s)")
|
||||
parser.add_argument('-dt' ,type=float,metavar="dt" ,default=dt,help="Cadence [sec] (default: %(default)s)")
|
||||
parser.add_argument('-logAx',type=int,metavar="logAx",default=logAx,help="Index of the axis to accumulate along in the upper-right plot (default: %(default)s)")
|
||||
parser.add_argument('-Nth' ,type=int,metavar="Nth",default=Nth,help="Number of threads to use (default: %(default)s)")
|
||||
parser.add_argument('-f',type=str,metavar="fieldnames",default=fieldNames,help="Comma-separated fields to plot (default: %(default)s)")
|
||||
parser.add_argument('-linear',action='store_true', default=noLog,help="Plot linear line plot instead of logarithmic (default: %(default)s)")
|
||||
parser.add_argument('-v',action='store_true', default=doVerb,help="Do verbose output (default: %(default)s)")
|
||||
parser.add_argument('-skipMovie',action='store_true', default=skipMovie,help="Skip automatic movie generation afterwards (default: %(default)s)")
|
||||
parser.add_argument('-merid',action='store_true', default=merid,help="Plot meridional instead of equatorial slice (default: %(default)s)")
|
||||
#parser.add_argument('-nompi', action='store_true', default=noMPI,help="Don't show MPI boundaries (default: %(default)s)")
|
||||
|
||||
|
||||
@@ -209,15 +235,17 @@ def main():
|
||||
fdir2 = os.getcwd()
|
||||
ftag2 = "msphere"
|
||||
oDir = "vid2D"
|
||||
ts = 0 #[min]
|
||||
te = 200 #[min]
|
||||
ts = 0.0 #[min]
|
||||
te = 200.0 #[min]
|
||||
dt = 0.0 #[sec] 0 default means every timestep
|
||||
logAx = 2
|
||||
Nth = 1 #Number of threads
|
||||
noMPI = False # Don't add MPI tiling
|
||||
noLog = False
|
||||
fieldNames = "Bx, By, Bz"
|
||||
doVerb = False
|
||||
skipMovie = False
|
||||
doEq = True
|
||||
|
||||
parser = create_command_line_parser()
|
||||
mviz.AddSizeArgs(parser)
|
||||
@@ -228,14 +256,16 @@ def main():
|
||||
ftag1 = args.id1
|
||||
fdir2 = args.d2
|
||||
ftag2 = args.id2
|
||||
ts = args.ts
|
||||
te = args.te
|
||||
dt = args.dt
|
||||
ts = args.ts
|
||||
te = args.te
|
||||
dt = args.dt
|
||||
logAx = args.logAx
|
||||
oSub = args.o
|
||||
Nth = args.Nth
|
||||
fieldNames = args.f
|
||||
noLog = args.linear
|
||||
doVerb = args.v
|
||||
doEq = not args.merid
|
||||
#noMPI = args.noMPI
|
||||
|
||||
fnList = [item.strip() for item in fieldNames.split(',')]
|
||||
@@ -275,8 +305,8 @@ def main():
|
||||
Nt = len(tOut)
|
||||
vO = np.arange(0,Nt)
|
||||
|
||||
print("Writing %d outputs between minutes %d and %d"%(Nt,ts,te))
|
||||
print("Using %d threads"%(Nth))
|
||||
print(f"Writing {Nt} outputs between minutes {ts} and {te}")
|
||||
print(f"Using {Nth} threads")
|
||||
|
||||
errTimes = []
|
||||
errListRel = []
|
||||
@@ -293,8 +323,8 @@ def main():
|
||||
met = m.list(errTimes)
|
||||
melr = m.list(errListRel)
|
||||
mela = m.list(errListAbs)
|
||||
#imageFutures = {executor.submit(makeImage,i,gsph1,gsph2,tOut,doVerb,xyBds,fnList,oDir,errTimes,errListRel,errListAbs,cv): i for i in range(0,Nt)}
|
||||
imageFutures = {executor.submit(makeImage,i,gsph1,gsph2,tOut,doVerb,xyBds,fnList,oDir,met,melr,mela,cv,dataCounter,vO,figSz,noMPI,noLog,fieldNames): i for i in range(0,Nt)}
|
||||
#imageFutures = {executor.submit(makeImage,i,gsph1,gsph2,tOut,doVerb,doEq,logAx,xyBds,fnList,oDir,errTimes,errListRel,errListAbs,cv): i for i in range(0,Nt)}
|
||||
imageFutures = {executor.submit(makeImage,i,gsph1,gsph2,tOut,doVerb,doEq,logAx,xyBds,fnList,oDir,met,melr,mela,cv,dataCounter,vO,figSz,noMPI,noLog,fieldNames): i for i in range(0,Nt)}
|
||||
for future in concurrent.futures.as_completed(imageFutures):
|
||||
try:
|
||||
retVal = future.result()
|
||||
@@ -304,9 +334,9 @@ def main():
|
||||
traceback.print_exc()
|
||||
exit()
|
||||
bar()
|
||||
|
||||
|
||||
makeMovie(oDir,oSub)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -102,8 +102,8 @@ def main():
|
||||
AxL.clear()
|
||||
AxR.clear()
|
||||
|
||||
mviz.PlotEqErrRel(gsph1,gsph2,nStp,xyBds,AxL,fnList,AxCB=AxCL)
|
||||
mviz.PlotEqErrAbs(gsph1,gsph2,nStp,xyBds,AxR,fnList,AxCB=AxCR)
|
||||
mviz.PlotErrRel(gsph1,gsph2,nStp,xyBds,AxL,fnList,AxCB=AxCL)
|
||||
mviz.PlotErrAbs(gsph1,gsph2,nStp,xyBds,AxR,fnList,AxCB=AxCR)
|
||||
|
||||
gsph1.AddTime(nStp,AxL,xy=[0.025,0.89],fs="x-large")
|
||||
|
||||
@@ -116,4 +116,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
836
kaipy/scripts/quicklook/mageDash.py
Executable file
836
kaipy/scripts/quicklook/mageDash.py
Executable file
@@ -0,0 +1,836 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
"""Display a live dashboard with information about an ongoing or completed MAGE run.
|
||||
|
||||
Display a live dashboard with information about an ongoing or completed MAGE run.
|
||||
|
||||
Author
|
||||
------
|
||||
Jeff Garretson
|
||||
"""
|
||||
|
||||
|
||||
# Import standard modules.
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import sys
|
||||
import signal
|
||||
import logging
|
||||
import io
|
||||
import base64
|
||||
import uuid
|
||||
import datetime
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Import supplemental modules.
|
||||
import h5py
|
||||
import numpy as np
|
||||
from flask_caching import Cache
|
||||
from dash import Dash, dcc, html, Input, Output, State
|
||||
from dash.exceptions import PreventUpdate
|
||||
import dash_bootstrap_components as dbc
|
||||
import plotly.graph_objs as go
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QUrl
|
||||
import matplotlib
|
||||
matplotlib.use("Agg") # safe for servers/headless
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.gridspec as gridspec
|
||||
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
|
||||
from matplotlib import dates
|
||||
import cmasher as cmr
|
||||
from astropy.time import Time
|
||||
|
||||
# Kaipy modules
|
||||
import kaipy.gamera.magsphere as msph
|
||||
import kaipy.gamera.msphViz as mviz
|
||||
import kaipy.raiju.raijuUtils as ru
|
||||
import kaipy.raiju.raijuViz as rv
|
||||
import kaipy.remix.remix as remix
|
||||
import kaipy.kaiViz as kv
|
||||
import kaipy.kaiTools as kt
|
||||
import kaipy.kaiH5 as kaiH5
|
||||
|
||||
# lock because matplotlib isn't thread safe
|
||||
matplotlib_lock = threading.Lock()
|
||||
|
||||
# parser options
|
||||
caseXml = ""
|
||||
firstStep = 1
|
||||
|
||||
# Dash GUI info below
|
||||
app = Dash(
|
||||
__name__,
|
||||
meta_tags=[
|
||||
{"name": "viewport", "content": "width=device-width, initial-scale=1.0"}
|
||||
],
|
||||
external_stylesheets=[dbc.themes.BOOTSTRAP]
|
||||
)
|
||||
|
||||
# Cache for large data
|
||||
cache = Cache(app.server, config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 0})
|
||||
|
||||
stackPlot = html.Div(
|
||||
id="stackPlot",
|
||||
children=[
|
||||
dcc.Graph(id="perf-fig")
|
||||
],
|
||||
)
|
||||
|
||||
Dst_Plot = html.Div(
|
||||
id = "dst-panel",
|
||||
children=[
|
||||
html.Img(id="dst-image", style={"width": "95%", "height": "auto"})
|
||||
]
|
||||
)
|
||||
|
||||
Overview_Layout = html.Div(
|
||||
id = "overview-panel",
|
||||
children=[
|
||||
html.H5(id="overview-xml",children=""),
|
||||
html.H5(id="overview-runid",children=""),
|
||||
html.H5(id="overview-githash",children=""),
|
||||
html.H5(id="overview-gitbranch",children=""),
|
||||
html.H5(id="overview-compiler",children=""),
|
||||
html.H5(id="overview-compileropts",children="Compiler Options:"),
|
||||
dcc.Textarea(id="textarea-compileropts",disabled=True,style={'width': '100%', 'height': 75}),
|
||||
html.H5(id="overview-datetime",children=""),
|
||||
Dst_Plot
|
||||
]
|
||||
)
|
||||
|
||||
Performance_Layout = html.Div(
|
||||
id = "performance-panel",
|
||||
children=[
|
||||
dbc.Container([
|
||||
dbc.Row([
|
||||
dbc.Col([
|
||||
html.H3(children="Choose Plot Type"),
|
||||
dcc.Dropdown(['Overall','Voltron','Gamera-Comp','Gamera-Ave','Gamera-Ind','Raiju'],
|
||||
'Overall', id='performance-dropdown',clearable=False,style={'width': '200px'})
|
||||
], width=6),
|
||||
dbc.Col([
|
||||
html.H3(children="Gamera I-Rank"),
|
||||
dcc.Dropdown([1],value=1,id='performance-idrop',disabled=True,clearable=False,style={'width': '150px'})
|
||||
],width=3),
|
||||
dbc.Col([
|
||||
html.H3(children="Gamera J-Rank"),
|
||||
dcc.Dropdown([1],value=1,id='performance-jdrop',disabled=True,clearable=False,style={'width': '150px'})
|
||||
],width=3)
|
||||
])
|
||||
]),
|
||||
stackPlot
|
||||
]
|
||||
)
|
||||
|
||||
Msph_Plot = html.Div(
|
||||
id = "msph-panel",
|
||||
children=[
|
||||
html.Img(id="msph-image", style={"width": "95%", "height": "auto"})
|
||||
]
|
||||
)
|
||||
|
||||
Gamera_Layout = html.Div(
|
||||
id = "gamera-panel",
|
||||
children=[
|
||||
Msph_Plot
|
||||
]
|
||||
)
|
||||
|
||||
Raiju_Plot = html.Div(
|
||||
id = "raijuplot-panel",
|
||||
children=[
|
||||
html.Img(id="raiju-image", style={"width": "95%", "height": "auto"})
|
||||
]
|
||||
)
|
||||
|
||||
Raiju_Layout = html.Div(
|
||||
id = "raiju-panel",
|
||||
children=[
|
||||
Raiju_Plot
|
||||
]
|
||||
)
|
||||
|
||||
Remix_Plot = html.Div(
|
||||
id = "remixplot-panel",
|
||||
children=[
|
||||
html.Img(id="remix-image", style={"width": "95%", "height": "auto"})
|
||||
]
|
||||
)
|
||||
|
||||
Remix_Layout = html.Div(
|
||||
id = "remix-panel",
|
||||
children=[
|
||||
Remix_Plot
|
||||
]
|
||||
)
|
||||
|
||||
tab_layout = html.Div(
|
||||
id = "tab-panel",
|
||||
children=[
|
||||
dcc.Tabs([dcc.Tab(label='Overview',
|
||||
children=[Overview_Layout]),
|
||||
dcc.Tab(label='Performance',
|
||||
children=[Performance_Layout]),
|
||||
dcc.Tab(label='Gamera',
|
||||
children=[Gamera_Layout]),
|
||||
dcc.Tab(label='Raiju',
|
||||
children=[Raiju_Layout]),
|
||||
dcc.Tab(label='Remix',
|
||||
children=[Remix_Layout]),
|
||||
])
|
||||
]
|
||||
)
|
||||
|
||||
top_layout = html.Div(
|
||||
id = "top-panel",
|
||||
children=[
|
||||
dcc.Slider(0, 120, 60, value=60, id="timeSlider"),
|
||||
html.Button("Refresh Data", id="refresh-button", n_clicks=0)
|
||||
],
|
||||
)
|
||||
|
||||
main_panel_layout = html.Div(
|
||||
id="main-panel",
|
||||
children=[
|
||||
top_layout,
|
||||
tab_layout
|
||||
],
|
||||
)
|
||||
|
||||
root_layout = html.Div(
|
||||
id="root",
|
||||
children=[
|
||||
dcc.Store(id="case-store"),
|
||||
dcc.Store(id="store-data"),
|
||||
main_panel_layout
|
||||
],
|
||||
)
|
||||
|
||||
app.layout = root_layout
|
||||
|
||||
|
||||
# non-dash helper routines here
|
||||
|
||||
def getStepFromTime(data, sliderValue):
|
||||
dataStep = np.argmin(np.abs(np.array(data['Vtime'])-sliderValue))
|
||||
return dataStep + firstStep
|
||||
|
||||
def fig_to_data_uri(fig, *, dpi=150, facecolor="white", tight=False):
|
||||
buf = io.BytesIO()
|
||||
canvas = FigureCanvas(fig)
|
||||
if tight:
|
||||
fig.tight_layout()
|
||||
fig.savefig(buf, format="png", dpi=dpi, facecolor=facecolor, bbox_inches="tight" if tight else None)
|
||||
plt.close(fig) # important to free memory when rendering in callbacks
|
||||
buf.seek(0)
|
||||
encoded = base64.b64encode(buf.read()).decode("ascii")
|
||||
return f"data:image/png;base64,{encoded}"
|
||||
|
||||
def get_free_port():
|
||||
s = socket.socket()
|
||||
s.bind(('', 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
||||
|
||||
class Communicator(QObject):
|
||||
dash_failed = pyqtSignal()
|
||||
|
||||
class DashViewer(QMainWindow):
|
||||
def __init__(self, url, shutdown_callback):
|
||||
super().__init__()
|
||||
self.setWindowTitle("MAGE Dashboard")
|
||||
self.setGeometry(100, 100, 1000, 800)
|
||||
|
||||
self.shutdown_callback = shutdown_callback
|
||||
|
||||
self.browser = QWebEngineView()
|
||||
self.browser.load(QUrl(url))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.browser)
|
||||
|
||||
container = QWidget()
|
||||
container.setLayout(layout)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.shutdown_callback()
|
||||
super().closeEvent(event)
|
||||
|
||||
def readCaseData(caseInfo):
|
||||
data = {}
|
||||
|
||||
with h5py.File(f"{caseInfo['runid']}.volt.h5", 'r') as f:
|
||||
data['Vtime'] = f['/timeAttributeCache/time'][()][firstStep:]
|
||||
if "/timeAttributeCache/_perf_stepTime" in f:
|
||||
data['VstepTime'] = f['/timeAttributeCache/_perf_stepTime'][()][firstStep:]
|
||||
data['VdeepUpdateTime'] = f['/timeAttributeCache/_perf_deepUpdateTime'][()][firstStep:]
|
||||
data['VgamTime'] = f['/timeAttributeCache/_perf_gamTime'][()][firstStep:]
|
||||
data['VsquishTime'] = f['/timeAttributeCache/_perf_squishTime'][()][firstStep:]
|
||||
data['VimagTime'] = f['/timeAttributeCache/_perf_imagTime'][()][firstStep:]
|
||||
data['VmixTime'] = f['/timeAttributeCache/_perf_mixTime'][()][firstStep:]
|
||||
data['VtubesTime'] = f['/timeAttributeCache/_perf_tubesTime'][()][firstStep:]
|
||||
data['VioTime'] = f['/timeAttributeCache/_perf_ioTime'][()][firstStep:]
|
||||
|
||||
for i in range(1,caseInfo['gamInum']+1):
|
||||
for j in range(1,caseInfo['gamJnum']+1):
|
||||
if caseInfo['gamSerial']:
|
||||
gamFilename = f'{caseInfo['runid']}.gam.h5'
|
||||
else:
|
||||
gamFilename = f'{caseInfo['runid']}_{caseInfo['gamInum']:04d}_{caseInfo['gamJnum']:04d}_0001_{i-1:04d}_{j-1:04d}_0000.gam.h5'
|
||||
with h5py.File(gamFilename, 'r') as f:
|
||||
data[f'Gtime{i}{j}'] = f['/timeAttributeCache/time'][()][firstStep:]
|
||||
data[f'Gdt{i}{j}'] = f['/timeAttributeCache/dt'][()][firstStep:]
|
||||
if "/timeAttributeCache/_perf_stepTime" in f:
|
||||
data[f'GstepTime{i}{j}'] = f['/timeAttributeCache/_perf_stepTime'][()][firstStep:]
|
||||
data[f'GvoltTime{i}{j}'] = f['/timeAttributeCache/_perf_voltTime'][()][firstStep:]
|
||||
data[f'GmathTime{i}{j}'] = f['/timeAttributeCache/_perf_mathTime'][()][firstStep:]
|
||||
data[f'GbcTime{i}{j}'] = f['/timeAttributeCache/_perf_bcTime'][()][firstStep:]
|
||||
data[f'GhaloTime{i}{j}'] = f['/timeAttributeCache/_perf_haloTime'][()][firstStep:]
|
||||
data[f'GioTime{i}{j}'] = f['/timeAttributeCache/_perf_ioTime'][()][firstStep:]
|
||||
data[f'GadvanceTime{i}{j}'] = f['/timeAttributeCache/_perf_advanceTime'][()][firstStep:]
|
||||
|
||||
data['gsph'] = str(uuid.uuid4())
|
||||
cache.set(data['gsph'], msph.GamsphPipe('.', caseInfo['runid'], doFast=True))
|
||||
|
||||
with h5py.File(f"{caseInfo['runid']}.raiju.h5", 'r') as f:
|
||||
data['Rtime'] = f['/timeAttributeCache/time'][()][firstStep:]
|
||||
if "/timeAttributeCache/_perf_stepTime" in f:
|
||||
data['RstepTime'] = f['/timeAttributeCache/_perf_stepTime'][()][firstStep:]
|
||||
data['RpreAdvanceTime'] = f['/timeAttributeCache/_perf_preAdvance'][()][firstStep:]
|
||||
data['RadvanceTime'] = f['/timeAttributeCache/_perf_advanceState'][()][firstStep:]
|
||||
data['RmomentsTime'] = f['/timeAttributeCache/_perf_moments'][()][firstStep:]
|
||||
|
||||
data['raiI'] = str(uuid.uuid4())
|
||||
cache.set(data['raiI'], ru.RAIJUInfo(f"{caseInfo['runid']}.raiju.h5",useTAC=True))
|
||||
|
||||
'''
|
||||
Commented out because remix hasn't had performance data added to its output yet
|
||||
with h5py.File(f"{caseInfo['runid']}.mix.h5", 'r') as f:
|
||||
if "/timeAttributeCache/perf_stepTime" not in f:
|
||||
print("*** ERROR *** This run's output files are too old, and don't have the necessary data.")
|
||||
raise PreventUpdate
|
||||
|
||||
data['MstepTime'] = f['/timeAttributeCache/perf_stepTime'][()][firstStep:]
|
||||
data['VgamTime'] = f['/timeAttributeCache/perf_gamTime'][()][firstStep:]
|
||||
data['VsquishTime'] = f['/timeAttributeCache/perf_squishTime'][()][firstStep:]
|
||||
data['VimagTime'] = f['/timeAttributeCache/perf_imagTime'][()][firstStep:]
|
||||
data['VmixTime'] = f['/timeAttributeCache/perf_mixTime'][()][firstStep:]
|
||||
data['VtubesTime'] = f['/timeAttributeCache/perf_tubesTime'][()][firstStep:]
|
||||
data['VioTime'] = f['/timeAttributeCache/perf_ioTime'][()][firstStep:]
|
||||
data['Vtime'] = f['/timeAttributeCache/time'][()][firstStep:]
|
||||
'''
|
||||
|
||||
return data
|
||||
|
||||
def getCaseInsensitiveXmlElement(E, name):
|
||||
if E is None:
|
||||
return None
|
||||
for element in E.iter():
|
||||
if element.tag.casefold() == name.casefold():
|
||||
return element
|
||||
return None
|
||||
|
||||
def getCaseInsensitiveXmlAttribute(E, name):
|
||||
if E is None:
|
||||
return None
|
||||
for attr_name,attr_value in E.attrib.items():
|
||||
if attr_name.casefold() == name.casefold():
|
||||
return attr_value
|
||||
return None
|
||||
|
||||
def getCaseInfo():
|
||||
caseInfo = {}
|
||||
|
||||
if not os.path.exists(caseXml):
|
||||
print(f"*** ERROR *** XML file {caseXml} could not be found.")
|
||||
raise PreventUpdate
|
||||
|
||||
caseInfo['xml'] = caseXml
|
||||
|
||||
tree = ET.parse(caseXml)
|
||||
root = tree.getroot()
|
||||
kaijuE = getCaseInsensitiveXmlElement(root, 'kaiju')
|
||||
gamE = getCaseInsensitiveXmlElement(kaijuE, 'gamera')
|
||||
voltE = getCaseInsensitiveXmlElement(kaijuE, 'voltron')
|
||||
|
||||
caseInfo['gamSerial'] = getCaseInsensitiveXmlAttribute(getCaseInsensitiveXmlElement(voltE,'coupling'),'doserial')
|
||||
if caseInfo['gamSerial'] is None:
|
||||
caseInfo['gamSerial'] = False
|
||||
else:
|
||||
caseInfo['gamSerial'] = bool(caseInfo['gamSerial'])
|
||||
|
||||
caseInfo['dtCouple'] = getCaseInsensitiveXmlAttribute(getCaseInsensitiveXmlElement(voltE,'coupling'),'dtCouple')
|
||||
if caseInfo['dtCouple'] is None:
|
||||
caseInfo['dtCouple'] = 5.0
|
||||
else:
|
||||
caseInfo['dtCouple'] = float(caseInfo['dtCouple'])
|
||||
|
||||
caseInfo['runid'] = getCaseInsensitiveXmlAttribute(getCaseInsensitiveXmlElement(gamE,'sim'),'runid')
|
||||
if caseInfo['runid'] is None:
|
||||
caseInfo['runid'] = 'msphere'
|
||||
|
||||
caseInfo['gamInum'] = getCaseInsensitiveXmlAttribute(getCaseInsensitiveXmlElement(gamE,'iPdir'),'N')
|
||||
if caseInfo['gamInum'] is None:
|
||||
caseInfo['gamInum'] = 1
|
||||
else:
|
||||
caseInfo['gamInum'] = int(caseInfo['gamInum'])
|
||||
|
||||
caseInfo['gamJnum'] = getCaseInsensitiveXmlAttribute(getCaseInsensitiveXmlElement(gamE,'jPdir'),'N')
|
||||
if caseInfo['gamJnum'] is None:
|
||||
caseInfo['gamJnum'] = 1
|
||||
else:
|
||||
caseInfo['gamJnum'] = int(caseInfo['gamJnum'])
|
||||
|
||||
if os.path.exists(f"{caseInfo['runid']}.gamCpl.h5"):
|
||||
caseInfo['ismpi'] = True
|
||||
else:
|
||||
caseInfo['ismpi'] = False
|
||||
|
||||
if not os.path.exists(f"{caseInfo['runid']}.volt.h5"):
|
||||
print(f"*** ERROR *** Case files could not be found using runid {caseInfo['runid']}")
|
||||
raise PreventUpdate
|
||||
|
||||
with h5py.File(f"{caseInfo['runid']}.volt.h5", 'r') as f:
|
||||
caseInfo['githash'] = np.char.decode(f.attrs['GITHASH'], encoding='utf-8')
|
||||
caseInfo['gitbranch'] = np.char.decode(f.attrs['GITBRANCH'], encoding='utf-8')
|
||||
caseInfo['compiler'] = np.char.decode(f.attrs['COMPILER'], encoding='utf-8')
|
||||
caseInfo['compileropts'] = np.char.decode(f.attrs['COMPILEROPTS'], encoding='utf-8')
|
||||
caseInfo['datetime'] = np.char.decode(f.attrs['DATETIME'], encoding='utf-8')
|
||||
|
||||
return caseInfo
|
||||
|
||||
# dash app callbacks here
|
||||
@app.callback(
|
||||
Output("case-store", "data"),
|
||||
Input("case-store", "id") # Ensures this is triggered first on startup, and then never again
|
||||
)
|
||||
def update_caseInfo(id):
|
||||
caseInfo = getCaseInfo()
|
||||
return caseInfo
|
||||
|
||||
@app.callback(
|
||||
Output("overview-xml","children"),
|
||||
Output("overview-runid","children"),
|
||||
Output("overview-githash","children"),
|
||||
Output("overview-gitbranch","children"),
|
||||
Output("overview-compiler","children"),
|
||||
Output("textarea-compileropts","value"),
|
||||
Output("overview-datetime","children"),
|
||||
Input("case-store", "data"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def updateOverviewText(caseInfo):
|
||||
return [
|
||||
f"Case XML: {caseInfo['xml']}",
|
||||
f"Runid: {caseInfo['runid']}",
|
||||
f"Githash: {caseInfo['githash']}",
|
||||
f"Gitbranch: {caseInfo['gitbranch']}",
|
||||
f"Compiler: {caseInfo['compiler']}",
|
||||
caseInfo['compileropts'],
|
||||
f"Sim Started At: {caseInfo['datetime']}"
|
||||
]
|
||||
|
||||
@app.callback(
|
||||
Output("performance-idrop","options"),
|
||||
Output("performance-jdrop","options"),
|
||||
Input("case-store", "data"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def updateRankDropdownOptions(caseInfo):
|
||||
return [list(range(1,caseInfo['gamInum']+1)), list(range(1,caseInfo['gamJnum']+1))]
|
||||
|
||||
@app.callback(
|
||||
Output("store-data", "data"),
|
||||
Input("refresh-button", "n_clicks"),
|
||||
Input("case-store", "data"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def update_data(nClicks, caseInfo):
|
||||
data = readCaseData(caseInfo)
|
||||
return data
|
||||
|
||||
@app.callback(
|
||||
Output("performance-idrop", "disabled"),
|
||||
Output("performance-jdrop", "disabled"),
|
||||
Input("performance-dropdown","value"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def enableRankDropdowns(perfType):
|
||||
if perfType == 'Gamera-Ind':
|
||||
return [False,False]
|
||||
return [True,True]
|
||||
|
||||
@app.callback(
|
||||
Output("perf-fig", "figure"),
|
||||
Input("store-data", "data"),
|
||||
Input("case-store", "data"),
|
||||
Input("performance-dropdown","value"),
|
||||
Input("performance-idrop","value"),
|
||||
Input("performance-jdrop","value"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def update_stackplot(data, caseInfo, perfType, iRank, jRank):
|
||||
fig = go.Figure()
|
||||
|
||||
if 'VstepTime' not in data:
|
||||
fig.add_annotation(
|
||||
text="Performance data not in MAGE output files.<br>Please rerun simulation with newer version.",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font=dict(size=20,color="black")
|
||||
)
|
||||
return fig
|
||||
|
||||
if perfType == 'Voltron':
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=data['VgamTime'], name='Gamera Time',
|
||||
mode='lines', fill='tozeroy', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=data['VsquishTime'], name='Squish Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=data['VimagTime'], name='Imag Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=data['VmixTime'], name='Mix Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=data['VtubesTime'], name='Tubes Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=data['VioTime'], name='I/O Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.update_layout(title="Voltron Performance")
|
||||
fig.update_layout(xaxis_title="Simulation Time", yaxis_title="Time per Update")
|
||||
elif perfType == 'Overall':
|
||||
fig.add_trace(go.Scatter(x=data['Vtime'], y=100*caseInfo['dtCouple']/np.array(data['VdeepUpdateTime']),
|
||||
name='Voltron Performance', mode='lines'))
|
||||
if caseInfo['ismpi'] and not caseInfo['gamSerial']:
|
||||
fig.add_trace(go.Scatter(x=data['Gtime11'], y=100*np.array(data['Gdt11'])/np.array(data['GadvanceTime11']),
|
||||
name='Gamera Performance', mode='lines'))
|
||||
fig.update_layout(title="Overall Performance")
|
||||
fig.update_layout(xaxis_title="Simulation Time", yaxis_title="% of Real-Time")
|
||||
elif perfType == 'Gamera-Comp':
|
||||
for i in range(1,caseInfo['gamInum']+1):
|
||||
for j in range(1,caseInfo['gamJnum']+1):
|
||||
fig.add_trace(go.Scatter(x=data[f'Gtime{i}{j}'],
|
||||
y=100*np.array(data[f'Gdt{i}{j}'])/np.array(data[f'GadvanceTime{i}{j}']),
|
||||
name=f'G-I{i}-J{j}', mode='lines'))
|
||||
fig.update_layout(title="Gamera Performance By Rank")
|
||||
fig.update_layout(xaxis_title="Simulation Time", yaxis_title="% of Real-Time")
|
||||
elif perfType == 'Gamera-Ave':
|
||||
GmathTimeAve = np.array(data['GmathTime11']).copy()
|
||||
GbcTimeAve = np.array(data['GbcTime11']).copy()
|
||||
GhaloTimeAve = np.array(data['GhaloTime11']).copy()
|
||||
GioTimeAve = np.array(data['GioTime11']).copy()
|
||||
for i in range(1,caseInfo['gamInum']+1):
|
||||
for j in range(1,caseInfo['gamJnum']+1):
|
||||
if i > 1 or j > 1:
|
||||
GmathTimeAve = GmathTimeAve + np.array(data[f'GmathTime{i}{j}'])
|
||||
GbcTimeAve = GbcTimeAve + np.array(data[f'GbcTime{i}{j}'])
|
||||
GhaloTimeAve = GhaloTimeAve + np.array(data[f'GhaloTime{i}{j}'])
|
||||
GioTimeAve = GioTimeAve + np.array(data[f'GioTime{i}{j}'])
|
||||
GmathTimeAve = GmathTimeAve / (caseInfo['gamInum']*caseInfo['gamJnum'])
|
||||
GbcTimeAve = GbcTimeAve / (caseInfo['gamInum']*caseInfo['gamJnum'])
|
||||
GhaloTimeAve = GhaloTimeAve / (caseInfo['gamInum']*caseInfo['gamJnum'])
|
||||
GioTimeAve = GioTimeAve / (caseInfo['gamInum']*caseInfo['gamJnum'])
|
||||
fig.add_trace(go.Scatter(x=data['Gtime11'], y=GmathTimeAve, name='Math Time',
|
||||
mode='lines', fill='tozeroy', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Gtime11'], y=GbcTimeAve, name='BC Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Gtime11'], y=GhaloTimeAve, name='Halo Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Gtime11'], y=GioTimeAve, name='IO Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.update_layout(title="Gamera Performance (Averaged Over All Ranks)")
|
||||
fig.update_layout(xaxis_title="Simulation Time", yaxis_title="Time per Update")
|
||||
elif perfType == 'Gamera-Ind':
|
||||
fig.add_trace(go.Scatter(x=data[f'Gtime{iRank}{jRank}'], y=data[f'GmathTime{iRank}{jRank}'],
|
||||
name='Math Time', mode='lines', fill='tozeroy', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data[f'Gtime{iRank}{jRank}'], y=data[f'GbcTime{iRank}{jRank}'],
|
||||
name='BC Time', mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data[f'Gtime{iRank}{jRank}'], y=data[f'GhaloTime{iRank}{jRank}'],
|
||||
name='Halo Time', mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data[f'Gtime{iRank}{jRank}'], y=data[f'GioTime{iRank}{jRank}'],
|
||||
name='IO Time', mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.update_layout(title=f"Gamera Performance-I{iRank}-J{jRank}")
|
||||
fig.update_layout(xaxis_title="Simulation Time", yaxis_title="Time per Update")
|
||||
pass
|
||||
elif perfType == 'Raiju':
|
||||
fig.add_trace(go.Scatter(x=data['Rtime'], y=data['RpreAdvanceTime'], name='Pre-Advance Time',
|
||||
mode='lines', fill='tozeroy', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Rtime'], y=data['RadvanceTime'], name='Advance Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.add_trace(go.Scatter(x=data['Rtime'], y=data['RmomentsTime'], name='Moments Time',
|
||||
mode='lines', fill='tonexty', stackgroup='one'))
|
||||
fig.update_layout(title="Raiju Performance")
|
||||
fig.update_layout(xaxis_title="Simulation Time", yaxis_title="Time per Update")
|
||||
else:
|
||||
fig.add_annotation(
|
||||
text=f"Unrecognized plot type '{perfType}'",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font=dict(size=20,color="black")
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
@app.callback(
|
||||
Output("timeSlider", "min"),
|
||||
Output("timeSlider", "max"),
|
||||
Input("store-data", "data"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def update_slider(data):
|
||||
return data['Vtime'][0], data['Vtime'][-1]
|
||||
|
||||
@app.callback(
|
||||
Output("msph-image", "src"),
|
||||
Input("store-data", "data"),
|
||||
Input("timeSlider", "value"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def updateMsphPlot(data, sliderValue):
|
||||
if data['gsph'] is None:
|
||||
return ''
|
||||
|
||||
with matplotlib_lock:
|
||||
step = getStepFromTime(data, sliderValue)
|
||||
figSz=(12, 7.5)
|
||||
# surprisingly hard to pass the size string
|
||||
sizeArg = lambda: None
|
||||
sizeArg.size = 'std'
|
||||
xyBds=mviz.GetSizeBds(sizeArg)
|
||||
fig = plt.figure(figsize=figSz)
|
||||
gs = gridspec.GridSpec(3, 1, height_ratios=[20, 1, 1], hspace=0.025)
|
||||
Ax = fig.add_subplot(gs[0, 0])
|
||||
Clb = fig.add_subplot(gs[-1, 0])
|
||||
mviz.PlotEqB(cache.get(data['gsph']), step, xyBds, Ax, Clb, doBz=False)
|
||||
return fig_to_data_uri(fig)
|
||||
|
||||
@app.callback(
|
||||
Output("raiju-image", "src"),
|
||||
Input("store-data", "data"),
|
||||
Input("case-store", "data"),
|
||||
Input("timeSlider", "value"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def updateRaijuPlot(data, caseInfo, sliderValue):
|
||||
if data['raiI'] is None:
|
||||
return ''
|
||||
|
||||
with matplotlib_lock:
|
||||
raiI = cache.get(data['raiI'])
|
||||
|
||||
step = getStepFromTime(data, sliderValue)
|
||||
figSz = (13,7)
|
||||
eqBnds = [-15,10,-10,10]
|
||||
fig = plt.figure(figsize=figSz)
|
||||
gs = gridspec.GridSpec(4, 1, height_ratios=[0.1,1,1,0.1],hspace=0.2,wspace=0.18)
|
||||
P_Clb = fig.add_subplot(gs[0, 0])
|
||||
P_Ax = fig.add_subplot(gs[1, 0])
|
||||
E_Ax = fig.add_subplot(gs[2, 0])
|
||||
E_Clb = fig.add_subplot(gs[3, 0])
|
||||
norm_press = kv.genNorm(05e-2,50,doLog=False)
|
||||
cmap_press = cmr.lilac
|
||||
kv.genCB(P_Clb, norm_press, "Proton Pressure [nPa]",cmap_press)
|
||||
P_Clb.xaxis.set_ticks_position("top")
|
||||
P_Clb.xaxis.set_label_position("top")
|
||||
kv.genCB(E_Clb, norm_press, "Electron Pressure [nPa]",cmap_press)
|
||||
|
||||
with h5py.File(f"{caseInfo['runid']}.raiju.h5", 'r') as rFile:
|
||||
s5 = rFile[f"Step#{step}"]
|
||||
spcIdx_p = ru.spcIdx(raiI.species, ru.flavs_s['HOTP'])
|
||||
spcIdx_e = ru.spcIdx(raiI.species, ru.flavs_s['HOTE'])
|
||||
spcIdx_psph = ru.spcIdx(raiI.species, ru.flavs_s['PSPH'])
|
||||
xmin = ru.getVar(s5,'xmin')
|
||||
ymin = ru.getVar(s5,'ymin')
|
||||
topo = ru.getVar(s5,'topo')
|
||||
active = ru.getVar(s5,'active')
|
||||
# plotting only active domain for now
|
||||
#if config['domain'] == "ACTIVE":
|
||||
# mask_cc = active != ru.domain['ACTIVE']
|
||||
#elif config['domain'] == "BUFFER":
|
||||
# mask_cc = active != ru.domain['INACTIVE']
|
||||
mask_cc = active != ru.domain['ACTIVE']
|
||||
mask_corner = topo==ru.topo['OPEN']
|
||||
press_all = ru.getVar(s5,'Pressure',mask=mask_cc,broadcast_dims=(2,))
|
||||
press_p = press_all[:,:,spcIdx_p+1] # First slot is bulk
|
||||
press_e = press_all[:,:,spcIdx_e+1]
|
||||
pot_corot = ru.getVar(s5, 'pot_corot', mask=mask_corner)
|
||||
pot_iono = ru.getVar(s5, 'pot_iono' , mask=mask_corner)
|
||||
pot_total = pot_corot + pot_iono
|
||||
levels_pot = np.arange(-250, 255, 5)
|
||||
|
||||
rv.plotXYMin(P_Ax, xmin, ymin, press_p,norm=norm_press,bnds=eqBnds,cmap=cmap_press)
|
||||
P_Ax.contour(xmin, ymin, pot_total, levels=levels_pot, colors='white',linewidths=0.5, alpha=0.3)
|
||||
rv.plotXYMin(E_Ax, xmin, ymin, press_e,norm=norm_press,bnds=eqBnds,cmap=cmap_press)
|
||||
E_Ax.contour(xmin, ymin, pot_total, levels=levels_pot, colors='white',linewidths=0.5, alpha=0.3)
|
||||
|
||||
return fig_to_data_uri(fig)
|
||||
|
||||
@app.callback(
|
||||
Output("remix-image", "src"),
|
||||
Input("store-data", "data"),
|
||||
Input("case-store", "data"),
|
||||
Input("timeSlider", "value"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def updateRemixPlot(data, caseInfo, sliderValue):
|
||||
with matplotlib_lock:
|
||||
step = getStepFromTime(data, sliderValue)
|
||||
figSz = (12,7.5)
|
||||
fig = plt.figure(figsize=figSz)
|
||||
gs = gridspec.GridSpec(1, 2, figure=fig, left=0.03, right=0.97, top=0.9, bottom=0.03)
|
||||
ion = remix.remix(f"{caseInfo['runid']}.mix.h5", step)
|
||||
ion.init_vars('NORTH')
|
||||
ion.plot('current', gs=gs[0, 0])
|
||||
ion.init_vars('SOUTH')
|
||||
ion.plot('current', gs=gs[0, 1])
|
||||
return fig_to_data_uri(fig)
|
||||
|
||||
@app.callback(
|
||||
Output("dst-image", "src"),
|
||||
Input("store-data", "data"),
|
||||
Input("case-store", "data"),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def updateDstPlot(data, caseInfo):
|
||||
with matplotlib_lock:
|
||||
LW = 0.75
|
||||
tpad = 8 #Number of hours beyond MHD to plot
|
||||
figSz = (14,7)
|
||||
fig = plt.figure(figsize=figSz)
|
||||
gs = gridspec.GridSpec(1,1,hspace=0.05,wspace=0.05)
|
||||
ax=fig.add_subplot(gs[0,0])
|
||||
|
||||
#UT formats for plotting
|
||||
isotfmt = '%Y-%m-%dT%H:%M:%S.%f'
|
||||
utfmt = '%H:%M \n%Y-%m-%d'
|
||||
|
||||
ut_symh,tD,dstD = kt.GetSymH("bcwind.h5")
|
||||
fvolt = f"{caseInfo['runid']}.volt.h5"
|
||||
BSDst = kaiH5.getTs(fvolt,sIds=None,aID="BSDst")
|
||||
MJD = kaiH5.getTs(fvolt,sIds=None,aID="MJD")
|
||||
I = np.isinf(MJD)
|
||||
MJD0 = MJD[~I].min()-1
|
||||
MJD[I] = MJD0
|
||||
UT = Time(MJD,format='mjd').isot
|
||||
ut = [datetime.datetime.strptime(UT[n],isotfmt) for n in range(len(UT))]
|
||||
iMax = len(ut)-1
|
||||
# Remove Restart Step. Tends to cause weird artifacts
|
||||
deldt = []
|
||||
for it in range(iMax,1,-1):
|
||||
dt = ut[it] - ut[it-1]
|
||||
dt = dt.total_seconds()
|
||||
if dt < 2.:
|
||||
deldt.append(it)
|
||||
BSDst = np.delete( BSDst,deldt )
|
||||
ut = np.delete( ut,deldt )
|
||||
|
||||
ax.plot(ut_symh,dstD,label="SYM-H",linewidth=2*LW)
|
||||
ax.plot(ut,BSDst,label="Biot-Savart Dst",linewidth=LW)
|
||||
ax.legend(loc='upper right',fontsize="small",ncol=2)
|
||||
ax.axhline(color='magenta',linewidth=0.5*LW)
|
||||
ax.xaxis_date()
|
||||
xfmt = dates.DateFormatter(utfmt)
|
||||
ax.set_ylabel("Dst [nT]")
|
||||
ax.xaxis.set_major_formatter(xfmt)
|
||||
xMinD = np.array(ut_symh).min()
|
||||
xMaxD = np.array(ut_symh).max()
|
||||
xMinS = np.array(ut).min()
|
||||
xMaxS = np.array(ut).max()
|
||||
if (xMaxD>xMaxS):
|
||||
xMax = min(xMaxS+datetime.timedelta(hours=tpad),xMaxD)
|
||||
else:
|
||||
xMax = xMaxS
|
||||
xMin = xMinD
|
||||
ax.set_xlim(xMin,xMax)
|
||||
return fig_to_data_uri(fig)
|
||||
|
||||
|
||||
# main startup routines
|
||||
def create_command_line_parser():
|
||||
"""Create the command-line argument parser.
|
||||
|
||||
Create the parser for command-line arguments.
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: Command-line argument parser for this script.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Shows a live dashboard for an ongoing, or completed, MAGE case.",
|
||||
formatter_class=argparse.RawTextHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"caseXml", help="Xml file used to run the case being shown."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-firstStep", type=int, default=firstStep, help="First Step# group to read data from in the case"
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def main():
|
||||
global caseXml
|
||||
global firstStep
|
||||
"""Display a live dashboard with information about an ongoing or completed MAGE run."""
|
||||
|
||||
# Set up the command-line parser.
|
||||
parser = create_command_line_parser()
|
||||
|
||||
# Parse the command-line arguments.
|
||||
args = parser.parse_args()
|
||||
caseXml = args.caseXml
|
||||
firstStep = args.firstStep
|
||||
|
||||
port = get_free_port()
|
||||
dash_url = f"http://127.0.0.1:{port}"
|
||||
server_thread = None
|
||||
|
||||
comms = Communicator()
|
||||
|
||||
def shutdown_dash():
|
||||
if server_thread and server_thread.is_alive():
|
||||
print("Shutting down Dash server...")
|
||||
signal.pthread_kill(server_thread.ident, signal.SIGINT)
|
||||
|
||||
# Cleanly exit Qt app if Dash crashes
|
||||
def on_dash_failed():
|
||||
print("Dash failed to start. Closing Qt app.")
|
||||
qt_app.quit()
|
||||
|
||||
comms.dash_failed.connect(on_dash_failed)
|
||||
|
||||
def run_dash():
|
||||
try:
|
||||
app.run(host='127.0.0.1', port=port, debug=False, use_reloader=False)
|
||||
except Exception as e:
|
||||
print(f"Dash server encountered an error: {e}")
|
||||
comms.dash_failed.emit() # Notify GUI thread to shut down
|
||||
|
||||
# reduce dash console output
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
server_thread = threading.Thread(target=run_dash)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
#os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
|
||||
|
||||
qt_app = QApplication(sys.argv)
|
||||
viewer = DashViewer(dash_url, shutdown_dash)
|
||||
viewer.show()
|
||||
sys.exit(qt_app.exec_())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user