Display#

Display method (mostly using visJS).

stochastic_matching.display.FULL_HTML_TEMPLATE = '\n<div id="%(name)s"></div>\n<script type="module">\n// 1. Define dark/light skins\nconst darkOptions = {\n    nodes: {color: {background: "#222", border: "#999", highlight: "#444"}, font: {color: "#fff"}},\n    edges: {color: {color: "#fff", highlight: "#FFD700"}, font: {color: "#fff"}}\n};\nconst lightOptions = {\n    nodes: {\n        color: {\n            background: "#fff",\n            border: "#222",\n            highlight: "#FFD700"},\n        font: {color: "#222"}\n    },\n    edges: {color: {color: "#222", highlight: "#3876c2"}, font: {color: "#222"}}\n};\n\n// 2. Mode detection\nfunction getTheme() {\n    // Pydata Sphinx Theme: regarde la classe sur body\n    const pydataTheme = document.documentElement.getAttribute("data-theme");\n    if (pydataTheme === "dark" || pydataTheme === "light") {\n        return pydataTheme;\n    }\n    // Jupyter\n    const jupyterLabTheme = document.body.getAttribute("data-jp-theme-name");\n    if (jupyterLabTheme) {\n        // Simplify theme name to \'dark\' or \'light\'\n        const lowerName = jupyterLabTheme.toLowerCase();\n        if (lowerName.includes("dark")) {\n            return "dark";\n        }\n        if (lowerName.includes("light")) {\n            return "light";\n        }\n    }\n    // System\n    return window.matchMedia && window.matchMedia(\'(prefers-color-scheme: dark)\').matches ? "dark" : "light";\n}\n\n// Universal function that loads vis.js only once\nfunction loadVis(url, callback) {\n    if (window.vis) {\n        console.log("vis-network already loaded");\n        callback();\n        return;\n    }\n    // Lookup other loads in progress\n    if (document.querySelector(\'script[data-vis-loaded]\')) {\n        // Script is loaded, wait until it is ready\n        var check = setInterval(function () {\n            if (window.vis) {\n                clearInterval(check);\n                console.log("vis-network sideload completed");\n                callback();\n            }\n        }, 20);\n        return;\n    }\n    // Add the script to the page, callback called when loaded\n    var script = document.createElement(\'script\');\n    script.src = url;\n    script.async = true;\n    script.setAttribute(\'data-vis-loaded\', \'1\');\n    script.onload = callback;\n    document.head.appendChild(script);\n    console.log(\'vis-network dynamically loaded\');\n}\n\n// Start display when vis is loaded\nfunction render() {\nloadVis(\'%(vis)s\', function () {\n    const nodes = %(nodes)s;\n    const edges = %(edges)s;\n    const data = {\n        nodes: nodes,\n        edges: edges,\n    };\n    const options = %(options)s;\n    const container = document.getElementById(\'%(name)s\');\n    const network = new vis.Network(container, data, options);\n    const theme = getTheme();\n    container._theme = theme;\n    const themeOptions = theme === "dark" ? darkOptions : lightOptions;\n    network.setOptions(themeOptions);\n    network.fit({maxZoomLevel: 200});\n})}\n\nrender();\n// Adapter dynamiquement si le thème change\nwindow.addEventListener("theme-changed", () => render());\nconst observer = new MutationObserver(mutations => {\n  for (const mutation of mutations) {\n    if (mutation.type === "attributes" && mutation.attributeName === "data-jp-theme-name") {\n      render();\n    }\n  }\n    });\nobserver.observe(document.body, { attributes: true });\nif (window.matchMedia) {\n  window.matchMedia(\'(prefers-color-scheme: dark)\').addEventListener(\'change\', () => render());\n}\n</script>\n'#

Default template.

stochastic_matching.display.HYPER_GRAPH_VIS_OPTIONS = {'groups': {'HyperEdge': {'color': {'background': 'black'}, 'fixed': {'x': False}, 'shape': 'dot', 'size': 5}, 'Node': {'fixed': {'x': False}}}}#

Default additional options for hypergraphs in the vis-network engine.

