Skip to content

Commit 823e6a4

Browse files
committed
Add marimo subcommand (arras-energy#269)
Signed-off-by: David P. Chassin <david.chassin@me.com>
1 parent 093d128 commit 823e6a4

File tree

5 files changed

+579
-0
lines changed

5 files changed

+579
-0
lines changed

subcommands/Makefile.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ bin_SCRIPTS += subcommands/gridlabd-library
1515
bin_SCRIPTS += subcommands/gridlabd-loaddata
1616
bin_SCRIPTS += subcommands/gridlabd-lock
1717
bin_SCRIPTS += subcommands/gridlabd-manual
18+
bin_SCRIPTS += subcommands/gridlabd-marimo
1819
bin_SCRIPTS += subcommands/gridlabd-matrix
1920
bin_SCRIPTS += subcommands/gridlabd-model
2021
bin_SCRIPTS += subcommands/gridlabd-openfido

subcommands/gridlabd-marimo

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/bin/bash
2+
## Syntax: gridlabd marimo [OPTIONS ...] [COMMAND] [NOTEBOOK]
3+
##
4+
## Options:
5+
##
6+
## `--debug`: send log to /dev/stderr
7+
##
8+
## `-h|--help|help`: get this help
9+
##
10+
## `--pid`: get the process id for the notebook (if any)
11+
##
12+
## `--url`: get the URL for the notebook (if any)
13+
##
14+
## `--verbose`: echo script to stderr
15+
##
16+
## `--wait`: wait for marimo server to exit
17+
##
18+
## Commands:
19+
##
20+
## `index`: get list of available notebooks that can be downloaded
21+
##
22+
## `list`: list open marimo notebooks
23+
##
24+
## `stop`: stops the marimo notebook
25+
##
26+
## Run a gridlabd marimo notebook.
27+
##
28+
## GridLAB-D marimo notebooks are distributed from the GitHub arras-energy
29+
## repository `marimo`.
30+
##
31+
## See also:
32+
##
33+
## * https://github.com/arras-energy/marimo
34+
## * https://docs.marimo.io/
35+
##
36+
37+
LIB=$GLD_ETC/marimo
38+
LOG=$LIB/marimo.log
39+
URL=https://raw.githubusercontent.com/arras-energy/marimo/refs/heads/main/source
40+
WAIT=no
41+
42+
E_OK=0
43+
E_SYNTAX=1
44+
E_FAIL=2
45+
E_NOTFOUND=3
46+
E_MISSING=4
47+
48+
function error()
49+
{
50+
# error MESSAGE [EXITCODE]
51+
#
52+
# Display an error message and optionally exit with EXITCODE
53+
#
54+
echo "ERROR: $1" > /dev/stderr
55+
test -z "$2" || exit $(eval 'echo $'$2)
56+
}
57+
58+
function warning()
59+
{
60+
# warning MESSAGE
61+
#
62+
# Display a warning message and optionally exit with EXITCODE
63+
#
64+
echo "WARNING: $1" > /dev/stderr
65+
}
66+
67+
function index()
68+
{
69+
# index
70+
#
71+
# Display a list of available notebooks online
72+
#
73+
curl -sL $URL/.index 2>>$LOG
74+
}
75+
76+
function getlist()
77+
{
78+
basename -s .py $(ps -ax | grep bin/marimo | grep -v grep | sed -r 's/ +/ /g' | cut -f7 -d' ') 2>>$LOG
79+
}
80+
81+
function getpid()
82+
{
83+
EXE=$LIB/$1.py
84+
ps ax | grep bin/marimo | grep $EXE | cut -f1 -d' ' 2>>$LOG
85+
}
86+
87+
function getinfo()
88+
{
89+
for PID in $*; do
90+
lsof -i -P -a -p $PID | tail -n +1 2>>$LOG
91+
done
92+
}
93+
94+
function geturl()
95+
{
96+
PID=$(getpid $1)
97+
if [ ! -z "$PID" ]; then
98+
LOC=$(lsof -i -P -a -p $PID | grep '(LISTEN)' | sed -r 's/ +/ /g' | cut -f9 -d' ' 2>>$LOG)
99+
echo "http://$LOC/"
100+
fi
101+
}
102+
103+
function download()
104+
{
105+
EXE=$LIB/$1.py
106+
ETAG=$LIB/.etag/$1
107+
if [ ! -f $EXE -a -z "$(index | grep $1)" ]; then
108+
error "no such notebook published" E_NOTFOUND
109+
fi
110+
if [ -f $ETAG ]; then
111+
curl -sL --etag-compare $ETAG --etag-save $ETAG $URL/$1.py > $EXE 2>>$LOG
112+
else
113+
curl -sL --etag-save $ETAG $URL/$1.py > $EXE 2>>$LOG
114+
fi
115+
if [ $? -ne 0 ]; then
116+
cat $EXE >>$LOG
117+
rm -f $EXE $ETAG 2>>$LOG
118+
error "unable to download '$1'" E_FAIL
119+
fi
120+
}
121+
122+
function start()
123+
{
124+
EXE=$LIB/$1.py
125+
if [ ! -f $EXE ]; then
126+
error "$1 not found" E_NOTFOUND
127+
fi
128+
if [ $WAIT == no ]; then
129+
marimo run $EXE 1>>$LOG 2>&1 &
130+
else
131+
marimo run $EXE 2>>$LOG
132+
fi
133+
}
134+
135+
function stop()
136+
{
137+
echo ""
138+
}
139+
140+
mkdir -p $LIB/.etag
141+
echo "*** COMMAND = '$0 $*' ***" >>$LOG
142+
143+
if [ $# -eq 0 ]; then
144+
grep '^## ' $0 | cut -c4-
145+
exit $E_SYNTAX
146+
fi
147+
148+
if [ -z "$GLD_ETC" ]; then
149+
error "missing gridlabd environment" E_MISSING
150+
fi
151+
152+
while [ $# -gt 0 ]; do
153+
case $1 in
154+
--debug)
155+
LOG=/dev/stderr
156+
;;
157+
--help|-h|help)
158+
cat $0 | grep '^## ' | cut -c4-
159+
exit $E_OK
160+
;;
161+
--pid)
162+
getpid $2
163+
shift 1
164+
;;
165+
--url)
166+
geturl $2
167+
shift 1
168+
;;
169+
--verbose)
170+
set -x
171+
;;
172+
--wait)
173+
WAIT=yes
174+
;;
175+
index) # list of available apps
176+
index
177+
;;
178+
list) # list of active apps
179+
getlist
180+
;;
181+
stop)
182+
PID=$(getpid $2)
183+
if [ ! -z "$PID" ]; then
184+
kill $PID
185+
fi
186+
shift 1
187+
;;
188+
-*)
189+
error "option '$1' is invalid" E_SYNTAX
190+
;;
191+
*)
192+
LOC=$(geturl $1)
193+
if [ -z "$LOC" ]; then
194+
download $1
195+
start $1
196+
else
197+
open $LOC
198+
fi
199+
;;
200+
esac
201+
shift 1
202+
done

