New: gui
Mod: global env
This commit is contained in:
parent
59e44dd08d
commit
29a9a7fa7f
@ -6,38 +6,37 @@
|
||||
|
||||
*anisotropy* is a ``Python`` package that is the result of science-research work
|
||||
on the anisotropy of permeability in the periodic porous media.
|
||||
A project uses own wrappers around external applications
|
||||
for constructing a shapes and meshes (``Salome``) and computing a flow (``OpenFOAM``).
|
||||
"""
|
||||
|
||||
__license__ = "GPL3"
|
||||
__version__ = "1.2.0"
|
||||
__author__ = __maintainer__ = "George Kusayko"
|
||||
__email__ = "gkusayko@gmail.com"
|
||||
__repository__ = "https://github.com/L-Nafaryus/anisotropy"
|
||||
|
||||
###
|
||||
# Environment
|
||||
##
|
||||
import os
|
||||
from os import path
|
||||
|
||||
env = dict(
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
)
|
||||
env.update(
|
||||
CLI = os.path.join(env["ROOT"], "anisotropy/core/cli.py")
|
||||
)
|
||||
env.update(
|
||||
BUILD = os.path.join(env["ROOT"], "build"),
|
||||
LOG = os.path.join(env["ROOT"], "logs"),
|
||||
CONFIG = os.path.join(env["ROOT"], "anisotropy/config/default.toml"),
|
||||
DOCS = os.path.join(env["ROOT"], "docs")
|
||||
)
|
||||
env.update(
|
||||
logger_name = "anisotropy",
|
||||
db_name = "anisotropy",
|
||||
db_path = env["BUILD"],
|
||||
salome_timeout = 15 * 60,
|
||||
openfoam_template = os.path.join(env["ROOT"], "anisotropy/openfoam/template")
|
||||
)
|
||||
PROJECT = path.abspath(path.dirname(__file__))
|
||||
TMP = "/tmp/anisotropy"
|
||||
|
||||
del os
|
||||
env = {
|
||||
"PROJECT": path.abspath(path.dirname(__file__)),
|
||||
"TMP": TMP,
|
||||
"CWD": TMP,
|
||||
"BUILD_DIR": "build",
|
||||
"CONF_FILE": "anisotropy.toml",
|
||||
"LOG_FILE": "anisotropy.log",
|
||||
"DB_FILE": "anisotropy.db"
|
||||
}
|
||||
|
||||
def loadEnv():
|
||||
prefix = "ANISOTROPY_"
|
||||
|
||||
for k, v in env.items():
|
||||
os.environ[f"{ prefix }{ k }"] = v
|
||||
|
||||
del os, path, PROJECT, TMP
|
||||
|
@ -77,6 +77,7 @@ def verboseLevel(level: int):
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option()
|
||||
def anisotropy():
|
||||
pass
|
||||
|
||||
@ -119,19 +120,19 @@ def init(path, verbose):
|
||||
@click.option(
|
||||
"-N", "--nprocs", "nprocs",
|
||||
type = click.INT,
|
||||
default = 1,
|
||||
#default = 1,
|
||||
help = "Count of parallel processes"
|
||||
)
|
||||
@click.option(
|
||||
"-s", "--stage", "stage",
|
||||
type = click.Choice(["all", "shape", "mesh", "flow", "postProcess"]),
|
||||
default = "all",
|
||||
#default = "all",
|
||||
help = "Current computation stage"
|
||||
)
|
||||
@click.option(
|
||||
"-f", "--force", "overwrite",
|
||||
is_flag = True,
|
||||
default = False,
|
||||
#default = False,
|
||||
help = "Overwrite existing entries"
|
||||
)
|
||||
@click.option(
|
||||
@ -149,12 +150,24 @@ def init(path, verbose):
|
||||
@click.option(
|
||||
"--exec-id", "execution"
|
||||
)
|
||||
def compute(path, configFile, nprocs, stage, overwrite, params, verbose, execution):
|
||||
@click.option(
|
||||
"--pid", "pid",
|
||||
help = "Specify pid file path"
|
||||
)
|
||||
@click.option(
|
||||
"--logfile", "logfile",
|
||||
help = "Specify log file path"
|
||||
)
|
||||
def compute(path, configFile, nprocs, stage, overwrite, params, verbose, execution, pid, logfile):
|
||||
from anisotropy.core.runner import UltimateRunner
|
||||
from anisotropy.core.config import DefaultConfig
|
||||
from anisotropy.core.utils import setupLogger
|
||||
|
||||
setupLogger(verboseLevel(verbose))
|
||||
if path:
|
||||
os.makedirs(os.path.abspath(path), exist_ok = True)
|
||||
os.chdir(os.path.abspath(path))
|
||||
|
||||
setupLogger(verboseLevel(verbose), logfile)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
config = DefaultConfig()
|
||||
@ -162,34 +175,69 @@ def compute(path, configFile, nprocs, stage, overwrite, params, verbose, executi
|
||||
if configFile:
|
||||
filepath = os.path.abspath(configFile)
|
||||
logger.info(f"Loading file from { filepath }")
|
||||
|
||||
try:
|
||||
config.load(configFile)
|
||||
|
||||
except FileNotFoundError:
|
||||
config.dump(configFile)
|
||||
|
||||
else:
|
||||
logger.info("Using default configuration")
|
||||
|
||||
config.update(
|
||||
nprocs = nprocs,
|
||||
stage = stage,
|
||||
overwrite = overwrite
|
||||
)
|
||||
args = {
|
||||
"nprocs": nprocs,
|
||||
"stage": stage,
|
||||
"overwrite": overwrite
|
||||
}
|
||||
|
||||
for k, v in args.items():
|
||||
if v is not None:
|
||||
config.update(**{ k: v })
|
||||
|
||||
if pid:
|
||||
pidpath = os.path.abspath(pid)
|
||||
|
||||
with open(pidpath, "w") as io:
|
||||
io.write(str(os.getpid()))
|
||||
|
||||
runner = UltimateRunner(config = config, exec_id = execution)
|
||||
runner.fill()
|
||||
runner.start()
|
||||
|
||||
os.remove(pidpath)
|
||||
|
||||
@anisotropy.command()
|
||||
@click.option(
|
||||
"-P", "--path", "path",
|
||||
default = os.getcwd(),
|
||||
help = "Specify directory to use (instead of cwd)"
|
||||
)
|
||||
@click.option(
|
||||
"-v", "--verbose", "verbose",
|
||||
count = True,
|
||||
help = "Increase verbose level"
|
||||
)
|
||||
def gui(verbose):
|
||||
import anisotropy
|
||||
from anisotropy.core.utils import setupLogger
|
||||
from anisotropy.gui import app
|
||||
|
||||
anisotropy.loadEnv()
|
||||
|
||||
os.makedirs(os.path.abspath(path), exist_ok = True)
|
||||
os.chdir(os.path.abspath(path))
|
||||
os.environ["ANISOTROPY_CWD"] = path
|
||||
|
||||
setupLogger(verboseLevel(verbose))
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app.run_server(debug = True)
|
||||
|
||||
|
||||
##############
|
||||
"""
|
||||
def version():
|
||||
msg = "Missed package anisotropy"
|
||||
|
||||
try:
|
||||
from anisotropy.core.main import Anisotropy
|
||||
msg = Anisotropy.version()
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return msg
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version = "", message = version())
|
||||
@ -197,339 +245,6 @@ def anisotropy():
|
||||
pass
|
||||
|
||||
|
||||
@anisotropy.command(
|
||||
help = "Initialize new anisotropy project."
|
||||
)
|
||||
@click.option(
|
||||
"-P", "--path", "path",
|
||||
default = os.getcwd(),
|
||||
help = "Specify directory to use (instead of cwd)"
|
||||
)
|
||||
def init(path):
|
||||
from anisotropy import env
|
||||
from anisotropy.core.main import Database
|
||||
|
||||
if not os.path.exists(path) or not os.path.isdir(path):
|
||||
click.echo(f"Cannot find directory { path }")
|
||||
|
||||
return
|
||||
|
||||
wds = [ "build", "logs" ]
|
||||
|
||||
for wd in wds:
|
||||
os.makedirs(os.path.join(path, wd), exist_ok = True)
|
||||
|
||||
shutil.copy(env["CONFIG"], os.path.join(path, "anisotropy.toml"), follow_symlinks = True)
|
||||
|
||||
db = Database(env["db_name"], path)
|
||||
db.setup()
|
||||
|
||||
click.echo(f"Initialized anisotropy project in { path }")
|
||||
|
||||
|
||||
@anisotropy.command(
|
||||
help = "Load parameters from configuration file and update database."
|
||||
)
|
||||
@click.option(
|
||||
"-f", "--force", "force",
|
||||
is_flag = True,
|
||||
default = False,
|
||||
help = "Overwrite existing entries"
|
||||
)
|
||||
@click.option(
|
||||
"-p", "--params", "params",
|
||||
metavar = "key=value",
|
||||
multiple = True,
|
||||
cls = KeyValueOption,
|
||||
help = "Specify control parameters to update (type, direction, theta)"
|
||||
)
|
||||
@click.option(
|
||||
"-P", "--path", "path",
|
||||
default = os.getcwd(),
|
||||
help = "Specify directory to use (instead of cwd)"
|
||||
)
|
||||
def update(force, params, path):
|
||||
from anisotropy import env
|
||||
from anisotropy.core.main import Anisotropy, Database
|
||||
|
||||
env.update(
|
||||
LOG = os.path.join(path, "logs"),
|
||||
BUILD = os.path.join(path, "build"),
|
||||
CONFIG = os.path.join(path, "anisotropy.toml"),
|
||||
db_path = path
|
||||
)
|
||||
|
||||
args = dict()
|
||||
|
||||
for param in params:
|
||||
args.update(param)
|
||||
|
||||
|
||||
model = Anisotropy()
|
||||
database = Database(env["db_name"], env["db_path"])
|
||||
|
||||
click.echo("Configuring database ...")
|
||||
database.setup()
|
||||
|
||||
if database.isempty() or update:
|
||||
paramsAll = model.loadFromScratch(env["CONFIG"])
|
||||
|
||||
if args.get("type"):
|
||||
paramsAll = [ entry for entry in paramsAll if args["type"] == entry["structure"]["type"] ]
|
||||
|
||||
if args.get("direction"):
|
||||
paramsAll = [ entry for entry in paramsAll if args["direction"] == entry["structure"]["direction"] ]
|
||||
|
||||
if args.get("theta"):
|
||||
paramsAll = [ entry for entry in paramsAll if args["theta"] == entry["structure"]["theta"] ]
|
||||
|
||||
from anisotropy.core.models import Structure, Mesh
|
||||
#from numpy
|
||||
for entry in paramsAll:
|
||||
database.update(entry)
|
||||
|
||||
|
||||
click.echo("{} entries was updated.".format(len(paramsAll)))
|
||||
|
||||
else:
|
||||
click.echo("Database was not modified.")
|
||||
|
||||
|
||||
@anisotropy.command(
|
||||
# help = """#Compute cases by chain (mesh -> flow)
|
||||
#
|
||||
# Control parameters: type, direction, theta (each parameter affects on a queue)
|
||||
"""
|
||||
)
|
||||
@click.option(
|
||||
"-s", "--stage", "stage",
|
||||
type = click.Choice(["all", "mesh", "flow", "postProcessing"]),
|
||||
default = "all",
|
||||
help = "Current computation stage"
|
||||
)
|
||||
@click.option(
|
||||
"-N", "--nprocs", "nprocs",
|
||||
type = click.INT,
|
||||
default = 1,
|
||||
help = "Count of parallel processes"
|
||||
)
|
||||
@click.option(
|
||||
"-f", "--force", "force",
|
||||
is_flag = True,
|
||||
default = False,
|
||||
help = "Overwrite existing entries"
|
||||
)
|
||||
@click.option(
|
||||
"-p", "--params", "params",
|
||||
metavar = "key=value",
|
||||
multiple = True,
|
||||
cls = KeyValueOption,
|
||||
help = "Overwrite existing parameter (except control variables)"
|
||||
)
|
||||
@click.option(
|
||||
"-P", "--path", "path",
|
||||
default = os.getcwd(),
|
||||
help = "Specify directory to use (instead of cwd)"
|
||||
)
|
||||
def compute(stage, nprocs, force, params, path):
|
||||
from anisotropy import env
|
||||
from anisotropy.core.main import Anisotropy, Database, logger
|
||||
from anisotropy.core.utils import setupLogger, parallel
|
||||
|
||||
env.update(
|
||||
LOG = os.path.join(path, "logs"),
|
||||
BUILD = os.path.join(path, "build"),
|
||||
CONFIG = os.path.join(path, "anisotropy.toml"),
|
||||
db_path = path
|
||||
)
|
||||
|
||||
setupLogger(logger, logging.INFO, env["LOG"])
|
||||
args = dict()
|
||||
|
||||
for param in params:
|
||||
args.update(param)
|
||||
|
||||
###
|
||||
logger.info("Writing pid ...")
|
||||
pidpath = os.path.join(path, "anisotropy.pid")
|
||||
|
||||
with open(pidpath, "w") as io:
|
||||
io.write(str(os.getpid()))
|
||||
|
||||
###
|
||||
# Preparations
|
||||
##
|
||||
database = Database(env["db_name"], env["db_path"])
|
||||
|
||||
logger.info("Loading database ...")
|
||||
database.setup()
|
||||
|
||||
params = database.loadGeneral(
|
||||
args.get("type"),
|
||||
args.get("direction"),
|
||||
args.get("theta")
|
||||
)
|
||||
queueargs = []
|
||||
|
||||
for p in params:
|
||||
s = p["structure"]
|
||||
|
||||
queueargs.append((s["type"], s["direction"], s["theta"]))
|
||||
|
||||
###
|
||||
# Wrap function
|
||||
##
|
||||
def computeCase(type, direction, theta):
|
||||
case = Anisotropy()
|
||||
case.db = database
|
||||
case.load(type, direction, theta)
|
||||
case.evalParams()
|
||||
case.update()
|
||||
|
||||
logger.info(f"Case: type = { type }, direction = { direction }, theta = { theta }")
|
||||
logger.info(f"Stage mode: { stage }")
|
||||
|
||||
if stage in ["mesh", "all"]:
|
||||
case.load(type, direction, theta)
|
||||
|
||||
if not case.params["meshresult"]["meshStatus"] == "Done" or force:
|
||||
logger.info("Current stage: mesh")
|
||||
out, err, returncode = case.computeMesh(path)
|
||||
|
||||
if out: logger.info(out)
|
||||
if err: logger.error(err)
|
||||
if returncode:
|
||||
logger.error("Mesh computation failed. Skipping flow computation ...")
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
logger.info("Mesh exists. Skipping ...")
|
||||
|
||||
if stage in ["flow", "all"]:
|
||||
case.load(type, direction, theta)
|
||||
|
||||
if not case.params["flowresult"]["flowStatus"] == "Done" or force:
|
||||
logger.info("Current stage: flow")
|
||||
out, err, returncode = case.computeFlow(path)
|
||||
|
||||
if out: logger.info(out)
|
||||
if err: logger.error(err)
|
||||
if returncode:
|
||||
logger.error("Flow computation failed.")
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
logger.info("Flow exists. Skipping ...")
|
||||
|
||||
if stage in ["postProcessing", "all"]:
|
||||
case.load(type, direction, theta)
|
||||
|
||||
if case.params["meshresult"]["meshStatus"] == "Done":
|
||||
logger.info("Current stage: mesh postProcessing")
|
||||
case.porosity()
|
||||
|
||||
else:
|
||||
logger.warning("Cannot compute mesh post processing values.")
|
||||
|
||||
if case.params["flowresult"]["flowStatus"] == "Done":
|
||||
logger.info("Current stage: flow postProcessing")
|
||||
case.flowRate()
|
||||
|
||||
else:
|
||||
logger.warning("Cannot compute flow post processing values.")
|
||||
|
||||
|
||||
###
|
||||
# Run
|
||||
##
|
||||
if nprocs == 1:
|
||||
for pos, qarg in enumerate(queueargs):
|
||||
computeCase(*qarg)
|
||||
|
||||
else:
|
||||
parallel(nprocs, queueargs, computeCase)
|
||||
|
||||
if os.path.exists(pidpath):
|
||||
logger.info("Removing pid ...")
|
||||
os.remove(pidpath)
|
||||
|
||||
logger.info("Computation done.")
|
||||
|
||||
|
||||
@anisotropy.command(
|
||||
help = "Kill process by pid file"
|
||||
)
|
||||
@click.option(
|
||||
"-P", "--path", "path",
|
||||
default = os.getcwd(),
|
||||
help = "Specify directory to use (instead of cwd)"
|
||||
)
|
||||
@click.argument("pidfile")
|
||||
def kill(path, pidfile):
|
||||
from anisotropy.salomepl.utils import SalomeManager
|
||||
|
||||
try:
|
||||
with open(os.path.join(path, pidfile), "r") as io:
|
||||
pid = io.read()
|
||||
|
||||
os.kill(int(pid), 9)
|
||||
|
||||
except FileNotFoundError:
|
||||
click.echo(f"Unknown file { pidfile }")
|
||||
|
||||
except ProcessLookupError:
|
||||
click.echo(f"Cannot find process with pid { pid }")
|
||||
|
||||
# TODO: killall method kills all salome instances. Not a good way
|
||||
SalomeManager().killall()
|
||||
|
||||
@anisotropy.command(
|
||||
help = "! Internal command"
|
||||
)
|
||||
@click.argument("root")
|
||||
@click.argument("type")
|
||||
@click.argument("direction")
|
||||
@click.argument("theta")
|
||||
@click.argument("path")
|
||||
def computemesh(root, type, direction, theta, path):
|
||||
# ISSUE: can't hide command from help, 'hidden = True' doesn't work
|
||||
# [Salome Environment]
|
||||
|
||||
###
|
||||
# Args
|
||||
##
|
||||
direction = [ float(num) for num in direction[1:-1].split(" ") if num ]
|
||||
theta = float(theta)
|
||||
|
||||
###
|
||||
# Modules
|
||||
##
|
||||
import os, sys
|
||||
|
||||
pyversion = "{}.{}".format(3, 9) #(*sys.version_info[:2])
|
||||
sys.path.extend([
|
||||
root,
|
||||
os.path.join(root, "env/lib/python{}/site-packages".format(pyversion)),
|
||||
os.path.abspath(".local/lib/python{}/site-packages".format(pyversion))
|
||||
])
|
||||
|
||||
from anisotropy import env
|
||||
from anisotropy.core.main import Anisotropy, Database
|
||||
import salome
|
||||
|
||||
###
|
||||
model = Anisotropy()
|
||||
model.db = Database(env["db_name"], path)
|
||||
model.load(type, direction, theta)
|
||||
|
||||
salome.salome_init()
|
||||
model.genmesh(path)
|
||||
salome.salome_close()
|
||||
|
||||
|
||||
|
||||
@anisotropy.command()
|
||||
@click.option(
|
||||
"-p", "--params", "params",
|
||||
@ -683,12 +398,4 @@ if __name__ == "__main__":
|
||||
# click.echo("Interrupted!")
|
||||
|
||||
#finally:
|
||||
# from anisotropy.salomepl.utils import SalomeManager
|
||||
# click.echo("Exiting ...")
|
||||
|
||||
# if os.path.exists("anisotropy.pid"):
|
||||
# os.remove("anisotropy.pid")
|
||||
|
||||
# SalomeManager().killall()
|
||||
|
||||
# sys.exit(0)
|
||||
|
@ -42,6 +42,7 @@ class Config(object):
|
||||
|
||||
self.content = toml.load(path)
|
||||
self.options = deepcopy(self.content["options"])
|
||||
self.content.pop("options")
|
||||
|
||||
def dump(self, filename: str):
|
||||
path = os.path.abspath(filename)
|
||||
@ -52,6 +53,8 @@ class Config(object):
|
||||
|
||||
os.makedirs(os.path.split(path)[0], exist_ok = True)
|
||||
|
||||
self.content.update(options = self.options)
|
||||
|
||||
with open(path, "w") as io:
|
||||
toml.dump(self.content, io, encoder = toml.TomlNumpyEncoder())
|
||||
|
||||
|
@ -13,16 +13,16 @@ from types import FunctionType
|
||||
import os
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
def _getFormat(self, level: int):
|
||||
def __init__(self, colors: bool = True):
|
||||
self.colors = colors
|
||||
|
||||
def applyColors(self, level: int, info: str, msg: str):
|
||||
grey = "\x1b[38;21m"
|
||||
yellow = "\x1b[33;21m"
|
||||
red = "\x1b[31;21m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
reset = "\x1b[0m"
|
||||
|
||||
info = "[%(levelname)s %(processName)s %(asctime)s %(funcName)s]" # [ %(processName)s ]
|
||||
msg = " %(message)s"
|
||||
|
||||
formats = {
|
||||
logging.DEBUG: grey + info + reset + msg,
|
||||
logging.INFO: grey + info + reset + msg,
|
||||
@ -34,7 +34,15 @@ class CustomFormatter(logging.Formatter):
|
||||
return formats.get(level)
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self._getFormat(record.levelno)
|
||||
info = "[%(levelname)s %(processName)s %(asctime)s %(funcName)s]" # [ %(processName)s ]
|
||||
msg = " %(message)s"
|
||||
|
||||
if self.colors:
|
||||
log_fmt = self.applyColors(record.levelno, info, msg)
|
||||
|
||||
else:
|
||||
log_fmt = info + msg
|
||||
|
||||
time_fmt = "%d-%m-%y %H:%M:%S"
|
||||
formatter = logging.Formatter(log_fmt, time_fmt)
|
||||
|
||||
@ -42,10 +50,7 @@ class CustomFormatter(logging.Formatter):
|
||||
|
||||
|
||||
def setupLogger(level: int, filepath: str = None):
|
||||
"""Applies settings to logger
|
||||
|
||||
:param logger:
|
||||
Instance of :class:`logging.Logger`
|
||||
"""Applies settings to root logger
|
||||
|
||||
:param level:
|
||||
Logging level (logging.INFO, logging.WARNING, ..)
|
||||
@ -53,8 +58,6 @@ def setupLogger(level: int, filepath: str = None):
|
||||
:param filepath:
|
||||
Path to directory
|
||||
"""
|
||||
#logger.handlers = []
|
||||
#logger.setLevel(level)
|
||||
|
||||
logging.addLevelName(logging.INFO, "II")
|
||||
logging.addLevelName(logging.WARNING, "WW")
|
||||
@ -64,20 +67,14 @@ def setupLogger(level: int, filepath: str = None):
|
||||
streamhandler = logging.StreamHandler()
|
||||
streamhandler.setLevel(level)
|
||||
streamhandler.setFormatter(CustomFormatter())
|
||||
#logger.addHandler(streamhandler)
|
||||
|
||||
logging.root.setLevel(level)
|
||||
logging.root.addHandler(streamhandler)
|
||||
|
||||
if filepath:
|
||||
if not os.path.exists(filepath):
|
||||
os.makedirs(filepath, exist_ok = True)
|
||||
logging.root.setLevel(level)
|
||||
|
||||
filehandler = logging.FileHandler(
|
||||
os.path.join(filepath, "{}.log".format("anisotropy"))
|
||||
)
|
||||
if filepath:
|
||||
filehandler = logging.FileHandler(filepath)
|
||||
filehandler.setLevel(logging.INFO)
|
||||
filehandler.setFormatter(CustomFormatter())
|
||||
filehandler.setFormatter(CustomFormatter(colors = False))
|
||||
|
||||
logging.root.addHandler(filehandler)
|
||||
|
||||
|
5
anisotropy/gui/__init__.py
Normal file
5
anisotropy/gui/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
from .main import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run_server(debug = True)
|
23
anisotropy/gui/about.py
Normal file
23
anisotropy/gui/about.py
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
from dash import html
|
||||
from dash import dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
from .styles import *
|
||||
import anisotropy
|
||||
|
||||
|
||||
###
|
||||
# Layout
|
||||
##
|
||||
layout = html.Div([
|
||||
html.H1(anisotropy.__name__),
|
||||
html.Hr(),
|
||||
html.P([
|
||||
"Author: {}".format(anisotropy.__author__),
|
||||
html.Br(),
|
||||
"License: {}".format(anisotropy.__license__),
|
||||
html.Br(),
|
||||
"Version: {}".format(anisotropy.__version__)
|
||||
])
|
||||
])
|
9
anisotropy/gui/app.py
Normal file
9
anisotropy/gui/app.py
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
import dash
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
app = dash.Dash(__name__, external_stylesheets = [ dbc.themes.LUX ])
|
||||
app.title = "anisotropy"
|
||||
app.config.update(
|
||||
update_title = None
|
||||
)
|
BIN
anisotropy/gui/assets/gh-light.png
Normal file
BIN
anisotropy/gui/assets/gh-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
anisotropy/gui/assets/simple.png
Normal file
BIN
anisotropy/gui/assets/simple.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
70
anisotropy/gui/main.py
Normal file
70
anisotropy/gui/main.py
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
from dash import html
|
||||
from dash import dcc
|
||||
from dash.dependencies import Input, Output, State
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
from . import (
|
||||
runner,
|
||||
settings,
|
||||
about
|
||||
)
|
||||
from .app import app
|
||||
from .styles import *
|
||||
import anisotropy
|
||||
|
||||
###
|
||||
# Layout
|
||||
##
|
||||
app.layout = html.Div([
|
||||
# Location
|
||||
dcc.Location(id = "url", refresh = False),
|
||||
|
||||
# Sidebar
|
||||
html.Div([
|
||||
# Sidebar
|
||||
html.H2([html.Img(src = "/assets/simple.png", height = "150px")], style = logo),
|
||||
html.Hr(style = { "color": "#ffffff" }),
|
||||
dbc.Nav([
|
||||
dbc.NavLink("Runner", href = "/", active = "exact", style = white),
|
||||
dbc.NavLink("Settings", href = "/settings", active = "exact", style = white),
|
||||
dbc.NavLink("About", href = "/about", active = "exact", style = white),
|
||||
], vertical = True, pills = True),
|
||||
|
||||
# Misc
|
||||
html.Hr(style = white),
|
||||
dbc.Container([
|
||||
dbc.Row([
|
||||
dbc.Col("v1.2.0"),
|
||||
dbc.Col(
|
||||
html.A(
|
||||
html.Img(src = "/assets/gh-light.png", height = "20px"),
|
||||
href = anisotropy.__repository__
|
||||
)
|
||||
)
|
||||
])
|
||||
], style = misc)
|
||||
], style = sidebar),
|
||||
|
||||
# Content
|
||||
html.Div(id = "page-content", style = page),
|
||||
], style = content)
|
||||
|
||||
|
||||
###
|
||||
# Callbacks
|
||||
##
|
||||
@app.callback(
|
||||
Output("page-content", "children"),
|
||||
[ Input("url", "pathname") ]
|
||||
)
|
||||
def displayPage(pathname):
|
||||
if pathname == "/settings":
|
||||
return settings.layout
|
||||
|
||||
elif pathname == "/about":
|
||||
return about.layout
|
||||
|
||||
else:
|
||||
return runner.layout
|
||||
|
168
anisotropy/gui/runner.py
Normal file
168
anisotropy/gui/runner.py
Normal file
@ -0,0 +1,168 @@
|
||||
|
||||
from dash.dash_table import DataTable
|
||||
from dash import html
|
||||
from dash import dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash.dependencies import Input, Output, State
|
||||
|
||||
import os
|
||||
|
||||
from .app import app
|
||||
from .styles import *
|
||||
from .utils import getSize
|
||||
|
||||
|
||||
###
|
||||
# Layout
|
||||
##
|
||||
layout = html.Div([
|
||||
# Messages and timer
|
||||
dbc.Alert(
|
||||
id = "status",
|
||||
duration = 10000,
|
||||
dismissable = True,
|
||||
is_open = False,
|
||||
style = message
|
||||
),
|
||||
dcc.Interval(id = "interval", interval = 1000, n_intervals = 0),
|
||||
|
||||
# Runner
|
||||
html.H2("Runner"),
|
||||
html.Hr(),
|
||||
dbc.Button("Start", id = "start", color = "success", style = minWidth),
|
||||
dbc.Button("Stop", id = "stop", color = "danger", disabled = True, style = minWidth),
|
||||
|
||||
# Monitor
|
||||
html.H2("Monitor"),
|
||||
html.Hr(),
|
||||
html.P(id = "runner-status"),
|
||||
DataTable(id = "monitor", columns = [], data = [], style_table = table),
|
||||
|
||||
# Log
|
||||
html.H2("Log"),
|
||||
html.Hr(),
|
||||
dbc.Button("Delete", id = "delete", style = minWidth),
|
||||
dcc.Textarea(id = "logger", disabled = True, style = bigText)
|
||||
|
||||
])
|
||||
|
||||
|
||||
###
|
||||
# Callbacks
|
||||
##
|
||||
@app.callback(
|
||||
Output("start", "active"),
|
||||
[ Input("start", "n_clicks") ],
|
||||
prevent_initial_call = True
|
||||
)
|
||||
def runnerStart(clicks):
|
||||
import subprocess
|
||||
|
||||
command = [
|
||||
"anisotropy",
|
||||
"compute",
|
||||
"-v",
|
||||
"--path", "/tmp/anisotropy",
|
||||
"--conf", "anisotropy.toml",
|
||||
"--pid", "anisotropy.pid",
|
||||
"--logfile", "anisotropy.log"
|
||||
]
|
||||
|
||||
subprocess.run(
|
||||
command,
|
||||
start_new_session = True,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@app.callback(
|
||||
Output("stop", "active"),
|
||||
[ Input("stop", "n_clicks") ],
|
||||
prevent_initial_call = True
|
||||
)
|
||||
def runnerStop(clicks):
|
||||
import psutil
|
||||
import signal
|
||||
|
||||
pidpath = "/tmp/anisotropy/anisotropy.pid"
|
||||
|
||||
try:
|
||||
pid = int(open(pidpath, "r").read())
|
||||
master = psutil.Process(pid)
|
||||
|
||||
except (FileNotFoundError, psutil.NoSuchProcess):
|
||||
return True
|
||||
|
||||
else:
|
||||
os.killpg(master.pid, signal.SIGTERM)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@app.callback(
|
||||
Output("monitor", "columns"),
|
||||
Output("monitor", "data"),
|
||||
Output("runner-status", "children"),
|
||||
Output("start", "disabled"),
|
||||
Output("stop", "disabled"),
|
||||
Output("delete", "disabled"),
|
||||
[ Input("interval", "n_intervals") ],
|
||||
)
|
||||
def monitorUpdate(intervals):
|
||||
import psutil
|
||||
|
||||
pidpath = "/tmp/anisotropy/anisotropy.pid"
|
||||
processes = []
|
||||
|
||||
try:
|
||||
pid = int(open(pidpath, "r").read())
|
||||
master = psutil.Process(pid)
|
||||
|
||||
except (FileNotFoundError, psutil.NoSuchProcess) as e:
|
||||
return [], [], "Status: not running", False, True, False
|
||||
|
||||
else:
|
||||
for process in [ master, *master.children() ]:
|
||||
created = psutil.time.localtime(process.create_time())
|
||||
processes.append({
|
||||
"name": process.name(),
|
||||
"pid": process.pid,
|
||||
"status": process.status(),
|
||||
"memory": getSize(process.memory_full_info().uss),
|
||||
"threads": process.num_threads(),
|
||||
"created": "{}:{}:{}".format(created.tm_hour, created.tm_min, created.tm_sec)
|
||||
})
|
||||
|
||||
columns = [ { "name": col, "id": col } for col in processes[0].keys() ]
|
||||
|
||||
return columns, processes, "Status: running", True, False, True
|
||||
|
||||
@app.callback(
|
||||
Output("logger", "value"),
|
||||
[ Input("interval", "n_intervals") ]
|
||||
)
|
||||
def logUpdate(intervals):
|
||||
logpath = "/tmp/anisotropy/anisotropy.log"
|
||||
|
||||
if os.path.exists(logpath):
|
||||
with open(logpath, "r") as io:
|
||||
log = io.read()
|
||||
|
||||
return log
|
||||
|
||||
else:
|
||||
return "Not found"
|
||||
|
||||
|
||||
@app.callback(
|
||||
Output("delete", "active"),
|
||||
[ Input("delete", "n_clicks") ]
|
||||
)
|
||||
def logDelete(clicks):
|
||||
logpath = "/tmp/anisotropy/anisotropy.log"
|
||||
|
||||
if os.path.exists(logpath):
|
||||
os.remove(logpath)
|
||||
|
||||
return True
|
107
anisotropy/gui/settings.py
Normal file
107
anisotropy/gui/settings.py
Normal file
@ -0,0 +1,107 @@
|
||||
|
||||
from dash import html
|
||||
from dash import dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash.dependencies import Input, Output, State
|
||||
|
||||
import os
|
||||
from .app import app
|
||||
from .styles import *
|
||||
|
||||
|
||||
###
|
||||
# Layout
|
||||
##
|
||||
layout = html.Div([
|
||||
# Messages
|
||||
dbc.Alert(
|
||||
id = "status",
|
||||
duration = 10000,
|
||||
dismissable = True,
|
||||
is_open = False,
|
||||
style = message
|
||||
),
|
||||
|
||||
# General
|
||||
html.H2("General"),
|
||||
html.Hr(),
|
||||
html.P("Path: {}".format(os.environ.get("ANISOTROPY_CWD", ""))),
|
||||
dbc.Button("Save", id = "submit", style = minWidth),
|
||||
|
||||
# Options
|
||||
html.H2("Options"),
|
||||
html.Hr(),
|
||||
html.P("Nprocs"),
|
||||
dcc.Input(id = "nprocs", type = "number", style = minWidth),
|
||||
html.P("Stage"),
|
||||
dcc.Dropdown(
|
||||
id = "stage",
|
||||
options = [ { "label": k, "value": k } for k in ["all", "shape", "mesh", "flow", "postProcess"] ],
|
||||
style = minWidth
|
||||
),
|
||||
|
||||
# Cases
|
||||
html.H2("Cases"),
|
||||
html.Hr(),
|
||||
dcc.Textarea(id = "cases", style = bigText),
|
||||
])
|
||||
|
||||
|
||||
###
|
||||
# Callbacks
|
||||
##
|
||||
@app.callback(
|
||||
Output("nprocs", "value"),
|
||||
Output("stage", "value"),
|
||||
Output("cases", "value"),
|
||||
[ Input("url", "pathname") ]
|
||||
)
|
||||
def settingsLoad(pathname):
|
||||
from anisotropy.core.config import DefaultConfig
|
||||
import toml
|
||||
|
||||
filepath = os.path.join(os.environ["ANISOTROPY_CWD"], os.environ["ANISOTROPY_CONF_FILE"])
|
||||
config = DefaultConfig()
|
||||
|
||||
if os.path.exists(filepath):
|
||||
config.load(filepath)
|
||||
|
||||
return config["nprocs"], config["stage"], toml.dumps(config.content)
|
||||
|
||||
|
||||
@app.callback(
|
||||
Output("status", "children"),
|
||||
Output("status", "is_open"),
|
||||
Output("status", "color"),
|
||||
[ Input("submit", "n_clicks") ],
|
||||
[
|
||||
State("nprocs", "value"),
|
||||
State("stage", "value"),
|
||||
State("cases", "value")
|
||||
],
|
||||
prevent_initial_call = True
|
||||
)
|
||||
def settingsSave(nclick, nprocs, stage, cases):
|
||||
from anisotropy.core.config import DefaultConfig
|
||||
import toml
|
||||
|
||||
filepath = os.path.join(os.environ["ANISOTROPY_CWD"], os.environ["ANISOTROPY_CONF_FILE"])
|
||||
config = DefaultConfig()
|
||||
|
||||
if os.path.exists(filepath):
|
||||
config.load(filepath)
|
||||
|
||||
config.update(
|
||||
nprocs = nprocs,
|
||||
stage = stage
|
||||
)
|
||||
|
||||
try:
|
||||
config.content = toml.loads(cases)
|
||||
config.dump(filepath)
|
||||
|
||||
except Exception as e:
|
||||
return str(e), True, "danger"
|
||||
|
||||
else:
|
||||
return f"Saved to { filepath }", True, "success"
|
56
anisotropy/gui/styles.py
Normal file
56
anisotropy/gui/styles.py
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
minWidth = {
|
||||
"min-width": "200px",
|
||||
"max-width": "200px",
|
||||
"margin-bottom": "15px"
|
||||
}
|
||||
|
||||
bigText = {
|
||||
"min-width": "100%",
|
||||
"min-height": "500px"
|
||||
}
|
||||
|
||||
message = {
|
||||
"position": "fixed",
|
||||
"top": "30px",
|
||||
"right": "30px",
|
||||
}
|
||||
|
||||
page = {
|
||||
"padding": "30px 50px 0px 50px"
|
||||
}
|
||||
|
||||
table = {
|
||||
"margin-bottom": "15px",
|
||||
"min-width": "100%"
|
||||
}
|
||||
|
||||
sidebar = {
|
||||
"position": "fixed",
|
||||
"top": 0,
|
||||
"left": 0,
|
||||
"bottom": 0,
|
||||
"width": "16rem",
|
||||
"padding": "2rem 1rem",
|
||||
"background-color": "#363636"
|
||||
}
|
||||
|
||||
content = {
|
||||
"margin-left": "18rem",
|
||||
"margin-right": "2rem",
|
||||
"padding": "2rem 1rem",
|
||||
}
|
||||
|
||||
misc = {
|
||||
"color": "#757575",
|
||||
"text-align": "center"
|
||||
}
|
||||
|
||||
logo = {
|
||||
"color": "#ffffff",
|
||||
"text-align": "center"
|
||||
}
|
||||
|
||||
white = {
|
||||
"color": "#ffffff"
|
||||
}
|
7
anisotropy/gui/utils.py
Normal file
7
anisotropy/gui/utils.py
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
def getSize(bytes):
|
||||
for unit in ["", "K", "M", "G", "T", "P"]:
|
||||
if bytes < 1024:
|
||||
return f"{bytes:.2f} {unit}iB"
|
||||
|
||||
bytes /= 1024
|
Loading…
Reference in New Issue
Block a user