diff --git a/clingraph/clingo_utils.py b/clingraph/clingo_utils.py index 0c108d9..c13a68b 100644 --- a/clingraph/clingo_utils.py +++ b/clingraph/clingo_utils.py @@ -1,18 +1,21 @@ """ Functions used for the clingo integration """ + import json import logging import jsonschema +import base64 from clingo.control import Control from clingo.script import enable_python -from clingo.symbol import String +from clingo.symbol import String, SymbolType from jsonschema import validate from .orm import Factbase from .exceptions import InvalidSyntaxJSON, InvalidSyntax enable_python() -log = logging.getLogger('custom') +log = logging.getLogger("custom") + class ClingraphContext: """ @@ -20,7 +23,7 @@ class ClingraphContext: passed in the command line via option `--viz-encoding` """ - def pos(self, x,y,scale=1): + def pos(self, x, y, scale=1): """ Position in the form of a tuple @@ -31,8 +34,8 @@ def pos(self, x,y,scale=1): (clingo.Symbol.String) position as a string of form (x,y)! """ scale = float(str(scale).strip('"')) - x = float(str(x))*scale - y = float(str(y))*scale + x = float(str(x)) * scale + y = float(str(y)) * scale return String(f"{x},{y}!") def concat(self, *args): @@ -44,7 +47,7 @@ def concat(self, *args): Returns: (clingo.Symbol.String) The string concatenating all symbols """ - return String(''.join([str(x).strip('"') for x in args])) + return String("".join([str(x).strip('"') for x in args])) def format(self, s, *args): """ @@ -52,11 +55,19 @@ def format(self, s, *args): Args: s (clingo.Symbol.String): The string to format, for example "{0} and {1}" - args: All symbols that can be accessed by the position starting in 0 + args: All symbols that can be accessed by the position starting in 0. + If there is a single tuple as an argument, then its arguments are considered one by one. Returns: (clingo.Symbol.String) The string concatenating all symbols """ - args_str = [str(v).strip('"') for v in args] + if ( + len(args) == 1 + and args[0].type == SymbolType.Function + and args[0].name == "" + ): + args_str = [str(v).strip('"') for v in args[0].arguments] + else: + args_str = [str(v).strip('"') for v in args] return String(s.string.format(*args_str)) def stringify(self, s, capitalize=False): @@ -69,7 +80,7 @@ def stringify(self, s, capitalize=False): (clingo.Symbol.String) The string """ val = str(s).strip('"') - val = val.replace('_',' ') + val = val.replace("_", " ") if capitalize: val = val[0].upper() + val[1:] return String(val) @@ -84,8 +95,7 @@ def cluster(self, s): (clingo.Symbol.String) The string with the cluster name """ val = str(s).strip('"') - return String("cluster_"+val) - + return String("cluster_" + val) def html_escape(self, s): """ @@ -98,11 +108,26 @@ def html_escape(self, s): """ return String( - str(s).strip('"') - .replace('&', '&') - .replace('"', '"') - .replace('<', '<') - .replace('>', '>')) + str(s) + .strip('"') + .replace("&", "&") + .replace('"', """) + .replace("<", "<") + .replace(">", ">") + ) + + def decodeB64(self, s): + """ + Decodes a base 64 string + + Args: + s (clingo.Symbol.String): A string in base 64 + Returns: + (clingo.Symbol.String): The string decoded + """ + s = str(s) + decoded = base64.b64decode(s).decode("ascii") + return String(decoded) def svg_init(self, property_name, property_value): """ @@ -149,7 +174,7 @@ def svg(self, event, element, property_name, property_value): element = str(element).strip('"') property_name = str(property_name).strip('"') property_value = str(property_value).strip('"') - s=String(f"{event}___{element}___{property_name}___{property_value} ") + s = String(f"{event}___{element}___{property_name}___{property_value} ") return s def color(self, option, opacity=None): @@ -175,7 +200,7 @@ def color(self, option, opacity=None): "yellow": "#FFAB00", "danger": "#FF5630", "red": "#FF5630", - "light": "#F4F5F7" + "light": "#F4F5F7", } if option not in colors: return String("#000000") @@ -195,26 +220,25 @@ def clinguin_fontname(self): return String("Helvetica Neue") - - def __getattr__(self, name): # pylint: disable=import-outside-toplevel import __main__ + return getattr(__main__, name) clingo_json_schema = { "type": "object", - "required": ["Call","Result"], - "properties":{ + "required": ["Call", "Result"], + "properties": { "Call": { - "type" : "array", + "type": "array", }, - "Result":{ + "Result": { "type": "string", - } - } + }, + }, } @@ -235,14 +259,20 @@ def parse_clingo_json(json_str): try: j = json.loads(json_str.encode()) validate(instance=j, schema=clingo_json_schema) - if j['Result'] == 'UNSATISFIABLE': - log.warning("Passing an unsatisfiable instance in the JSON. This wont produce any results") + if j["Result"] == "UNSATISFIABLE": + log.warning( + "Passing an unsatisfiable instance in the JSON. This wont produce any results" + ) if len(j["Call"]) > 1: - log.warning("Calls will multiple theads from clingo are not supported by clingraph") + log.warning( + "Calls will multiple theads from clingo are not supported by clingraph" + ) if not "Witnesses" in j["Call"][0]: - log.warning("No Witnesses (stable models) in the JSON output, no output will be produced by clingraph") + log.warning( + "No Witnesses (stable models) in the JSON output, no output will be produced by clingraph" + ) witnesses = [] else: witnesses = j["Call"][0]["Witnesses"] @@ -255,10 +285,12 @@ def parse_clingo_json(json_str): return models_prgs except json.JSONDecodeError as e: - raise InvalidSyntax('The json can not be read.',str(e)) from None + raise InvalidSyntax("The json can not be read.", str(e)) from None except jsonschema.exceptions.ValidationError as e: - raise InvalidSyntaxJSON('The json does not have the expected structure. Make sure you used the -outf=2 option in clingo.',str(e)) from None - + raise InvalidSyntaxJSON( + "The json does not have the expected structure. Make sure you used the -outf=2 option in clingo.", + str(e), + ) from None def _get_json(args, stdin): @@ -320,14 +352,15 @@ def _get_json(args, stdin): elem.addEventListener(event, function() { console.log(event) local_event = event; - class_name = local_event + "_" + elem.id; - var children = Object.values(document.getElementsByClassName(class_name)); + elem_id = elem.id.split(' ').join(''); + class_name = local_event + "___" + elem_id; + var children = elements.filter(el => Array.from(el.classList).some(cls => cls.startsWith(class_name))); children.forEach(c => { c.classList.forEach(c_elem =>{ c_vals = c_elem.split('___') if (c_vals.length == 4){ if (c_vals[0]==local_event){ - if(c_vals[1]==elem.id){ + if(c_vals[1]==elem_id){ property = c_vals[2] property_val = c_vals[3] c.style[property]=property_val @@ -348,6 +381,7 @@ def _get_json(args, stdin): """ + def add_svg_interaction_to_string(s): """ Adds the svg interaction script to string representation of the svg image @@ -355,11 +389,12 @@ def add_svg_interaction_to_string(s): Args: s [str]: the svg string """ - s = s.replace("#111111","currentcolor") + s = s.replace("#111111", "currentcolor") s = s[:-8] - s+= SVG_SCRIPT + s += SVG_SCRIPT return s + def add_svg_interaction(paths): """ Adds the svg interaction script to a list of svg files defined in the paths. @@ -373,12 +408,13 @@ def add_svg_interaction(paths): if not path_dic: continue for path in path_dic.values(): - with open(path, 'r', encoding='UTF-8') as f: + with open(path, "r", encoding="UTF-8") as f: s = f.read() s = add_svg_interaction_to_string(s) - with open(path, 'w', encoding='UTF-8') as f: + with open(path, "w", encoding="UTF-8") as f: f.write(s) + ADD_IDS_PRG = """ #defined edge/2. #defined edge/1. @@ -394,6 +430,7 @@ def add_svg_interaction(paths): attr(graph,ID,id,ID):-graph(ID,_). """ + def add_elements_ids(ctl): """ Adds a program to the control that will set the ids of the elements to the id attribute @@ -401,41 +438,42 @@ def add_elements_ids(ctl): ctl Clingo.Control: The clingo control object that is used """ - ctl.add("base",[],ADD_IDS_PRG) + ctl.add("base", [], ADD_IDS_PRG) -def _get_fbs_from_encoding(args,stdin,prgs_from_json): +def _get_fbs_from_encoding(args, stdin, prgs_from_json): """ Obtains the factbase by running clingo to compute the stable models of a visualization encoding """ fbs = [] + def add_fb_model(m): - fbs.append(Factbase.from_model(m, - prefix=args.prefix, - default_graph=args.default_graph)) + fbs.append( + Factbase.from_model(m, prefix=args.prefix, default_graph=args.default_graph) + ) cl_args = ["-n1"] if args.seed is not None: - cl_args.append(f'--seed={args.seed}') + cl_args.append(f"--seed={args.seed}") if prgs_from_json is not None: for prg in prgs_from_json: ctl = Control(cl_args) ctl.load(args.viz_encoding.name) - ctl.add("base",[],prg) - if args.format == 'svg': + ctl.add("base", [], prg) + if args.format == "svg": add_elements_ids(ctl) - ctl.ground([("base", [])],ClingraphContext()) + ctl.ground([("base", [])], ClingraphContext()) ctl.solve(on_model=add_fb_model) else: ctl = Control(cl_args) ctl.load(args.viz_encoding.name) - ctl.add("base",[],stdin) - if args.format == 'svg': + ctl.add("base", [], stdin) + if args.format == "svg": add_elements_ids(ctl) for f in args.files: ctl.load(f.name) - ctl.ground([("base", [])],ClingraphContext()) + ctl.ground([("base", [])], ClingraphContext()) ctl.solve(on_model=add_fb_model) return fbs diff --git a/docs/clingraph/examples/color.md b/docs/clingraph/examples/color.md new file mode 100644 index 0000000..e4e3aff --- /dev/null +++ b/docs/clingraph/examples/color.md @@ -0,0 +1,37 @@ +### Graph coloring + +**Features used:** +- Clingo integration +- Multi model +- Rendering +- Model selection +- View + +```console +clingo examples/color/color.lp --outf=2 | clingraph --out=render --view --dir='out/color' --format=png --select-model=0 -log=info +``` + +![](https://raw.githubusercontent.com/potassco/clingraph/master/examples/color/default.png) + +- `color.lp` +```prolog +node(1..6). + +edge( + (1,2); (1,3); (1,4); + (2,4); (2,5); (2,6); + (3,4); (3,5); + (5,6) +). + +color(red; green; blue). + +{ assign(N, C) : color(C) } = 1 :- node(N). + +:- edge((N, M)), assign(N, C), assign(M, C). + +#show node/1. +#show edge/1. +#show attr(node, N, style, filled): node(N). +#show attr(node, N, color, C) : assign(N, C). +``` \ No newline at end of file diff --git a/docs/clingraph/examples/index.md b/docs/clingraph/examples/index.md new file mode 100644 index 0000000..8fc4be2 --- /dev/null +++ b/docs/clingraph/examples/index.md @@ -0,0 +1,7 @@ +```{include} ../../../examples/README.md +``` + +```{toctree} +color.md +``` + diff --git a/docs/clingraph/svg.rst b/docs/clingraph/svg.rst index d09e932..3af1889 100644 --- a/docs/clingraph/svg.rst +++ b/docs/clingraph/svg.rst @@ -8,8 +8,8 @@ This interactivity, will be single shot, which means that the svg file will be g **Command line** - This functionality is limited to the command line using the integration with *clingo* with the ``--viz-encoding`` parameter. - + For the interaction to work, the argument ``id`` must be provided explicitly, equal to the node/edge ID. With a rule such as ``attr(node,X,id,X):-node(X).``. + When *clingraph* is used in integration with *clingo* with the ``--viz-encoding`` without a custom prefix, these rules are automatically added for nodes and edges.   Interaction =========== @@ -74,7 +74,7 @@ API usage For API usage, the :ref:`ClingraphContext` should be provided. Additionally, the following functions must be called by hand: -- ``add_elements_ids``: with the Control object before calling clingo. +- ``add_elements_ids``: with the Control object before calling clingo. - ``add_svg_interaction``: with the svg paths after rendering. See the API Documentation of :ref:`Clingo Utils` for details. @@ -93,6 +93,12 @@ See the API Documentation of :ref:`Clingo Utils` for details. paths = render(graphs,directory ='out',format='svg') add_svg_interaction(paths) +.. warning:: + + For the interaction to work, the argument ``id`` must be provided explicitly, equal to the node/edge ID. With a rule such as ``attr(node,X,id,X):-node(X).``. + These rules are by the ``add_elements_ids`` function. If a prefix is used these rules must be added to the encoding by hand. +  + Limitations =========== diff --git a/examples/elevator/viz.lp b/examples/elevator/viz.lp index 756774b..585d92f 100644 --- a/examples/elevator/viz.lp +++ b/examples/elevator/viz.lp @@ -1,7 +1,8 @@ graph(T):-time(T). + node((X,T),T):-floor(X),time(T). -attr(graph,T,(label,1),T):-time(T). attr(graph,T,(label,0),"Time:"):-time(T). +attr(graph,T,(label,1),T):-time(T). attr(graph,T,bgcolor,"white"):-time(T). attr(node,(X,T),shape,none):-floor(X),time(T), not at(X,T). attr(node,(X,T),shape,square):-floor(X),time(T),at(X,T). @@ -13,5 +14,6 @@ attr(node,(X,T),(pos,x),0):-floor(X),time(T). attr(node,(X,T),(pos,y),X+1):-floor(X),time(T). attr(node,(X,T),fontcolor,red):-floor(X),time(T),called(X,T). attr(node,(X,T),color,green):-floor(X),time(T),served(X,T). + edge(((X,T),(X+1,T)),T):-floor(X),floor(X+1),time(T). diff --git a/examples/sudoku/README.md b/examples/sudoku/README.md index 7b84db5..3e2489f 100644 --- a/examples/sudoku/README.md +++ b/examples/sudoku/README.md @@ -9,7 +9,7 @@ - HTML labels -`clingo examples/sudoku/encoding.lp examples/sudoku/instance.lp -n 0 --outf=2 | clingraph --view --dir='out/sudoku' --format=png --out=render --prefix=viz_ --engine=neato --default-graph=sudoku --viz-encoding=examples/sudoku/viz.lp --name-format=model_{model_number}` +`clingo examples/sudoku/encoding.lp examples/sudoku/instance.lp -n 0 --outf=2 | clingraph --view --dir='out/sudoku' --format=png --out=render --engine=neato --default-graph=sudoku --viz-encoding=examples/sudoku/viz.lp --name-format=model_{model_number}` #### First stable model ![](model_0.png) diff --git a/examples/sudoku/encoding.lp b/examples/sudoku/encoding.lp index 4d8dc9e..cfa3371 100644 --- a/examples/sudoku/encoding.lp +++ b/examples/sudoku/encoding.lp @@ -1,12 +1,12 @@ -% %%%%%%%%%%%%%%%%%%%%%%%%% Encoding +%%%%%%%%%%%%%%%%%%%%%%%%% Encoding #const dim = 3. - val(1..dim*dim). pos(X,Y) :- val(X), val(Y). subgrid(X,Y,(((X-1)/dim)*dim+((Y-1)/dim))) :- pos(X,Y). + 1 { sudoku(X,Y,V) : val(V) } 1 :- pos(X,Y). :- sudoku(X,Y,V), sudoku(X',Y,V), X != X'. diff --git a/examples/sudoku/viz.lp b/examples/sudoku/viz.lp index 8ca971b..fc25dba 100644 --- a/examples/sudoku/viz.lp +++ b/examples/sudoku/viz.lp @@ -1,29 +1,15 @@ -% %%%%%%%%%%%%%%%%%%%%%%%%% Visualization -viz_graph(sudoku). +#const dim = 3. -% Name of graph -viz_attr(graph, sudoku, name, "Sudoku"). - -% Global graph attributes -viz_attr(graph, sudoku, nodesep, 1). -viz_attr(graph, sudoku, newrank, true). - -% Global node attributes -viz_attr(graph_nodes, sudoku, shape, square). -viz_attr(graph_nodes, sudoku, style, filled). -viz_attr(graph_nodes, sudoku, fillcolor, white). -viz_attr(graph_nodes, sudoku, width, "1"). -viz_attr(graph_nodes, sudoku, fontsize, 30). +%%%%%%%%%%%%%%%%%%%%%%%% Visualization +graph(sudoku). % Nodes -viz_node(pos(X,Y), sudoku):- pos(X,Y). -viz_attr(node, pos(X,Y), fillcolor, "grey88"):- pos(X,Y), subgrid(X,Y,S), S\2==0. - -viz_attr(node, pos(X,Y), label, V):- sudoku(X,Y,V), not initial(X,Y,_). -viz_attr(node, pos(X,Y), label, @format("<{}>",V)):- initial(X,Y,V). -viz_attr(node, pos(X,Y), fontcolor, gray):- sudoku(X,Y,V), not initial(X,Y,_). -viz_attr(node, pos(X,Y), pos, @pos(X,Y)):- pos(X,Y). - -% Edges to show subgrids -viz_edge((pos(X,Y),pos(X+1,Y)),sudoku):- pos(X,Y), pos(X+1,Y), subgrid(X,Y,S), subgrid(X+1,Y,S). -viz_edge((pos(X,Y),pos(X,Y+1)),sudoku):- pos(X,Y), pos(X,Y+1), subgrid(X,Y,S), subgrid(X,Y+1,S). +node(pos(X,Y), sudoku):- pos(X,Y). +attr(node, pos(X,Y), label, V):- sudoku(X,Y,V). +attr(node, pos(X,Y), shape, square):- sudoku(X,Y,V). +attr(node, pos(X,Y), style, filled):- sudoku(X,Y,V). +attr(node, pos(X,Y), fontsize, 30):- sudoku(X,Y,V). +attr(node, pos(X,Y), width, "1"):- sudoku(X,Y,V). +attr(node, pos(X,Y), pos, @pos(X,Y)):- pos(X,Y). +attr(node, pos(X,Y), fillcolor, lightblue):- pos(X,Y), (((X-1)/dim)+((Y-1)/dim))\2==0. +attr(node, pos(X,Y), fontcolor, gray50):- sudoku(X,Y,V), not initial(X,Y,_).