Skip to content

Add marimo subcommand #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions subcommands/Makefile.mk
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ bin_SCRIPTS += subcommands/gridlabd-library
bin_SCRIPTS += subcommands/gridlabd-loaddata
bin_SCRIPTS += subcommands/gridlabd-lock
bin_SCRIPTS += subcommands/gridlabd-manual
bin_SCRIPTS += subcommands/gridlabd-marimo
bin_SCRIPTS += subcommands/gridlabd-matrix
bin_SCRIPTS += subcommands/gridlabd-model
bin_SCRIPTS += subcommands/gridlabd-openfido
Expand Down
202 changes: 202 additions & 0 deletions subcommands/gridlabd-marimo
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/bin/bash
## Syntax: gridlabd marimo [OPTIONS ...] [COMMAND] [NOTEBOOK]
##
## Options:
##
## `--debug`: send log to /dev/stderr
##
## `-h|--help|help`: get this help
##
## `--pid`: get the process id for the notebook (if any)
##
## `--url`: get the URL for the notebook (if any)
##
## `--verbose`: echo script to stderr
##
## `--wait`: wait for marimo server to exit
##
## Commands:
##
## `index`: get list of available notebooks that can be downloaded
##
## `list`: list open marimo notebooks
##
## `stop`: stops the marimo notebook
##
## Run a gridlabd marimo notebook.
##
## GridLAB-D marimo notebooks are distributed from the GitHub arras-energy
## repository `marimo`.
##
## See also:
##
## * https://github.com/arras-energy/marimo
## * https://docs.marimo.io/
##

LIB=$GLD_ETC/marimo
LOG=$LIB/marimo.log
URL=https://raw.githubusercontent.com/arras-energy/marimo/refs/heads/main/source
WAIT=no

E_OK=0
E_SYNTAX=1
E_FAIL=2
E_NOTFOUND=3
E_MISSING=4

function error()
{
# error MESSAGE [EXITCODE]
#
# Display an error message and optionally exit with EXITCODE
#
echo "ERROR: $1" > /dev/stderr
test -z "$2" || exit $(eval 'echo $'$2)
}

function warning()
{
# warning MESSAGE
#
# Display a warning message and optionally exit with EXITCODE
#
echo "WARNING: $1" > /dev/stderr
}

function index()
{
# index
#
# Display a list of available notebooks online
#
curl -sL $URL/.index 2>>$LOG
}

function getlist()
{
basename -s .py $(ps -ax | grep bin/marimo | grep -v grep | sed -r 's/ +/ /g' | cut -f7 -d' ') 2>>$LOG
}

function getpid()
{
EXE=$LIB/$1.py
ps ax | grep bin/marimo | grep $EXE | cut -f1 -d' ' 2>>$LOG
}

function getinfo()
{
for PID in $*; do
lsof -i -P -a -p $PID | tail -n +1 2>>$LOG
done
}

function geturl()
{
PID=$(getpid $1)
if [ ! -z "$PID" ]; then
LOC=$(lsof -i -P -a -p $PID | grep '(LISTEN)' | sed -r 's/ +/ /g' | cut -f9 -d' ' 2>>$LOG)
echo "http://$LOC/"
fi
}

function download()
{
EXE=$LIB/$1.py
ETAG=$LIB/.etag/$1
if [ ! -f $EXE -a -z "$(index | grep $1)" ]; then
error "no such notebook published" E_NOTFOUND
fi
if [ -f $ETAG ]; then
curl -sL --etag-compare $ETAG --etag-save $ETAG $URL/$1.py > $EXE 2>>$LOG
else
curl -sL --etag-save $ETAG $URL/$1.py > $EXE 2>>$LOG
fi
if [ $? -ne 0 ]; then
cat $EXE >>$LOG
rm -f $EXE $ETAG 2>>$LOG
error "unable to download '$1'" E_FAIL
fi
}

function start()
{
EXE=$LIB/$1.py
if [ ! -f $EXE ]; then
error "$1 not found" E_NOTFOUND
fi
if [ $WAIT == no ]; then
marimo run $EXE 1>>$LOG 2>&1 &
else
marimo run $EXE 2>>$LOG
fi
}

function stop()
{
echo ""
}

mkdir -p $LIB/.etag
echo "*** COMMAND = '$0 $*' ***" >>$LOG

if [ $# -eq 0 ]; then
grep '^## ' $0 | cut -c4-
exit $E_SYNTAX
fi

if [ -z "$GLD_ETC" ]; then
error "missing gridlabd environment" E_MISSING
fi

while [ $# -gt 0 ]; do
case $1 in
--debug)
LOG=/dev/stderr
;;
--help|-h|help)
cat $0 | grep '^## ' | cut -c4-
exit $E_OK
;;
--pid)
getpid $2
shift 1
;;
--url)
geturl $2
shift 1
;;
--verbose)
set -x
;;
--wait)
WAIT=yes
;;
index) # list of available apps
index
;;
list) # list of active apps
getlist
;;
stop)
PID=$(getpid $2)
if [ ! -z "$PID" ]; then
kill $PID
fi
shift 1
;;
-*)
error "option '$1' is invalid" E_SYNTAX
;;
*)
LOC=$(geturl $1)
if [ -z "$LOC" ]; then
download $1
start $1
else
open $LOC
fi
;;
esac
shift 1
done
2 changes: 2 additions & 0 deletions tools/Makefile.mk
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dist_pkgdata_DATA += tools/framework.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/gridlabd_model.py
dist_pkgdata_DATA += tools/gridlabd_runner.py
dist_pkgdata_DATA += tools/group.py
dist_pkgdata_DATA += tools/insights.py
dist_pkgdata_DATA += tools/install.py
Expand Down
175 changes: 175 additions & 0 deletions tools/gridlabd_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""GridLAB-D JSON file accessor

