From 54494f6335de3258531be9eeed79496d724449b4 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 21 Sep 2023 18:20:55 -0700 Subject: [PATCH] Add create_childs tool (#1335) Signed-off-by: David P. Chassin --- geodata/geodata_powerline.py | 2 +- tools/Makefile.mk | 15 +-- tools/create_childs.py | 186 +++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 tools/create_childs.py diff --git a/geodata/geodata_powerline.py b/geodata/geodata_powerline.py index 42e88710e..4df6b9a0a 100644 --- a/geodata/geodata_powerline.py +++ b/geodata/geodata_powerline.py @@ -196,7 +196,7 @@ def TODO(value=float('nan')): } default_config = { - "cabletype_file" : f"{GLD_ETC}/gridlabd/geodata_powerline_cabletypes.csv", + "cabletype_file" : f"{GLD_ETC}/geodata_powerline_cabletypes.csv", } OPTIONS = default_options diff --git a/tools/Makefile.mk b/tools/Makefile.mk index 89cefc030..ce50f2058 100644 --- a/tools/Makefile.mk +++ b/tools/Makefile.mk @@ -1,14 +1,16 @@ +dist_pkgdata_DATA += tools/create_childs.py dist_pkgdata_DATA += tools/create_ductbank.py dist_pkgdata_DATA += tools/create_filter.py -dist_pkgdata_DATA += tools/create_player.py dist_pkgdata_DATA += tools/create_meters.py +dist_pkgdata_DATA += tools/create_player.py dist_pkgdata_DATA += tools/create_poles.py dist_pkgdata_DATA += tools/create_schedule.py dist_pkgdata_DATA += tools/eia_recs.py dist_pkgdata_DATA += tools/find_location.py -dist_pkgdata_DATA += tools/fire_report.py dist_pkgdata_DATA += tools/fire_danger.py +dist_pkgdata_DATA += tools/fire_report.py dist_pkgdata_DATA += tools/fit_filter.py +dist_pkgdata_DATA += tools/gldserver.py dist_pkgdata_DATA += tools/gridlabd-editor.png dist_pkgdata_DATA += tools/gridlabd-editor.py dist_pkgdata_DATA += tools/group.py @@ -16,13 +18,12 @@ dist_pkgdata_DATA += tools/insights.py dist_pkgdata_DATA += tools/install.py dist_pkgdata_DATA += tools/isone.py dist_pkgdata_DATA += tools/market_data.py -dist_pkgdata_DATA += tools/mdb_info.py dist_pkgdata_DATA += tools/market_model.py -dist_pkgdata_DATA += tools/meteostat_weather.py +dist_pkgdata_DATA += tools/mdb_info.py dist_pkgdata_DATA += tools/metar2glm.py -dist_pkgdata_DATA += tools/read_dlp.py -dist_pkgdata_DATA += tools/gldserver.py +dist_pkgdata_DATA += tools/meteostat_weather.py dist_pkgdata_DATA += tools/noaa_forecast.py dist_pkgdata_DATA += tools/nsrdb_weather.py -dist_pkgdata_DATA += tools/ucar_weather.py dist_pkgdata_DATA += tools/pole_analysis.py +dist_pkgdata_DATA += tools/read_dlp.py +dist_pkgdata_DATA += tools/ucar_weather.py diff --git a/tools/create_childs.py b/tools/create_childs.py new file mode 100644 index 000000000..c9f8bc9af --- /dev/null +++ b/tools/create_childs.py @@ -0,0 +1,186 @@ +# Syntax: create_childs [-i|--input=INPUTFILE] [-o|--output=GLMFILE] [OPTIONS ...] +"""Syntax: create_childs [-i|--input=INPUTFILE] [-o|--output=GLMFILE] [OPTIONS ...] + +Options +------- + -P|--parents=NAME:VALUE,... specify parent property pattern to match (required) + -C|--childs=NAME:VALUE,... specify child property list to assign (required) + -N|--names=STRING specify object naming convention (default is '{class}_{name}') + -M|--modules=NAME,... specify module names to use (defaults to those found) + +Description +----------- + +The `create_childs` tool adds child objects to all objects that match the +parent object pattern specified. + +Parent patterns and child properties as specified as a comma-separate list of +`NAME:VALUE` strings, e.g., `class:node` or `nominal_voltage:2.4kV`. Parent +patterns use `regex` pattern matching. Child properties may include `{NAME}` +format strings where `NAME` is a property of the parent object. This +allows copying of values from the parent object. This formatting also can be +applied to the naming string, e.g., `-N='{name}_L' to append '_L' to the +parent object name. + +Example +------- + +The following creates a GLM file containing a `triplex_load` objects attached +to `triplex_node` objects with names starting as `N_` in the file `my-network.json`: + +~~~ +$ gridlabd create_childs -i=my-network.json -o=loads.glm -P='class:triplex_node,name:^N_' -C='class:triplex_load,nominal_voltage:{nominal_voltage},phases:{phases},constant_power_B:1.2+0.1jkVA' +~~~ +""" + +import sys, os +import json +import re +import datetime +import subprocess +import random + +EXENAME = os.path.splitext(os.path.basename(sys.argv[0]))[0] + +DEBUG = False +WARNING = True +QUIET = False +VERBOSE = False + +E_OK = 0 +E_INVALID = 1 +E_FAILED = 2 +E_SYNTAX = 8 +E_EXCEPTION = 9 +EXITCODE = E_OK + +class GldException(Exception): + pass + +def error(msg,code=None): + if type(code) is int: + global EXITCODE + EXITCODE = code + if DEBUG: + raise GldException(msg) + print("ERROR [create_childs]:",msg,file=sys.stderr) + exit(code) + + +def load(): + + if not INPUTFILE.endswith(".json"): + tmpfile = "." + while os.path.exists(tmpfile): + tmpfile = f"tmp{hex(random.randint(1e30,1e31))[2:]}.json" + try: + result = subprocess.run(["gridlabd","-C",INPUTFILE,"-o",tmpfile]) + assert(result.returncode==0) + with open(tmpfile,"r") as fh: + model = json.load(fh) + except: + raise + finally: + os.remove(tmpfile) + pass + else: + with open(INPUTFILE,"r") as fh: + model = json.load(fh) + return model + +def save(fh): + print(f"// generated by '{' '.join(sys.argv)}' at {datetime.datetime.now()}",file=fh) + for name in MODULES: + print(f"module {name};",file=fh) + classname = CHILDS["class"] + for obj,data in OBJECTS.items(): + print(f"object {classname} {{",file=fh) + for prop,value in data.items(): + print(f" {prop} \"{value}\";",file=fh) + print("}",file=fh) + +def main(): + + PATTERN = {} + for name,pattern in PARENTS.items(): + PATTERN[name] = re.compile(pattern) + + if "class" not in CHILDS: + error("you must include a class name in the child properties",E_INVALID) + classname = CHILDS["class"] + model = load() + assert(model['application']=='gridlabd') + global MODULES + if not MODULES: + MODULES = list(model['modules']) + + for obj,data in model['objects'].items(): + data['name'] = obj + ok = True + for name,pattern in PATTERN.items(): + if not pattern.match(data[name]): + ok = False + break + if ok: + name = f"{classname}_{obj}" if NAMING is None else NAMING.format(**data) + OBJECTS[name] = dict(parent=obj,name=name) + for prop,value in CHILDS.items(): + if not prop in ["class"]: + OBJECTS[name][prop] = value.format(**data) + + if OUTPUTFILE.endswith(".glm"): + with open(OUTPUTFILE,"w") as fh: + save(fh) + else: + error("invalid output file format") + + return E_OK + +INPUTFILE = "/dev/stdin" +OUTPUTFILE = "/dev/stdout" +PARENTS = None +CHILDS = None +NAMING = None +OBJECTS = {} +MODULES = [] + +if __name__ == "__main__": + + if len(sys.argv) == 1: + print(__doc__.split('\n')[0],file=sys.stderr) + exit(E_SYNTAX) + + for arg in sys.argv[1:]: + spec = arg.split("=") + if len(spec) == 1: + tag = arg + value = None + else: + tag = spec[0] + value = '='.join(spec[1:]) + + if tag in ["-h","--help","help"]: + print(__doc__) + exit(E_OK) + if tag in ["-i","--input"]: + INPUTFILE = value if value else "/dev/stdin" + elif tag in ["-o","--output"]: + OUTPUTFILE = value if value else "/dev/stdout" + elif tag in ["-P","--parent"]: + PARENTS = dict([x.split(":") for x in value.split(",")]) + elif tag in ["-C","--childs"]: + CHILDS = dict([x.split(":") for x in value.split(",")]) + elif tag in ["-N","--names"]: + NAMING = value + elif tag in ["-M","--modules"]: + MODULES = value.split(",") + else: + error(f"option '{arg}' is invalid",E_INVALID) + + if PARENTS is None: + error("you must specify the parent patterns to match") + if CHILDS is None: + error("you must specify the child properties to define") + + EXITCODE = main() + exit(EXITCODE)