15. schematic - manual drawing

This example demonstrates the basic functionality of the schematic module. The schematic module is a simple wrapper around matplotlib that allows programatically drawing diagrams, e.g. for tensor networks, in 2D and also pseudo-3D. It is used as the backend for automatic drawing in TensorNetwork.draw, but it also useful for making manual diagrams not associated with a tensor network. The main object is the Drawing class.

15.1. Illustrative full examples

The following examples are intended to be illustrative of a full drawing. If you supply a dict of presets to Drawing, then you can provide default styling for various elements simply by name.

15.1.1. 2D example

%config InlineBackend.figure_formats = ['svg']
from quimb import schematic

presets = {
    'bond': {'linewidth': 3},
    'phys': {'linewidth': 1.5},
    'center': {
        # `get_wong_color` uses more colorblind friendly colors
        'color': schematic.get_wong_color('orange'),
        'hatch': '/////',
    },
    'left': {
        'color': schematic.get_wong_color('bluedark'),
    },
    'right': {
        'color': schematic.get_wong_color('blue'),
    },
}

d = schematic.Drawing(presets=presets)

L = 10
center = 5

for i in range(10):
    # draw tensor
    d.circle((i, 0), preset=(
        "center" if i == center else
        "left" if i < center else
        "right"
    ))

    # draw physical index
    d.line((i, 0), (i, -2/3), preset='phys')

    # draw virtual bond
    if i + 1 < L:
        d.line((i, 0), (i + 1, 0), preset='bond')

    # draw isometric conditions
    if i != center:
        d.arrowhead((i, -2/3), (i, 0), preset='phys')
    if i < center - 1:
        d.arrowhead((i, 0), (i + 1, 0), preset='bond')
    if i > center + 1:
        d.arrowhead((i, 0), (i - 1, 0), preset='bond')

# label the left
if center > 0:
    d.text((center - 1, 0.8), 'LEFT')
    d.patch_around([(i, 0) for i in range(center)], radius=0.5)

# label pair
if center + 1 < L:
    d.patch_around_circles(
        (center, 0), 0.3,
        (center + 1, 0), 0.3,
        facecolor=(.2, .8, .5, .4),
    )

# label the right
if center + 2 < L:
    d.text((center + 2, 0.8), 'RIGHT')
    d.patch_around([(i, 0) for i in range(center + 2, L)], radius=0.5)
../_images/7a81f2c9b0297b8310118b37e7958d7d6c146e272ae2bf7b9c6965c2a737175c.svg

15.1.2. Pseudo-3D example

If you supply 3D coordinates to Drawing methods then the objects will be mapped by the axonometric projection to 2D and given appropriate z-ordering. The projection and orientation can be controlled in the Drawing constructor.

d = schematic.Drawing(presets=presets, figsize=(10, 10))

L = 10

radius = 0.15

for center in range(L):
    # map the stage into a 3D x-coordinate
    x = 2 * center

    for i in range(10):
        # draw tensor, can now use cube rather than circle
        d.cube((x, i, 0), radius=radius, preset=(
            "center" if i == center else
            "left" if i < center else
            "right"
        ))

        # draw physical index
        d.line((x, i, 0), (x, i, -2/3), preset='phys')

        # draw virtual bond
        if i + 1 < L:
            d.line((x, i + radius, 0), (x, i + 1 - radius, 0), preset='bond')

        # draw isometric conditions
        if i != center:
            d.arrowhead((x, i, -2/3), (x, i, 0), preset='phys')
        if i < center - 1:
            d.arrowhead((x, i, 0), (x, i + 1, 0), preset='bond')
        if i > center + 1:
            d.arrowhead((x, i, 0), (x, i - 1, 0), preset='bond')

    # label the left
    if center > 0:
        d.patch_around([(x, i, 0) for i in range(center)], radius=3 * radius, smoothing=0.0)

    # label pair
    if center + 1 < L:
        d.patch_around_circles(
            (x, center, 0), 2.5 * radius,
            (x, center + 1, 0), 2.5 * radius,
            facecolor=(.2, .7, .3, .3),
        )

    # label the right
    if center + 2 < L:
        d.patch_around([(x, i, 0) for i in range(center + 2, L)], radius=3 * radius, smoothing=0.0)

d.text((0, 0, 1), '$T^i_{e_i}$', color=schematic.get_wong_color('orange'))
../_images/a3270d226d8413b543aac2620a95f40e0f4f8a02aa7b24bbaf97c0ac0679d16b.svg

15.2. Individual elements:

