<jupyter_start><jupyter_text>Displaying objects in notebooksThis module provides support for displaying objects, such as instance of GTSAM classes.  Showing graphsFor showing graphs, Graphviz and its python wrapper need to be installed. I did that with `conda install python-graphviz` which also installs graphviz, but these are installed by default on Google's colabs.<jupyter_code># export
import math
import gtsam
import graphviz
from gtbook.discrete import Variables
from gtbook.dbn import dbn_writer, has_positions<jupyter_output><empty_output><jupyter_text>Some GTSAM objects have a `dot` method, but notebooks cannot render this. We use a trick, by the following small class, inherting from `graphviz.Source` :<jupyter_code># export
class show(graphviz.Source):
    """ Display an object with a dot method as a graph."""

    def __init__(self, obj, *args, **kwargs):
        """Construct from object with 'dot' method."""
        # This small class takes an object, calls its dot function, and uses the
        # resulting string to initialize a graphviz.Source instance. This in turn
        # has a _repr_mimebundle_ method, which then renders it in the notebook.
        engine = "dot"

        # Check for Variables argument
        if args and isinstance(args[0], Variables):
            assert len(args) == 1, "Variables must be only positional argument."
            args = [args[0].keyFormatter()]

        # Check if a DotWriter needs creating (or amending)
        writer = dbn_writer(**kwargs)
        if writer is not None:
            kwargs = {"writer": writer}
            if has_positions(writer):
                engine = "neato"
        else:
            kwargs = {}

        # also switch to neato if NonlinearFactprGraph, as needs values always
        if isinstance(obj, gtsam.NonlinearFactorGraph):
            engine = "neato"
        super().__init__(obj.dot(*args, **kwargs), engine=engine)

from gtsam import DiscreteBayesNet

Asia = (0, 2)
Smoking = (1, 2)
Tuberculosis = (2, 2)
LungCancer = (3, 2)
Either = (4, 2)

bayesNet = DiscreteBayesNet()
bayesNet.add(Asia, "99/1")
bayesNet.add(Smoking, "50/50")
bayesNet.add(Tuberculosis, [Asia], "99/1 95/5")
bayesNet.add(LungCancer, [Smoking], "99/1 90/10")
bayesNet.add(Either, [Tuberculosis, LungCancer], "F T T T")

show(bayesNet)<jupyter_output><empty_output><jupyter_text>We can, however, create our own formatter in python, and pass it as an optional argument:<jupyter_code>domain = ["Asia", "Smoking", "Tuberculosis", "LungCancer", "Either"]
def keyFormatter(key): return domain[key]


show(bayesNet, keyFormatter)<jupyter_output><empty_output><jupyter_text>It also works for factor graphs, as illustrated below:<jupyter_code># convert bayesNet to a factor graph and render:
from gtsam import DiscreteFactorGraph
fg = DiscreteFactorGraph(bayesNet)
show(fg, keyFormatter, binary_edges=True)<jupyter_output><empty_output><jupyter_text>Factor graph graphviz rendering can be customized even more, through a DotWriter object. For example, the following displays the "primamry constraint graph":<jupyter_code>from gtsam import DotWriter
writer = DotWriter(figureWidthInches=5, figureHeightInches=5,
                   plotFactorPoints=False, binaryEdges=False)
show(fg, keyFormatter, writer)<jupyter_output><empty_output><jupyter_text>Individual factors can also be shown, althoug arrows are not labeled (yet):<jupyter_code>show(fg.at(2))<jupyter_output><empty_output><jupyter_text>HTMLHTML can be rendered natively, like so:<jupyter_code>bayesNet<jupyter_output><empty_output><jupyter_text>However, to provide optional arguments, we have to resort to a similar trick:<jupyter_code># export
class pretty:
    """Render an object as html with optional arguments."""

    def __init__(self, obj, *args):
        if args and isinstance(args[0], Variables):
            assert len(args) == 1, "Variables must be only argument."
            variables = args[0]
            if isinstance(obj, gtsam.DiscreteValues):
                self._html = variables.values_html(obj)
            else:
                self._html = obj._repr_html_(
                    variables.keyFormatter(), variables.names())
        else:
            if isinstance(obj, gtsam.DiscreteValues):
                self._html = f"{obj}"
            elif isinstance(obj, gtsam.Pose2):
                x = round(obj.x(), 2)
                y = round(obj.y(), 2)
                theta = round(math.degrees(obj.theta()), 2)
                self._html = f"(x={x}, y={y}, theta={theta})"
            else:
                self._html = obj._repr_html_(*args)

    def _repr_html_(self):
        return self._html

