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
|
*anisotropy* is a ``Python`` package that is the result of science-research work
|
||||||
on the anisotropy of permeability in the periodic porous media.
|
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"
|
__license__ = "GPL3"
|
||||||
__version__ = "1.2.0"
|
__version__ = "1.2.0"
|
||||||
__author__ = __maintainer__ = "George Kusayko"
|
__author__ = __maintainer__ = "George Kusayko"
|
||||||
__email__ = "gkusayko@gmail.com"
|
__email__ = "gkusayko@gmail.com"
|
||||||
|
__repository__ = "https://github.com/L-Nafaryus/anisotropy"
|
||||||
|
|
||||||
###
|
###
|
||||||
# Environment
|
# Environment
|
||||||
##
|
##
|
||||||
import os
|
import os
|
||||||
|
from os import path
|
||||||
|
|
||||||
env = dict(
|
PROJECT = path.abspath(path.dirname(__file__))
|
||||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
TMP = "/tmp/anisotropy"
|
||||||
)
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
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.group()
|
||||||
|
@click.version_option()
|
||||||
def anisotropy():
|
def anisotropy():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -119,19 +120,19 @@ def init(path, verbose):
|
|||||||
@click.option(
|
@click.option(
|
||||||
"-N", "--nprocs", "nprocs",
|
"-N", "--nprocs", "nprocs",
|
||||||
type = click.INT,
|
type = click.INT,
|
||||||
default = 1,
|
#default = 1,
|
||||||
help = "Count of parallel processes"
|
help = "Count of parallel processes"
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-s", "--stage", "stage",
|
"-s", "--stage", "stage",
|
||||||
type = click.Choice(["all", "shape", "mesh", "flow", "postProcess"]),
|
type = click.Choice(["all", "shape", "mesh", "flow", "postProcess"]),
|
||||||
default = "all",
|
#default = "all",
|
||||||
help = "Current computation stage"
|
help = "Current computation stage"
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-f", "--force", "overwrite",
|
"-f", "--force", "overwrite",
|
||||||
is_flag = True,
|
is_flag = True,
|
||||||
default = False,
|
#default = False,
|
||||||
help = "Overwrite existing entries"
|
help = "Overwrite existing entries"
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -149,12 +150,24 @@ def init(path, verbose):
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--exec-id", "execution"
|
"--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.runner import UltimateRunner
|
||||||
from anisotropy.core.config import DefaultConfig
|
from anisotropy.core.config import DefaultConfig
|
||||||
from anisotropy.core.utils import setupLogger
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
config = DefaultConfig()
|
config = DefaultConfig()
|
||||||
@ -162,34 +175,69 @@ def compute(path, configFile, nprocs, stage, overwrite, params, verbose, executi
|
|||||||
if configFile:
|
if configFile:
|
||||||
filepath = os.path.abspath(configFile)
|
filepath = os.path.abspath(configFile)
|
||||||
logger.info(f"Loading file from { filepath }")
|
logger.info(f"Loading file from { filepath }")
|
||||||
config.load(configFile)
|
|
||||||
|
try:
|
||||||
|
config.load(configFile)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
config.dump(configFile)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info("Using default configuration")
|
logger.info("Using default configuration")
|
||||||
|
|
||||||
config.update(
|
args = {
|
||||||
nprocs = nprocs,
|
"nprocs": nprocs,
|
||||||
stage = stage,
|
"stage": stage,
|
||||||
overwrite = overwrite
|
"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 = UltimateRunner(config = config, exec_id = execution)
|
||||||
runner.fill()
|
runner.fill()
|
||||||
runner.start()
|
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.group()
|
||||||
@click.version_option(version = "", message = version())
|
@click.version_option(version = "", message = version())
|
||||||
@ -197,339 +245,6 @@ def anisotropy():
|
|||||||
pass
|
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()
|
@anisotropy.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"-p", "--params", "params",
|
"-p", "--params", "params",
|
||||||
@ -683,12 +398,4 @@ if __name__ == "__main__":
|
|||||||
# click.echo("Interrupted!")
|
# click.echo("Interrupted!")
|
||||||
|
|
||||||
#finally:
|
#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)
|
# sys.exit(0)
|
||||||
|
@ -42,6 +42,7 @@ class Config(object):
|
|||||||
|
|
||||||
self.content = toml.load(path)
|
self.content = toml.load(path)
|
||||||
self.options = deepcopy(self.content["options"])
|
self.options = deepcopy(self.content["options"])
|
||||||
|
self.content.pop("options")
|
||||||
|
|
||||||
def dump(self, filename: str):
|
def dump(self, filename: str):
|
||||||
path = os.path.abspath(filename)
|
path = os.path.abspath(filename)
|
||||||
@ -52,6 +53,8 @@ class Config(object):
|
|||||||
|
|
||||||
os.makedirs(os.path.split(path)[0], exist_ok = True)
|
os.makedirs(os.path.split(path)[0], exist_ok = True)
|
||||||
|
|
||||||
|
self.content.update(options = self.options)
|
||||||
|
|
||||||
with open(path, "w") as io:
|
with open(path, "w") as io:
|
||||||
toml.dump(self.content, io, encoder = toml.TomlNumpyEncoder())
|
toml.dump(self.content, io, encoder = toml.TomlNumpyEncoder())
|
||||||
|
|
||||||
|
@ -13,16 +13,16 @@ from types import FunctionType
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
class CustomFormatter(logging.Formatter):
|
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"
|
grey = "\x1b[38;21m"
|
||||||
yellow = "\x1b[33;21m"
|
yellow = "\x1b[33;21m"
|
||||||
red = "\x1b[31;21m"
|
red = "\x1b[31;21m"
|
||||||
bold_red = "\x1b[31;1m"
|
bold_red = "\x1b[31;1m"
|
||||||
reset = "\x1b[0m"
|
reset = "\x1b[0m"
|
||||||
|
|
||||||
info = "[%(levelname)s %(processName)s %(asctime)s %(funcName)s]" # [ %(processName)s ]
|
|
||||||
msg = " %(message)s"
|
|
||||||
|
|
||||||
formats = {
|
formats = {
|
||||||
logging.DEBUG: grey + info + reset + msg,
|
logging.DEBUG: grey + info + reset + msg,
|
||||||
logging.INFO: grey + info + reset + msg,
|
logging.INFO: grey + info + reset + msg,
|
||||||
@ -34,7 +34,15 @@ class CustomFormatter(logging.Formatter):
|
|||||||
return formats.get(level)
|
return formats.get(level)
|
||||||
|
|
||||||
def format(self, record):
|
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"
|
time_fmt = "%d-%m-%y %H:%M:%S"
|
||||||
formatter = logging.Formatter(log_fmt, time_fmt)
|
formatter = logging.Formatter(log_fmt, time_fmt)
|
||||||
|
|
||||||
@ -42,19 +50,14 @@ class CustomFormatter(logging.Formatter):
|
|||||||
|
|
||||||
|
|
||||||
def setupLogger(level: int, filepath: str = None):
|
def setupLogger(level: int, filepath: str = None):
|
||||||
"""Applies settings to logger
|
"""Applies settings to root logger
|
||||||
|
|
||||||
:param logger:
|
|
||||||
Instance of :class:`logging.Logger`
|
|
||||||
|
|
||||||
:param level:
|
:param level:
|
||||||
Logging level (logging.INFO, logging.WARNING, ..)
|
Logging level (logging.INFO, logging.WARNING, ..)
|
||||||
|
|
||||||
:param filepath:
|
:param filepath:
|
||||||
Path to directory
|
Path to directory
|
||||||
"""
|
"""
|
||||||
#logger.handlers = []
|
|
||||||
#logger.setLevel(level)
|
|
||||||
|
|
||||||
logging.addLevelName(logging.INFO, "II")
|
logging.addLevelName(logging.INFO, "II")
|
||||||
logging.addLevelName(logging.WARNING, "WW")
|
logging.addLevelName(logging.WARNING, "WW")
|
||||||
@ -64,20 +67,14 @@ def setupLogger(level: int, filepath: str = None):
|
|||||||
streamhandler = logging.StreamHandler()
|
streamhandler = logging.StreamHandler()
|
||||||
streamhandler.setLevel(level)
|
streamhandler.setLevel(level)
|
||||||
streamhandler.setFormatter(CustomFormatter())
|
streamhandler.setFormatter(CustomFormatter())
|
||||||
#logger.addHandler(streamhandler)
|
|
||||||
|
|
||||||
logging.root.setLevel(level)
|
|
||||||
logging.root.addHandler(streamhandler)
|
logging.root.addHandler(streamhandler)
|
||||||
|
|
||||||
|
logging.root.setLevel(level)
|
||||||
|
|
||||||
if filepath:
|
if filepath:
|
||||||
if not os.path.exists(filepath):
|
filehandler = logging.FileHandler(filepath)
|
||||||
os.makedirs(filepath, exist_ok = True)
|
|
||||||
|
|
||||||
filehandler = logging.FileHandler(
|
|
||||||
os.path.join(filepath, "{}.log".format("anisotropy"))
|
|
||||||
)
|
|
||||||
filehandler.setLevel(logging.INFO)
|
filehandler.setLevel(logging.INFO)
|
||||||
filehandler.setFormatter(CustomFormatter())
|
filehandler.setFormatter(CustomFormatter(colors = False))
|
||||||
|
|
||||||
logging.root.addHandler(filehandler)
|
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