Here we demonstrate the different types of individual element that can be placed. The hash_to_color function is useful way to deterministically generate colors from hashable objects.

import numpy as np

15.2.1. Circles

Drawing.circle draws a circle with a given radius and center coordinates.

d = schematic.Drawing()

coos = [
    (i, j, 0)
    for i in range(4)
    for j in range(4)
]

for coo in coos:
    d.circle(
        coo,
        radius=np.random.uniform(0.2, 0.3),
        color=schematic.hash_to_color(str(coo))
    )

# dot is a simple alias circle
d.dot((1.5, 1.5, 0))
../_images/1ea887cdf3b04df69ef630fb695c87e4b9ea99aa01fdc6a6e5b7216e3217a308.svg

15.2.2. Cubes

Drawing.cube draws a cube with a given ‘radius’ and center coordinates, only for 3D coordinates.

d = schematic.Drawing()

coos = [
    (i, j, 0)
    for i in range(4)
    for j in range(4)
]

for coo in coos:
    d.cube(
        coo,
        radius=np.random.uniform(0.2, 0.3),
        color=schematic.hash_to_color(str(coo))
    )
../_images/4ff251f4c0f565fe29751257ad8fa3213d490f672863eb41ea8f7a936030c1ed.svg

15.2.3. Text

Drawing.text places text in data coordinates (including 3D). Drawing.label_ax and Drawing.label_fig are the same but default to axis and figure coordinates respectively.

d = schematic.Drawing()

coos = [
    (i, j, 0)
    for i in range(4)
    for j in range(4)
]

for coo in coos:
    d.text(
        coo, str(coo),
        color=schematic.hash_to_color(str(coo))
    )

# labels are the same but use the axes or figure coordinates
d.label_ax(0.1, 0.9, '$\\mathbf{B}$', fontsize=20)
d.label_fig(0.5, 0.0, '$\\sum_e \\prod_i ~ T^i_{e_i}$', fontsize=16)
../_images/f3a5e236b8fd7359e583e45be5bbdaf1562bc560525cc7cde9cb0d1e1e4a6c00.svg

15.2.4. General shapes

Drawing.shape draws a general filled shape given a sequence of 2D or 3D coordinates.

d = schematic.Drawing()

rng = np.random.default_rng(1)
pts = rng.normal(size=(8, 3, 3))

for coos in pts:
    d.shape(
        coos,
        alpha=0.8,
        hatch="XXX",
        edgecolor=schematic.hash_to_color(str(coos)),
    )
../_images/0f291c4e035cf759838781f29df03a36986b88bfab97d164584e33e5040b2fad.svg

15.2.5. Markers

Drawing.marker is a convenience method for specifying the shape of a patch using a single string or integer, to yield a regular polygon.

d = schematic.Drawing()

for p in range(3, 10):
    d.marker((p, 0), marker=p)
    d.text((p, 0.5), f"{p}-gon")
../_images/aca31ca809c5209ff556edc10dfe8c39366bc056c9d232325e5fec0b81fb5fa4.svg

15.3. Lines and curves

15.3.1. Lines

The basic method for drawing lines between a pair of 2D or 3d points is Drawing.line.

d = schematic.Drawing()

d.line((0, 0, 0), (0, 0, 1))
d.line((0, 1, 0), (0, 1, 1), arrowhead=True)
d.line((1, 0, 0), (1, 0, 1), linewidth=4)
d.line((1, 1, 0), (1, 1, 1), linestyle=':')
../_images/4758ab295950d83df0d04e80dbb12e0fc75efc96b37f979fbfcec23a1e175a3c.svg

15.3.2. Arrows and labels

You can easily add text and arrows along lines:

d = schematic.Drawing()

pa, pb, pc = (0, 0), (1, 1), (2, 0.5)

d.line(pa, pb, text="hello\n")
d.line(pb, pc, text=dict(text="world\n", color='red'), arrowhead=dict(center=1))

# calling `line` with `text=` is a shortcut for `text_between`
d.text_between(pa, pc, "Could this be a shortcut?", color="green")
../_images/246ab3829e234f15c0c831f2941f17d7cc9f8d7ad6f6c241d733041581bd6427.svg

15.3.3. Curves

If you want a line to pass through multiple points, you can use Drawing.curve to draw a smooth curve.

d = schematic.Drawing()

d.curve(
    [(0, 0), (1, 1), (2.5, 0.5), (3.5, 1.5)],
    linestyle='-.', linewidth=5,
)