"""

import os, sys
import json
import re
import io
from gridlabd_runner import gridlabd
import pandas as pd

VERSION = "4.3.1" # oldest version supported by this module
ENCODING = "utf-8" # data encode to use for JSON files

class GridlabdModelException(Exception):
pass

class GridlabdModel:

def __init__(self,
data = None, # GLM filename, JSON data, or JSON filename
name = None, # filename to use (overrides default filename)
force = False, # force overwrite of existing JSON
initialize = False, # initialize GLM first
):

if name:
self.filename = name

if data is None: # new file
if not name:
self.autoname(force)
gridlabd("-o",self.filename)
self.read_json(self.filename)

elif type(data) is str and os.path.splitext(data)[1] == ".glm": # glm file

self.read_glm(data,force=force,initialize=initialize)

elif type(data) is str and os.path.splitext(data)[1] == ".json": # json file

self.read_json(data)

else: # raw data

if not name:
self.autoname(force)
self.load(data)

def autoname(self,force=False):
"""Generate a new filename

Arguments:
- force (bool): use the first name generate regardless of whether the file exists
"""
num = 0
def _name(num):
return f"untitled-{num}.json"
while os.path.exists(_name(num)) and not force:
num += 1
self.filename = _name(num)

def read_glm(self,filename,force=True,initialize=False):
"""Read GLM file

Arguments:
- filename (str): name of GLM file to read
- force (bool): force overwrite of JSON
- initialize (bool): initialize GLM first
"""
with open(filename,"r") as fh:
self.filename = filename
self.load(data)

def read_json(self,filename):
"""Read JSON file

Arguments:
- filename (str): name of JSON file to read
"""
with open(filename,"r") as fh:
data = json.load(fh)
self.filename = filename
self.load(data)

def load(self,data):
"""Load data from dictionary

Arguments:
- data (dict|str|bytes): data to load (must be a valid GLM model)
"""
if type(data) is bytes:
data = data.decode(ENCODING)
if type(data) is str:
if data[0] in ["[","{"]:
data = json.loads(data)
else:
data = json.loads(gridlabd("-C",".glm","-o","-json",binary=True,source=io.StringIO(data)))
print(data)
if not type(data) is dict:
raise GridlabdModelException("data is not a dictionary")
if data["application"] != "gridlabd":
raise GridlabdModelException("data is not a gridlabd model")
if data["version"] < VERSION:
raise GridlabdModelException("data is from an outdated version of gridlabd")
for key,values in data.items():
setattr(self,key,values)
self.is_modified = False

def get_objects(self,classes=None,as_type=dict,**kwargs):
"""Find objects belonging to specified classes

Arguments:
- classes (str|list of str): patterns of class names to search (default is '.*')
- as_type (class): type to use for return value
- kwargs (dict): arguments for as_type

Return:
- as_type: object data
"""
if type(classes) is str:
match = classes.split(",")
else:
match = []
for y in classes if classes else '.*':
match.extend([x for x in self.classes if re.match(y,x)])
data = dict([(obj,data) for obj,data in self.objects.items() if data["class"] in match])
return as_type(data,**kwargs)

def rename(self,name):
self.filename = name
self.is_modified = True

if __name__ == "__main__":

import unittest

if not os.path.exists("/tmp/13.glm"):
gridlabd("model","get","IEEE/13")
os.system("mv 13.glm /tmp")
gridlabd("-C","/tmp/13.glm","-o","/tmp/13.json")

class TestModel(unittest.TestCase):

# def test_glm(self):
# glm = GridlabdModel("/tmp/13.glm",force=True)
# loads = glm.get_objects(classes=[".*load"])
# self.assertEqual(len(loads),10)

# def test_json(self):
# glm = GridlabdModel("/tmp/13.json")
# loads = glm.get_objects(classes=["load","triplex_load"])
# self.assertEqual(len(loads),10)

# def test_new(self):

# glm = GridlabdModel("/tmp/test.json",force=True)
# self.assertEqual(glm.objects,{})
# self.assertEqual(glm.classes,{})

def test_str_glm(self):
with open("/tmp/13.glm","r") as fh:
data = fh.read()
glm = GridlabdModel(data)
loads = glm.get_objects(classes=["load","triplex_load"])
self.assertEqual(len(loads),10)

# def test_str_json(self):
# with open("/tmp/13.json","r") as fh:
# data = json.load(fh)
# glm = GridlabdModel(data)
# loads = glm.get_objects(classes=["load","triplex_load"])
# self.assertEqual(len(loads),10)

unittest.main()
Loading
Loading