stochastic_matching.display.PNG_TEMPLATE = '\n<div id="%(name)s"></div>\n<img id="%(name)s-canvasImg" alt="Right click to save me!">\n<script>\n// Universal function that loads vis.js only once\nfunction loadVis(url, callback) {\n  if (window.vis) {\n    console.log("vis-network already loaded");\n    callback();\n    return;\n  }\n  // Lookup other loads in progress\n  if (document.querySelector(\'script[data-vis-loaded]\')) {\n    // Script is loaded, wait until it is ready\n    var check = setInterval(function() {\n      if (window.vis) {\n        clearInterval(check);\n        console.log("vis-network sideload completed");\n        callback();\n      }\n    }, 20);\n    return;\n  }\n  // Add the script to the page, callback called when loaded\n  var script = document.createElement(\'script\');\n  script.src = url;\n  script.async = true;\n  script.setAttribute(\'data-vis-loaded\', \'1\');\n  script.onload = callback;\n  document.head.appendChild(script);\n  console.log(\'vis-network dynamically loaded\');\n}\n\n// Start display when vis is loaded\nloadVis(\'%(vis)s\', function() {\n  var nodes = %(nodes)s;\n  var edges = %(edges)s;\n  var data = {\n    nodes: nodes,\n    edges: edges,\n  };\n  var options = %(options)s;\n  var container = document.getElementById(\'%(name)s\');\n  var network = new vis.Network(container, data, options);\n  network.on("afterDrawing", function (ctx) {\n      var dataURL = ctx.canvas.toDataURL();\n      document.getElementById(\'%(name)s-canvasImg\').src = dataURL;\n    });\n  network.fit({ maxZoomLevel: 1000 });\n});\n</script>\n'#

Alternate template with a mirror PNG that ca be saved.

stochastic_matching.display.VIS_LOCATION = 'https://unpkg.com/vis-network/standalone/umd/vis-network.min'#

Default location of vis-network.js .

stochastic_matching.display.VIS_OPTIONS = {'height': '100%', 'interaction': {'navigationButtons': False}, 'width': '100%'}#

Default options for the vis-network engine.

stochastic_matching.display.default_description(model)[source]#
Parameters:

model (stochastic_matching.model.Model) – Model to visualize.

Returns:

  • nodes_info list of dict – Skeleton node description.

  • nodes_info list of dict – Skeleton node description.

Examples

>>> import stochastic_matching as sm
>>> diamond = sm.CycleChain()
>>> default_description(diamond) 
([{'id': 0, 'label': '', 'title': '0'},
  {'id': 1, 'label': '', 'title': '1'},
  {'id': 2, 'label': '', 'title': '2'},
  {'id': 3, 'label': '', 'title': '3'}],
[{'title': '0: (0, 1)', 'label': ''},
 {'title': '1: (0, 2)', 'label': ''},
 {'title': '2: (1, 2)', 'label': ''},
 {'title': '3: (1, 3)', 'label': ''},
 {'title': '4: (2, 3)', 'label': ''}])
>>> diamond.names = 'alpha'
>>> default_description(diamond) 
([{'id': 0, 'label': '', 'title': '0: A'},
  {'id': 1, 'label': '', 'title': '1: B'},
  {'id': 2, 'label': '', 'title': '2: C'},
  {'id': 3, 'label': '', 'title': '3: D'}],
[{'title': '0: (A, B)', 'label': ''},
 {'title': '1: (A, C)', 'label': ''},
 {'title': '2: (B, C)', 'label': ''},
 {'title': '3: (B, D)', 'label': ''},
 {'title': '4: (C, D)', 'label': ''}])
stochastic_matching.display.info_maker(model, disp_rates=True, disp_flow=True, flow=None, disp_kernel=False, disp_zero=True, check_flow=False, check_tolerance=0.01)[source]#
Parameters:
  • model (Model) – Model to visualize.

  • disp_rates (bool, optional) – Labels the nodes with their rates. Otherwise the names are used.

  • disp_flow (bool, optional) – Label the edges with the given flow.

  • flow (ndarray, optional) – Flow to use. If None, the base flow will be used. If not None, overrides disp_flow.

  • disp_kernel (bool, optional) – Display the kernel basis on the edges. Compatible with the display of a flow.

  • disp_zero (bool, optional) – If False, do not label the edge with a null flow.

  • check_flow (bool, optional) – If True, color the edges with their positivity and the nodes with their compliance to the conservation law.

  • check_tolerance (float, optional) – Relative error when checking conservation law on nodes. For simulations, a relatively high value is recommended, for example 1e-2.