# you can draw just the arrowhead separatel
d.arrowhead((1, 1), (2.5, 0.5), linewidth=5, width=0.15)
../_images/56bf1fbd53d6cf3ab9924290058d5cb04ff3e0ca159d57f15d8e6085ae6f93a8.svg

Curves pass exactly through all points given, with the smoothing kwarg controlling… how smoothly they do this.

import matplotlib as mpl

d = schematic.Drawing()

rng = np.random.default_rng(1)
pts = rng.normal(size=(20, 3))
cm = mpl.colormaps.get_cmap('RdPu')

for pt in pts:
    d.dot(pt, color='black', radius=0.05)

for smoothing in np.linspace(0.0, 2.0, 11):
    d.curve(pts, smoothing=smoothing, color=cm(smoothing / 2))

d.label_ax(1.0, 0.60, "smoothing=0.0", color=cm(0.0))
d.label_ax(1.0, 0.65, "smoothing=1.0", color=cm(0.5))
d.label_ax(1.0, 0.70, "smoothing=2.0", color=cm(1.0))
../_images/93f61774dd033107e0b4edddb8455be06695260369c4c0f2a8a2fbd743e262a7.svg

15.3.4. Multi-edges

If you want to programmatically draw multiple lines from one place to the other (‘multi-edges’) you can use line_offset:

d = schematic.Drawing()

pa, pb = (0, 0, 0), (0, 1, 1)

green = schematic.get_wong_color("green")
red = schematic.get_wong_color("red")
blue = schematic.get_wong_color("blue")

d.circle(pa, color=green)
d.circle(pb, color=red)

# you can still use arrowheads and text labels
d.line_offset(pa, pb, 0.2, arrowhead=dict(center=0.9), text='forwards\n', color=blue)
d.line_offset(pa, pb, 0.0, arrowhead=dict(center=0.9, reverse=True), text='backwards\n', color=blue)
d.line_offset(pa, pb, -0.2, arrowhead=dict(center=0.9, reverse="both"), text='both ways!\n', color=blue, midlength=0.4)
../_images/d7f638072aacf767fe71892e7dbc8095210eb7fc3b415c85127dfd0ad880485e.svg

15.4. Highlighting areas and groups of objects

15.4.1. Patches around general areas

In technical drawings it is often useful to highlight areas. The Drawing.patch method does this by filling in a curve, given by a sequence of 2D or 3D coordinates.

d = schematic.Drawing(figsize=(4, 4))

d.marker((0, 0), marker='s', color=schematic.get_wong_color('yellow'))
d.marker((0, 1), marker='s', color=schematic.get_wong_color('bluedark'))
d.patch([
    (-.3, -.3),
    (+.3, -.3),
    (+.3, +1.3),
    (-.3, +1.3),
], smoothing=0.3)
../_images/9ee1020d4a466992bceea8a87d5334758f7c43ba9a7518710e5e139f4f55b9b0.svg

15.4.2. Patches around two circles

If you want to specifically highlight two circles, you can use Drawing.patch_around_circles, and simply specify the two circles by their center coordinates and radii.

d = schematic.Drawing(figsize=(4, 4))

d.circle((0, 0), radius=3, color=schematic.get_wong_color('pink'))
d.circle((10, 1), radius=2, color=schematic.get_wong_color('blue'))

d.patch_around_circles(
    (0, 0), 3,
    (10, 1), 2,
    padding=0.5,
)
../_images/a455a143681b2e8c5f3165536ae06b61e7fc7ff127caf84dd67a5628db059249.svg

15.4.3. Patches around arbitrary collections of objects

If you want to highlight an arbitrary collection of objects, you can call Drawing.patch_around, this computes the convex hull of the objects and draws a patch around it.

d = schematic.Drawing()

for pt in pts[:7]:
    d.dot(pt, color='orange', radius=0.05)
for pt in pts[7:]:
    d.dot(pt, color='black', radius=0.05)

d.patch_around(pts[:7], edgecolor='orange')
../_images/47afc2e4cfd015d641083081c37d0b47d9fff2b4d67b421161d9678ba7af7efc.svg

You can control how much padding is added around the perimeter of the objects using the radius kwarg.

d = schematic.Drawing()

for k, pt in enumerate(pts):
    d.dot(pt, color=schematic.hash_to_color(str(k)), radius=0.05)

for k in range(1, len(pts)):
    d.patch_around(
        pts[:k],
        radius=0.05 * k,
        facecolor=schematic.hash_to_color(str(k - 1)),
        linestyle="-",
        zorder=-k,
        alpha=0.5,
    )
../_images/5a9547dcacc40d309ed1018011dfb4685f64a214311c997f274dcfe3c90db354.svg