mirror of https://github.com/dnomd343/klotski.git
4 changed files with 257 additions and 205 deletions
@ -0,0 +1,95 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
import os |
|||
from typing import override |
|||
from dataclasses import dataclass |
|||
|
|||
import igraph as ig |
|||
from lxml import etree |
|||
from graphml import INode, IEdge, Config, GraphML |
|||
|
|||
|
|||
@dataclass(frozen=True) |
|||
class Node(INode): |
|||
tag: str |
|||
step: int |
|||
codes: list[str] |
|||
|
|||
@staticmethod |
|||
@override |
|||
def add_keys(graphml: etree.Element, cfg: Config) -> None: |
|||
etree.SubElement(graphml, 'key', attrib={ |
|||
'id': 'step', |
|||
'for': 'node', |
|||
'attr.name': 'step', |
|||
'attr.type': 'int' |
|||
}) |
|||
etree.SubElement(graphml, 'key', attrib={ |
|||
'id': 'codes', |
|||
'for': 'node', |
|||
'attr.name': 'codes', |
|||
'attr.type': 'string' |
|||
}) |
|||
if cfg.is_yed: |
|||
INode._add_yed_key(graphml) |
|||
|
|||
@override |
|||
def render(self, cfg: Config) -> etree.Element: |
|||
node_xml = etree.Element('node', id=self.tag) |
|||
etree.SubElement(node_xml, 'data', key='step').text = str(self.step) |
|||
etree.SubElement(node_xml, 'data', key='codes').text = '+'.join(self.codes) |
|||
|
|||
if cfg.is_yed: |
|||
color = cfg.colors[self.step] |
|||
label = f'{self.tag} ({self.step})' |
|||
node_xml.append(self._yed_render(cfg, color, label)) |
|||
|
|||
return node_xml |
|||
|
|||
|
|||
@dataclass(frozen=True) |
|||
class Edge(IEdge): |
|||
src: str |
|||
dst: str |
|||
|
|||
@staticmethod |
|||
@override |
|||
def add_keys(graphml: etree.Element, cfg: Config) -> None: |
|||
pass |
|||
|
|||
@override |
|||
def render(self, is_yed: bool) -> etree.Element: |
|||
return etree.Element('edge', source=self.src, target=self.dst) |
|||
|
|||
|
|||
def to_graphml(tag: str, file: str, output: str, is_yed: bool) -> None: |
|||
print(f'Convert graph {file} into {output}') |
|||
|
|||
g = ig.Graph.Read_Pickle(file) |
|||
|
|||
nodes = [] |
|||
for v in g.vs: |
|||
node = Node(v['tag'], v['step'], v['codes']) |
|||
nodes.append(node) |
|||
|
|||
edges = [] |
|||
for n1, n2 in g.get_edgelist(): |
|||
node_1 = nodes[n1] |
|||
node_2 = nodes[n2] |
|||
if node_1.step < node_2.step: |
|||
node_1, node_2 = node_2, node_1 |
|||
edges.append(Edge(node_1.tag, node_2.tag)) |
|||
|
|||
gml = GraphML(tag, nodes, edges) |
|||
colors = GraphML.build_colors(max(x.step for x in nodes) + 1, ['#0000ff', '#e8daef', '#ff0000']) |
|||
cfg = Config(is_yed=is_yed, colors=colors) |
|||
gml.save_graphml(output, cfg) |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
os.makedirs('output-combine-gml', exist_ok=True) |
|||
for name in sorted(os.listdir('output-combine')): |
|||
name = name.removesuffix('.pkl') |
|||
tag = name.split('_')[0] if '_' in name else name |
|||
to_graphml(tag, f'output-combine/{name}.pkl', f'output-combine-gml/{name}.graphml', False) |
|||
to_graphml(tag, f'output-combine/{name}.pkl', f'output-combine-gml/{name}-yed.graphml', True) |
@ -1,44 +0,0 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
import os |
|||
import igraph as ig |
|||
|
|||
|
|||
def load_legacy(file: str) -> ig.Graph: |
|||
g = ig.Graph.Read_Pickle(file) |
|||
for node in g.vs: |
|||
node['codes'] = sorted(node['codes']) |
|||
return g |
|||
|
|||
|
|||
def load_modern(file: str) -> ig.Graph: |
|||
g = ig.Graph.Read_Pickle(file) |
|||
|
|||
for idx, node in enumerate(g.vs): |
|||
assert sorted(node['codes']) == node['codes'] |
|||
assert int(node['tag'].removeprefix('U')) == idx |
|||
|
|||
del g.vs['tag'] |
|||
return g |
|||
|
|||
|
|||
def compare(g1: ig.Graph, g2: ig.Graph) -> None: |
|||
assert g1.vcount() == g2.vcount() |
|||
assert g1.ecount() == g2.ecount() |
|||
assert g1.isomorphic(g2) |
|||
|
|||
assert {len(x.attributes()) for x in g1.es} == {0} |
|||
assert {len(x.attributes()) for x in g2.es} == {0} |
|||
|
|||
data_a = {min(x['codes']): x.attributes() for x in g1.vs} |
|||
data_b = {min(x['codes']): x.attributes() for x in g2.vs} |
|||
assert data_a == data_b |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
for name in sorted(os.listdir('output-combine')): |
|||
if '_' not in name: |
|||
continue |
|||
g1 = load_legacy(f'combined/{name.split('_')[1]}') |
|||
g2 = load_modern(f'output-combine/{name}') |
|||
compare(g1, g2) |
@ -0,0 +1,116 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
import io |
|||
import numpy as np |
|||
from lxml import etree |
|||
from dataclasses import dataclass |
|||
from abc import ABC, abstractmethod |
|||
import matplotlib.colors as mcolors |
|||
|
|||
|
|||
@dataclass |
|||
class Config: |
|||
is_yed: bool |
|||
colors: list[str] |
|||
pretty_xml: bool = True |
|||
|
|||
yed_xmlns: str = 'http://www.yworks.com/xml/graphml' |
|||
yed_node_type: str = 'ellipse' |
|||
yed_node_width: int = 50 |
|||
yed_node_height: int = 50 |
|||
yed_node_font_size: int = 10 |
|||
|
|||
|
|||
class INode(ABC): |
|||
@staticmethod |
|||
def _add_yed_key(graphml: etree.Element) -> None: |
|||
etree.SubElement(graphml, 'key', attrib={ |
|||
'id': 'info', |
|||
'for': 'node', |
|||
'yfiles.type': 'nodegraphics' |
|||
}) |
|||
|
|||
@staticmethod |
|||
def _yed_render(cfg: Config, color: str, text: str) -> etree.Element: |
|||
yed_ns = f'{{{cfg.yed_xmlns}}}' |
|||
info = etree.Element('data', attrib={'key': 'info'}) |
|||
shape = etree.SubElement(info, f'{yed_ns}ShapeNode') |
|||
etree.SubElement(shape, f'{yed_ns}Fill', attrib={ |
|||
'color': color |
|||
}) |
|||
etree.SubElement(shape, f'{yed_ns}Shape', attrib={ |
|||
'type': cfg.yed_node_type |
|||
}) |
|||
etree.SubElement(shape, f'{yed_ns}Geometry', attrib={ |
|||
'height': str(cfg.yed_node_width), |
|||
'width': str(cfg.yed_node_height), |
|||
}) |
|||
label = etree.SubElement(shape, f'{yed_ns}NodeLabel', attrib={ |
|||
'fontSize': str(cfg.yed_node_font_size), |
|||
'modelName': 'internal' |
|||
}) |
|||
label.text = text |
|||
return info |
|||
|
|||
@staticmethod |
|||
@abstractmethod |
|||
def add_keys(graphml: etree.Element, cfg: Config) -> None: |
|||
pass |
|||
|
|||
@abstractmethod |
|||
def render(self, cfg: Config) -> etree.Element: |
|||
pass |
|||
|
|||
|
|||
class IEdge(ABC): |
|||
@staticmethod |
|||
@abstractmethod |
|||
def add_keys(graphml: etree.Element, cfg: Config) -> None: |
|||
pass |
|||
|
|||
@abstractmethod |
|||
def render(self, cfg: Config) -> etree.Element: |
|||
pass |
|||
|
|||
|
|||
class GraphML: |
|||
def __init__(self, tag: str, nodes: list[INode], edges: list[IEdge]): |
|||
self.__tag = tag |
|||
self.__nodes = nodes |
|||
self.__edges = edges |
|||
assert len(nodes) > 0 and len(edges) > 0 |
|||
|
|||
@staticmethod |
|||
def __nsmap(cfg: Config) -> dict[str | None, str]: |
|||
return { |
|||
None: 'http://graphml.graphdrawing.org/xmlns', |
|||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance', |
|||
**({'y': cfg.yed_xmlns} if cfg.is_yed else {}) |
|||
} |
|||
|
|||
@staticmethod |
|||
def build_colors(num: int, bwr: list[str]) -> list[str]: |
|||
cmap = mcolors.LinearSegmentedColormap.from_list('custom_bwr', bwr) |
|||
return [mcolors.to_hex(cmap(x)) for x in np.linspace(0, 1, num)] |
|||
|
|||
def __build_graphml(self, cfg: Config) -> etree.Element: |
|||
graphml = etree.Element('graphml', nsmap=self.__nsmap(cfg)) |
|||
graphml.set( |
|||
'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation', |
|||
'http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd' |
|||
) |
|||
self.__nodes[0].add_keys(graphml, cfg) |
|||
self.__edges[0].add_keys(graphml, cfg) |
|||
graph = etree.SubElement(graphml, 'graph', id=self.__tag, edgedefault='undirected') |
|||
[graph.append(x.render(cfg)) for x in self.__nodes] |
|||
[graph.append(x.render(cfg)) for x in self.__edges] |
|||
return graphml |
|||
|
|||
def save_graphml(self, file: str, cfg: Config) -> None: |
|||
xml_tree = etree.ElementTree(self.__build_graphml(cfg)) |
|||
|
|||
fake_output = io.BytesIO() |
|||
xml_tree.write(fake_output, pretty_print=cfg.pretty_xml, xml_declaration=True, encoding='utf-8') |
|||
content = fake_output.getvalue().decode('utf-8') |
|||
with open(file, 'w') as fp: |
|||
fp.write(content.replace('&#10;', ' ')) |
Loading…
Reference in new issue