Returns:

  • nodes_info (list of dict) – Description of nodes.

  • edges_info (list of dict) – Description of edges.

Examples

It is probably best to play a bit with the options, but the following examples should give the general idea.

We start with the so-called pyramid graph (the names comes from one of its kernel, and not from the shape of the graph itself).

>>> import stochastic_matching as sm
>>> pyramid = sm.Pyramid(names='alpha')

By default, the label of a node (its displayed name) is its arrival rate, and the label of an edge is its Moore_penrose flow.

>>> n_i, e_i = info_maker(pyramid)
>>> n_i[3]
{'id': 3, 'label': '2', 'title': '3: D'}
>>> e_i[2]
{'title': '2: (B, C)', 'label': '1'}

We can disable the display of the arrival rates, so the actual name of the node will be displayed.

>>> n_i, e_i = info_maker(pyramid, disp_rates=False)
>>> n_i[3]
{'id': 3, 'label': 'D', 'title': '3: D'}

We ask for no label on edges.

>>> n_i, e_i = info_maker(pyramid, disp_flow=False)
>>> e_i[2]
{'title': '2: (B, C)', 'label': ''}

We can set custom weights on the edges, for instance use a different flow vector.

>>> flow = np.array([0., 2., 0., 3., 1., 1., 0., 2., 0., 2., 0., 1., 1.])
>>> n_i, e_i = info_maker(pyramid, flow=flow)
>>> assert np.allclose([float(e_i[i]['label']) for i in range(pyramid.m)], flow)

We can ask for the kernel basis to be indicated as well.

>>> n_i, e_i = info_maker(pyramid, flow=flow, disp_kernel=True)
>>> e_i[2]
{'title': '2: (B, C)', 'label': '0+α1'}
>>> e_i[3]
{'title': '3: (B, F)', 'label': '3-α1+α2-α3'}

We can remove the flow to have just the kernel basis.

>>> n_i, e_i = info_maker(pyramid, disp_flow=False, disp_kernel=True)
>>> e_i[2]
{'title': '2: (B, C)', 'label': '+α1'}
>>> e_i[3]
{'title': '3: (B, F)', 'label': '-α1+α2-α3'}

By asking null values to be silent, we get avoid things like 0+….

>>> n_i, e_i = info_maker(pyramid, flow=flow, disp_zero=False, disp_kernel=True)
>>> e_i[2]
{'title': '2: (B, C)', 'label': '+α1'}
>>> e_i[3]
{'title': '3: (B, F)', 'label': '3-α1+α2-α3'}

If we ask to check the flow, null edges are displayed in orange.

>>> n_i, e_i = info_maker(pyramid, flow=flow, check_flow=True)
>>> e_i[2]
{'title': '2: (B, C)', 'label': '0', 'color': 'orange'}

Note that the kernel basis is not necessarily +/- 1, even on simple graphs.

>>> kayak = sm.KayakPaddle()
>>> n_i, e_i = info_maker(kayak, disp_kernel=True)
>>> e_i[3]
{'title': '3: (2, 3)', 'label': '1+2α1'}

When kernel is displayed, edges that are not part of any kernel are shown in black.

>>> diamond = sm.CycleChain()
>>> n_i, e_i = info_maker(diamond, disp_kernel=True)
>>> e_i[2]
{'title': '2: (1, 2)', 'label': '1', 'color': 'black'}

Nodes that do not check the conservation law and negative edges are shown in red.

>>> n_i, e_i = info_maker(diamond, flow=[-1]*5, check_flow=True)
>>> n_i[2]
{'id': 2, 'label': '3', 'title': '2', 'color': 'red'}
>>> e_i[2]
{'title': '2: (1, 2)', 'label': '-1', 'color': 'red'}
stochastic_matching.display.int_2_str(model, i)[source]#
Parameters:
  • model (Model) – A stochastic model.

  • i (int) – Node index.

Returns:

Name of the node.