tools/Makefile.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ dist_pkgdata_DATA += tools/framework.py
1616
dist_pkgdata_DATA += tools/gldserver.py
1717
dist_pkgdata_DATA += tools/gridlabd-editor.png
1818
dist_pkgdata_DATA += tools/gridlabd-editor.py
19+
dist_pkgdata_DATA += tools/gridlabd_model.py
20+
dist_pkgdata_DATA += tools/gridlabd_runner.py
1921
dist_pkgdata_DATA += tools/group.py
2022
dist_pkgdata_DATA += tools/insights.py
2123
dist_pkgdata_DATA += tools/install.py

tools/gridlabd_model.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""GridLAB-D JSON file accessor
2+
3+
"""
4+
5+
import os, sys
6+
import json
7+
import re
8+
import io
9+
from gridlabd_runner import gridlabd
10+
import pandas as pd
11+
12+
VERSION = "4.3.1" # oldest version supported by this module
13+
ENCODING = "utf-8" # data encode to use for JSON files
14+
15+
class GridlabdModelException(Exception):
16+
pass
17+
18+
class GridlabdModel:
19+
20+
def __init__(self,
21+
data = None, # GLM filename, JSON data, or JSON filename
22+
name = None, # filename to use (overrides default filename)
23+
force = False, # force overwrite of existing JSON
24+
initialize = False, # initialize GLM first
25+
):
26+
27+
if name:
28+
self.filename = name
29+
30+
if data is None: # new file
31+
if not name:
32+
self.autoname(force)
33+
gridlabd("-o",self.filename)
34+
self.read_json(self.filename)
35+
36+
elif type(data) is str and os.path.splitext(data)[1] == ".glm": # glm file
37+
38+
self.read_glm(data,force=force,initialize=initialize)
39+
40+
elif type(data) is str and os.path.splitext(data)[1] == ".json": # json file
41+
42+
self.read_json(data)
43+
44+
else: # raw data
45+
46+
if not name:
47+
self.autoname(force)
48+
self.load(data)
49+
50+
def autoname(self,force=False):
51+
"""Generate a new filename
52+
53+
Arguments:
54+
- force (bool): use the first name generate regardless of whether the file exists
55+
"""
56+
num = 0
57+
def _name(num):
58+
return f"untitled-{num}.json"
59+
while os.path.exists(_name(num)) and not force:
60+
num += 1
61+
self.filename = _name(num)
62+
63+
def read_glm(self,filename,force=True,initialize=False):
64+
"""Read GLM file
65+
66+
Arguments:
67+
- filename (str): name of GLM file to read
68+
- force (bool): force overwrite of JSON
69+
- initialize (bool): initialize GLM first
70+
"""
71+
with open(filename,"r") as fh:
72+
self.filename = filename
73+
self.load(data)
74+
75+
def read_json(self,filename):
76+
"""Read JSON file
77+
78+
Arguments:
79+
- filename (str): name of JSON file to read
80+
"""
81+
with open(filename,"r") as fh:
82+
data = json.load(fh)
83+
self.filename = filename
84+
self.load(data)
85+
86+
def load(self,data):
87+
"""Load data from dictionary
88+
89+
Arguments:
90+
- data (dict|str|bytes): data to load (must be a valid GLM model)
91+
"""
92+
if type(data) is bytes:
93+
data = data.decode(ENCODING)
94+
if type(data) is str:
95+
if data[0] in ["[","{"]:
96+
data = json.loads(data)
97+
else:
98+
data = json.loads(gridlabd("-C",".glm","-o","-json",binary=True,source=io.StringIO(data)))
99+
print(data)
100+
if not type(data) is dict:
101+
raise GridlabdModelException("data is not a dictionary")
102+
if data["application"] != "gridlabd":
103+
raise GridlabdModelException("data is not a gridlabd model")
104+
if data["version"] < VERSION:
105+
raise GridlabdModelException("data is from an outdated version of gridlabd")
106+
for key,values in data.items():
107+
setattr(self,key,values)
108+
self.is_modified = False
109+
110+
def get_objects(self,classes=None,as_type=dict,**kwargs):
111+
"""Find objects belonging to specified classes
112+
113+
Arguments:
114+
- classes (str|list of str): patterns of class names to search (default is '.*')
115+
- as_type (class): type to use for return value
116+
- kwargs (dict): arguments for as_type
117+
118+
Return:
119+
- as_type: object data
120+
"""
121+
if type(classes) is str:
122+
match = classes.split(",")
123+
else:
124+
match = []
125+
for y in classes if classes else '.*':
126+
match.extend([x for x in self.classes if re.match(y,x)])
127+
data = dict([(obj,data) for obj,data in self.objects.items() if data["class"] in match])
128+
return as_type(data,**kwargs)
129+
130+
def rename(self,name):
131+
self.filename = name
132+
self.is_modified = True
133+
134+
if __name__ == "__main__":
135+
136+
import unittest
137+
138+
if not os.path.exists("/tmp/13.glm"):
139+
gridlabd("model","get","IEEE/13")
140+
os.system("mv 13.glm /tmp")
141+
gridlabd("-C","/tmp/13.glm","-o","/tmp/13.json")
142+
143+
class TestModel(unittest.TestCase):
144+
145+
# def test_glm(self):
146+
# glm = GridlabdModel("/tmp/13.glm",force=True)
147+
# loads = glm.get_objects(classes=[".*load"])
148+
# self.assertEqual(len(loads),10)
149+
150+
# def test_json(self):
151+
# glm = GridlabdModel("/tmp/13.json")
152+
# loads = glm.get_objects(classes=["load","triplex_load"])
153+
# self.assertEqual(len(loads),10)
154+
155+
# def test_new(self):
156+
157+
# glm = GridlabdModel("/tmp/test.json",force=True)
158+
# self.assertEqual(glm.objects,{})
159+
# self.assertEqual(glm.classes,{})
160+
161+
def test_str_glm(self):
162+
with open("/tmp/13.glm","r") as fh:
163+
data = fh.read()
164+
glm = GridlabdModel(data)
165+
loads = glm.get_objects(classes=["load","triplex_load"])
166+
self.assertEqual(len(loads),10)
167+
168+
# def test_str_json(self):
169+
# with open("/tmp/13.json","r") as fh:
170+
# data = json.load(fh)
171+
# glm = GridlabdModel(data)
172+
# loads = glm.get_objects(classes=["load","triplex_load"])
173+
# self.assertEqual(len(loads),10)
174+
175+
unittest.main()

0 commit comments

Comments
 (0)