pose = gtsam.Pose2(12.4, 42.5, math.radians(45))
pretty(pose)
pretty(bayesNet, keyFormatter)<jupyter_output><empty_output><jupyter_text>Factor graphs work as well:<jupyter_code>graph = DiscreteFactorGraph()
graph.add([Asia, Smoking], "4 1 10 4")
pretty(graph, keyFormatter)<jupyter_output><empty_output><jupyter_text>And Bayes trees:<jupyter_code>fg = DiscreteFactorGraph(bayesNet)
bt = fg.eliminateMultifrontal()
pretty(bt, keyFormatter)<jupyter_output><empty_output><jupyter_text>And with a different (worse) ordering:<jupyter_code>ordering = gtsam.Ordering()
ordering.push_back(Either[0])
ordering.push_back(LungCancer[0])
ordering.push_back(Tuberculosis[0])
ordering.push_back(Smoking[0])
ordering.push_back(Asia[0])
bt2 = fg.eliminateMultifrontal(ordering)
pretty(bt2, keyFormatter)<jupyter_output><empty_output><jupyter_text>Works now for `VectorValues` as well, typically without formatter as default KeyFormatter kicks in for symbols.<jupyter_code>vv = gtsam.VectorValues()
X =  gtsam.symbol_shorthand.X
vv.insert(X(1), gtsam.Point2(2, 3.1))
vv.insert(X(2), gtsam.Point2(4, 5.2))
vv.insert(X(5), gtsam.Point2(6, 7.3))
vv.insert(X(7), gtsam.Point2(8, 9.4))
vv<jupyter_output><empty_output><jupyter_text>Domains We also provide support for the `Variables` data structure:<jupyter_code>variables = Variables()
T = variables.discrete("Tuberculosis", ["-", "+"])
L = variables.discrete("LungCancer", ["No", "Yes"])
C = variables.discrete("Either", ["Nope", "One or both"])

fragment = DiscreteBayesNet()
fragment.add(T, "99/1")
fragment.add(L, "50/50")
fragment.add(C, [T, L],  "F T T T")

pretty(fragment, variables)

assignment = variables.assignment({C: "Nope"})
assert pretty(assignment)._repr_html_() == 'DiscreteValues{2: 0}'
pretty(assignment, variables)<jupyter_output><empty_output><jupyter_text>And of course, showing a graph as well:<jupyter_code>show(fragment, variables)<jupyter_output><empty_output><jupyter_text>Formatting HMMSHMMs, and in general dynamic Bayes nets, should really be shown left to right. We create a writer with appropriate `variablePositions` is the object given to `show` is a Bayes net with X/A/Z variables:<jupyter_code>A = variables.discrete_series('A', range(2), ["U", "D"])
X = variables.discrete_series('X', range(3), ["room1", "room2"])
Z = variables.discrete_series('Z', range(3), ["light", "dark"])
hmm = DiscreteBayesNet()
hmm.add(X[0], "99/1")
for k in range(2):
    hmm.add(X[k+1], [X[k], A[k]],  "1/2 3/4 1/2 3/4")
for k in range(3):
    hmm.add(Z[k], [X[k]],  "1/2 3/4")
show(hmm, variables, hints={"A":2, "X":1, "Z":0}, boxes={A[0][0],A[1][0]})

fg = gtsam.DiscreteFactorGraph(hmm)
show(fg, variables, hints={"A":2, "X":1, "Z":0})<jupyter_output><empty_output><jupyter_text>Displaying a NonlinearFactorGraph<jupyter_code>fg = gtsam.NonlinearFactorGraph()
model = gtsam.noiseModel.Unit.Create(2)
fg.push_back(gtsam.BetweenFactorPoint2(1, 2, [1,1], model))
values = gtsam.Values()
values.insert(1, [1,1])
values.insert(2, [2,2])
show(fg, values)<jupyter_output><empty_output><jupyter_text>ArtWe also provide some facilities for displaying some random art:<jupyter_code># export
from IPython.display import HTML
import random

ROBOTS = ["Robot%20menagerie", "Trash%20sorting%20robot%20with%20gripper", "iRobot%20vacuuming%20robot", "Warehouse%20robots",
          "Two-wheeled%20Toy%20Robot", "Autonomous%20Vehicle%20with%20LIDAR%20and%20cameras", "Autonomous%20camera%20drone"]


def randomImages(ch: int, sec: int, style: str, nrImages: int, maxIndex: int = 8):
    """Create an HTML element with some random images.

    Args:
        ch (int): chapter number (base 1)
        sec (int): section number
        style (str): "cubist" | "steampunk" | "expressive"
        nrImages (int): number of images
        maxIndex (int, optional): Indexes to sample from. Defaults to 8.

    Returns:
        HTML: div with nrImages
    """
    perc = round(100/nrImages)
    robot = ROBOTS[ch-1]

    def image_tag(index):
        url = f"https://github.com/gtbook/robotics/blob/main/Art/{style}/S{ch}{sec}-{robot}-{index:02d}.jpg?raw=1"
        return f"<img src='{url}' style='height:256 width:{perc}%'/>"
    indices = random.sample(range(maxIndex), nrImages)
    return HTML(f"""
        <div align='center'>
        {" ".join([image_tag(index) for index in indices])}
        </div>
        """)

from IPython.display import display
for ch in range(1, 8):
    print(ROBOTS[ch-1])
    display(randomImages(ch, 3, "steampunk", 1))<jupyter_output>Robot%20menagerie
{'fraction_non_alphanumeric': 0.1319724920860168, 'fraction_numerical': 0.02106756904268093, 'mean_word_length': 4.460071513706794, 'pattern_counts': {'":': 8, '<': 71, '<?xml version=': 0, '>': 71, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '351835', 'license': 'Apache-2.0', 'n_tokens_mistral': 3270, 'n_tokens_neox': 3076, 'n_words': 782, 'path': 'display.ipynb', 'repo_name': 'dellaert/nbdev_test', 'size': '9161'}