Return type:

str

Examples

>>> import stochastic_matching as sm
>>> diamond = sm.CycleChain()
>>> int_2_str(diamond, 2)
'2'
>>> diamond.names = ['One', 'Two', 'Three', 'Four']
>>> int_2_str(diamond, 2)
'Three'
>>> diamond.names = 'alpha'
>>> int_2_str(diamond, 2)
'C'
stochastic_matching.display.show(model, bipartite=False, png=False, **kwargs)[source]#

End-to-end display solution for model. It is basically a pipe Model -> info_maker() -> vis_converter() -> vis_show().

The extra arguments are passed when needed on the right spot along the pipe, allowing maximal flexibility.

Parameters:
  • model (Model) – The model to display.

  • bipartite (bool, optional) – Tells if the bipartite node/edge structure should be explicitly shown.

  • png (bool) – Make a mirror PNG that can be saved.

  • **kwargs – Keyword arguments. See info_maker(), vis_converter(), and vis_show() for details.

Return type:

HTML

Examples

>>> import stochastic_matching as sm
>>> pyramid = sm.Pyramid()

Basic display.

>>> show(pyramid)
<IPython.core.display.HTML object>

With this, the nodes and edges will not show anything. The result can be exported as png.

>>> nodes_info=[{'label': ''} for _ in range(pyramid.n)]
>>> edges_info=[{'label': ''} for _ in range(pyramid.m)]
>>> show(pyramid, nodes_info=nodes_info, edges_info=edges_info, png=True)
<IPython.core.display.HTML object>

Fan is a true hypergraph.

>>> fan = sm.Fan()

To display:

>>> show(fan)
<IPython.core.display.HTML object>

To display in bipartite mode:

>>> show(fan, bipartite=True)
<IPython.core.display.HTML object>
stochastic_matching.display.vis_code(vis_nodes=None, vis_edges=None, vis_options=None, template=None, vis=None, div_name=None)[source]#

Create HTML to display a Vis network graph.

Parameters:
  • vis_nodes (list of dict) – List the nodes of the graph. Each node is a dictionary with mandatory key id.

  • vis_edges (list of dict) – List the edges of the graph. Each node is a dictionary with mandatory keys from and to.

  • vis_options (dict, optional) – Options to pass to Vis.

  • template (str, optional) – Template to use. Default to HTML_TEMPLATE.

  • vis (str, optional) – Location of vis.js. Default to VIS_LOCATION

  • div_name (str, optional) – Id of the div that will host the display.

Returns:

Vis code (HTML by default).

Return type:

str

Examples

>>> node_list = [{'id': 0}, {'id': 1}, {'id': 2}, {'id': 3}]
>>> edge_list = [{'from': 0, 'to': 1}, {'from': 0, 'to': 2},
...          {'from': 1, 'to': 3}, {'from': 2, 'to': 3}]
>>> print(vis_code(vis_nodes=node_list, vis_edges=edge_list)) 
<div class="sm-graph" id="box-...">
<div id="..."></div>
<a href="#" id="fit-..."
style="position: absolute; left: 10px; bottom: 10px; text-decoration: none; color: #888; font-size: min(2vw, 10px);
z-index: 10; pointer-events: auto;"
> Reload
</a>
...
stochastic_matching.display.vis_converter(model, nodes_info, edges_info)[source]#
Parameters:
  • model (Model) – Model to visualize.

  • nodes_info (list of dict) – Description of nodes.

  • edges_info (list of dict) – Description of edges.

Returns:

  • vis_nodes (list of dict) – Description of the nodes that will be displayed in vis. If the graph is simple, this is just the input nodes. For hypergraphs, both nodes and hyperedges are displayed as nodes in vis.

  • vis_edges (list of dict) – Description of the edges that will be displayed in vis. If the graph is simple, this is just the input edges, with endpoints info added for vis. For hypergraphs, each edge in vis links a node and a hyperedge..

Examples

