580 lines
14 KiB
Python
Raw Normal View History

2021-08-23 21:54:40 +05:00
# -*- coding: utf-8 -*-
# This file is part of anisotropy.
# License: GNU GPL version 3, see the file "LICENSE" for details.
2021-07-14 23:30:50 +05:00
import click
2021-08-12 20:00:30 +05:00
import ast
import os, sys, shutil
import logging
2021-07-14 23:30:50 +05:00
2021-08-12 20:00:30 +05:00
class LiteralOption(click.Option):
def type_cast_value(self, ctx, value):
try:
return ast.literal_eval(value)
except:
raise click.BadParameter(f"{ value } (Type error)")
class KeyValueOption(click.Option):
def _convert(self, ctx, value):
if not value:
return {}
2021-08-12 20:00:30 +05:00
if value.find("=") == -1:
raise click.BadParameter(f"{ value } (Missed '=')")
params = value.split("=")
if not len(params) == 2:
raise click.BadParameter(f"{ value } (Syntax error)")
key, val = params[0].strip(), params[1].strip()
if val[0].isalpha():
val = f"'{ val }'"
try:
return { key: ast.literal_eval(val) }
except:
raise click.BadParameter(f"{ value } (Type error)")
def type_cast_value(self, ctx, value):
if isinstance(value, list):
return [ self._convert(ctx, val) for val in value ]
else:
return self._convert(ctx, value)
2021-09-07 14:08:04 +05:00
class CliListOption(click.Option):
def _convert(self, ctx, value):
if not value:
return []
output = [ val for val in value.split(",") ]
if "" in output:
raise click.BadParameter(f"{ value } (Trying to pass empty item)")
return output
def type_cast_value(self, ctx, value):
if isinstance(value, list):
return [ self._convert(ctx, val) for val in value ]
else:
return self._convert(ctx, value)
2021-08-12 20:00:30 +05:00
def version():
msg = "Missed package anisotropy"
2021-07-14 23:30:50 +05:00
try:
from anisotropy.core.main import Anisotropy
msg = Anisotropy.version()
except ImportError:
pass
return msg
2021-07-14 23:30:50 +05:00
@click.group()
@click.version_option(version = "", message = version())
def anisotropy():
pass
2021-07-14 23:30:50 +05:00
@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 }")
2021-08-23 21:54:40 +05:00
@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(
2021-09-07 14:08:04 +05:00
"-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"] ]
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)
2021-08-23 21:54:40 +05:00
Control parameters: type, direction, theta (each parameter affects on a queue)
"""
)
@click.option(
"-s", "--stage", "stage",
type = click.Choice(["all", "mesh", "flow", "postProcessing"]),
2021-08-23 21:54:40 +05:00
default = "all",
help = "Current computation stage"
)
@click.option(
2021-08-24 17:30:42 +05:00
"-N", "--nprocs", "nprocs",
2021-08-23 21:54:40 +05:00
type = click.INT,
default = 1,
help = "Count of parallel processes"
)
@click.option(
"-f", "--force", "force",
is_flag = True,
default = False,
help = "Overwrite existing entries"
)
2021-08-23 21:54:40 +05:00
@click.option(
2021-09-07 14:08:04 +05:00
"-p", "--params", "params",
2021-08-23 21:54:40 +05:00
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"])
2021-08-12 20:00:30 +05:00
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()))
2021-08-24 17:30:42 +05:00
###
# 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"]))
2021-08-24 17:30:42 +05:00
###
# Wrap function
##
def computeCase(type, direction, theta):
2021-08-24 17:30:42 +05:00
case = Anisotropy()
case.db = database
2021-08-24 17:30:42 +05:00
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)
2021-08-24 17:30:42 +05:00
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 ...")
2021-08-24 17:30:42 +05:00
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)
2021-08-24 17:30:42 +05:00
if out: logger.info(out)
if err: logger.error(err)
if returncode:
logger.error("Flow computation failed.")
return
else:
logger.info("Flow exists. Skipping ...")
2021-08-23 21:54:40 +05:00
if stage in ["postProcessing", "all"]:
case.load(type, direction, theta)
2021-08-11 23:17:43 +05:00
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.")
2021-08-23 21:54:40 +05:00
###
# Run
##
2021-08-24 17:30:42 +05:00
if nprocs == 1:
for pos, qarg in enumerate(queueargs):
computeCase(*qarg)
else:
parallel(nprocs, queueargs, computeCase)
2021-08-12 20:00:30 +05:00
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()
2021-08-23 21:54:40 +05:00
@anisotropy.command(
help = "! Internal command"
2021-08-23 21:54:40 +05:00
)
@click.argument("root")
2021-08-11 23:17:43 +05:00
@click.argument("type")
@click.argument("direction")
2021-08-11 23:17:43 +05:00
@click.argument("theta")
@click.argument("path")
def computemesh(root, type, direction, theta, path):
2021-08-23 21:54:40 +05:00
# ISSUE: can't hide command from help, 'hidden = True' doesn't work
# [Salome Environment]
2021-08-11 23:17:43 +05:00
###
# Args
##
2021-08-11 23:17:43 +05:00
direction = [ float(num) for num in direction[1:-1].split(" ") if num ]
theta = float(theta)
###
# Modules
##
2021-08-11 23:17:43 +05:00
import os, sys
pyversion = "{}.{}".format(3, 9) #(*sys.version_info[:2])
sys.path.extend([
root,
2021-09-07 14:08:04 +05:00
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)
2021-08-11 23:17:43 +05:00
model.load(type, direction, theta)
salome.salome_init()
model.genmesh(path)
salome.salome_close()
@anisotropy.command()
@click.option(
2021-09-07 14:08:04 +05:00
"-p", "--params", "params",
metavar = "key=value",
multiple = True,
cls = KeyValueOption,
2021-09-07 14:08:04 +05:00
help = "Select by control parameters (type, direction, theta)"
)
@click.option(
"-P", "--path", "path",
2021-09-07 14:08:04 +05:00
metavar = "PATH",
default = os.getcwd(),
help = "Specify directory to use (instead of cwd)"
)
@click.option(
"--list", "printlist",
is_flag = True,
help = "Print a list of avaliable fields."
)
@click.option(
"--export",
metavar = "PATH",
2021-09-07 14:08:04 +05:00
help = "Export output."
)
@click.option(
"--fields", "fields",
metavar = "f1,f2,...",
multiple = True,
cls = CliListOption,
help = "Select fields to use."
)
@click.argument(
2021-09-07 14:08:04 +05:00
"output",
required = False,
type = click.Choice(["cli", "plot"]),
default = "cli"
)
2021-09-07 14:08:04 +05:00
def show(params, path, printlist, export, fields, output):
from anisotropy import env
from anisotropy.core.database import Database, Structure
2021-09-07 14:08:04 +05:00
from pandas import DataFrame, Series
import matplotlib.pyplot as plt
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)
###
db = Database(env["db_name"], env["db_path"])
db.setup()
searchargs = []
if args.get("type"):
searchargs.append(Structure.type == args["type"])
if args.get("direction"):
searchargs.append(Structure.direction == str(args["direction"]))
if args.get("theta"):
searchargs.append(Structure.theta == args["theta"])
result = db.search(searchargs)
result.sort(key = lambda src: f"{ src['type'] }{ src['direction'] }{ src['theta'] }")
df = DataFrame(result)
df_keys = [ key for key in df.keys() ]
if printlist:
click.echo("Avaliable fields for query:")
click.echo("\t{}".format("\n\t".join(df_keys)))
return
if not result:
click.echo("Empty result.")
return
2021-09-07 14:08:04 +05:00
tables = []
if fields:
2021-09-07 14:08:04 +05:00
for fieldslist in fields:
for field in fieldslist:
if field not in df_keys:
click.echo(f"Unknown field '{ field }'. Try to use '--list' flag to see all avaliable fields.")
2021-09-07 14:08:04 +05:00
return
2021-09-07 14:08:04 +05:00
tables.append(df[fieldslist])
else:
tables.append(df)
if output == "plot":
fig, ax = plt.subplots(nrows = 1, ncols = 1)
2021-09-07 14:08:04 +05:00
for table in tables:
table.plot(table.keys()[0], table.keys()[1], ax = ax, style = "o")
2021-09-07 14:08:04 +05:00
plt.legend()
plt.grid()
2021-09-07 14:08:04 +05:00
if export:
2021-09-07 14:08:04 +05:00
supported = ["csv", "jpg"]
filepath, ext = os.path.splitext(export)
ext = ext.replace(".", "")
if ext not in supported:
click.echo(f"Unknown extension '{ ext }'.")
return
if ext == "csv":
if len(tables) == 1:
tables[0].to_csv(export, sep = ";")
else:
for n, table in enumerate(tables):
table.to_csv("{}.{}.{}".format(filepath, n, ext), sep = ";")
elif ext == "jpg":
plt.savefig(export)
else:
2021-09-07 14:08:04 +05:00
if output == "cli":
res = "\n\n".join([ table.to_string() for table in tables ])
click.echo(res)
elif output == "plot":
plt.show()
###
# CLI entry
##
2021-08-11 23:17:43 +05:00
if __name__ == "__main__":
#try:
anisotropy()
#except KeyboardInterrupt:
# 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)