>>> import stochastic_matching as sm
>>> diamond = sm.CycleChain()
>>> nodes, edges = default_description(diamond)
>>> vis_converter(diamond, nodes, edges) 
([{'id': 0, 'label': '', 'title': '0'},
  {'id': 1, 'label': '', 'title': '1'},
  {'id': 2, 'label': '', 'title': '2'},
  {'id': 3, 'label': '', 'title': '3'}],
 [{'title': '0: (0, 1)', 'label': '', 'from': 0, 'to': 1},
  {'title': '1: (0, 2)', 'label': '', 'from': 0, 'to': 2},
  {'title': '2: (1, 2)', 'label': '', 'from': 1, 'to': 2},
  {'title': '3: (1, 3)', 'label': '', 'from': 1, 'to': 3},
  {'title': '4: (2, 3)', 'label': '', 'from': 2, 'to': 3}])
>>> diamond.adjacency = None
>>> vis_converter(diamond, nodes, edges) 
([{'id': 0, 'label': '', 'title': '0', 'x': 0, 'group': 'Node'},
  {'id': 1, 'label': '', 'title': '1', 'x': 0, 'group': 'Node'},
  {'id': 2, 'label': '', 'title': '2', 'x': 0, 'group': 'Node'},
  {'id': 3, 'label': '', 'title': '3', 'x': 0, 'group': 'Node'},
  {'title': '0: (0, 1)', 'label': '', 'from': 0, 'to': 1, 'id': 4, 'group': 'HyperEdge', 'x': 600},
  {'title': '1: (0, 2)', 'label': '', 'from': 0, 'to': 2, 'id': 5, 'group': 'HyperEdge', 'x': 600},
  {'title': '2: (1, 2)', 'label': '', 'from': 1, 'to': 2, 'id': 6, 'group': 'HyperEdge', 'x': 600},
  {'title': '3: (1, 3)', 'label': '', 'from': 1, 'to': 3, 'id': 7, 'group': 'HyperEdge', 'x': 600},
  {'title': '4: (2, 3)', 'label': '', 'from': 2, 'to': 3, 'id': 8, 'group': 'HyperEdge', 'x': 600}],
 [{'from': 0, 'to': 4, 'title': '0 <-> 0: (0, 1)'},
  {'from': 0, 'to': 5, 'title': '0 <-> 1: (0, 2)'},
  {'from': 1, 'to': 4, 'title': '1 <-> 0: (0, 1)'},
  {'from': 1, 'to': 6, 'title': '1 <-> 2: (1, 2)'},
  {'from': 1, 'to': 7, 'title': '1 <-> 3: (1, 3)'},
  {'from': 2, 'to': 5, 'title': '2 <-> 1: (0, 2)'},
  {'from': 2, 'to': 6, 'title': '2 <-> 2: (1, 2)'},
  {'from': 2, 'to': 8, 'title': '2 <-> 4: (2, 3)'},
  {'from': 3, 'to': 7, 'title': '3 <-> 3: (1, 3)'},
  {'from': 3, 'to': 8, 'title': '3 <-> 4: (2, 3)'}])
>>> candy = sm.HyperPaddle()
>>> vis_nodes, vis_edges = vis_converter(candy, *default_description(candy))
>>> vis_nodes[2]
{'id': 2, 'label': '', 'title': '2', 'x': 0, 'group': 'Node'}
>>> vis_nodes[13]
{'title': '6: (2, 3, 4)', 'label': '', 'id': 13, 'group': 'HyperEdge', 'x': 600}
>>> vis_edges[6]
{'from': 2, 'to': 13, 'title': '2 <-> 6: (2, 3, 4)'}
stochastic_matching.display.vis_show(vis_nodes=None, vis_edges=None, vis_options=None, template=None, vis=None, div_name=None)[source]#

Display a Vis graph (within a IPython / Jupyter session).

Parameters:
  • vis_nodes (list of dict) – List the nodes of the graph. Each node is a dictionary with mandatory key id.

  • vis_edges (list of dict) – List the edges of the graph. Each node is a dictionary with mandatory keys from and to.

  • vis_options (dict, optional) – Options to pass to Vis.

  • template (str, optional) – Template to use. Default to HTML_TEMPLATE.

  • vis (str, optional) – Location of vis.js. Default to VIS_LOCATION

  • div_name (str, optional) – Id of the div that will host the display.

Return type:

HTML

Examples

>>> vis_show()
<IPython.core.display.HTML object>