Source code for kingdon.graph
from collections.abc import Callable
from functools import cached_property
from types import GeneratorType
import timeit
from typing import List, Tuple
import anywidget
import traitlets
import pathlib
import numpy as np
from kingdon.multivector import MultiVector
TREE_TYPES = (list, tuple)
[docs]
def walker(encoded_generator, tree_types=TREE_TYPES):
result = []
for item in encoded_generator:
if isinstance(item, GeneratorType):
result.extend(walker(item))
elif isinstance(item, tree_types):
result.append(walker(item))
else:
result.append(item)
return result
[docs]
def encode(o, tree_types=TREE_TYPES, root=False):
if root and isinstance(o, tree_types):
yield from (encode(value, tree_types) for value in o)
elif isinstance(o, tree_types):
yield o.__class__(encode(value, tree_types) for value in o)
elif isinstance(o, MultiVector) and len(o.shape) > 1:
# if isinstance(o._values, np.ndarray):
# ovals = o._values.T
# yield {'mvs': ovals.tobytes(), 'shape': ovals.shape}
# else:
yield from (encode(value) for value in o.itermv())
elif isinstance(o, MultiVector):
values = o._values.tobytes() if isinstance(o._values, np.ndarray) else o._values.copy()
if len(o) != len(o.algebra):
# If not full mv, also pass the keys and let ganja figure it out.
yield {'mv': values, 'keys': o._keys}
else:
yield {'mv': values}
elif isinstance(o, Callable):
yield encode(o(), tree_types)
else:
yield o
[docs]
class GraphWidget(anywidget.AnyWidget):
_esm = pathlib.Path(__file__).parent / "graph.js"
# Required arguments.
algebra = traitlets.Instance("kingdon.algebra.Algebra")
options = traitlets.Dict().tag(sync=True)
# Properties derived from the required arguments which have to be available to js.
signature = traitlets.List([]).tag(sync=True) # Signature of the algebra
cayley = traitlets.List([]).tag(sync=True) # Cayley table of the algebra
key2idx = traitlets.Dict({}).tag(sync=True) # Conversion from binary keys to indices
# Properties needed to paint the scene.
raw_subjects = traitlets.List([]) # A place to store the original input.
pre_subjects = traitlets.List([]) # Store the prepared subjects.
draggable_points = traitlets.List([]).tag(sync=True) # points at the first level of nesting are interactive.
draggable_points_idxs = traitlets.List([]).tag(sync=True) # indices of the draggable points in pre_subjects.
subjects = traitlets.List([]).tag(sync=True) # Result of evaluating pre_subjects.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.on_msg(self._handle_custom_msg)
def _handle_custom_msg(self, data, buffers):
"""If triggered, reevaluate the subjects. """
if data["type"] == "update_mvs":
# TODO: only update those that are callable for better performance?
self.subjects = self.get_subjects()
def _get_pre_subjects(self):
if len(self.raw_subjects) == 1 and not isinstance((s := self.raw_subjects[0]), MultiVector) and isinstance(s, Callable):
# Assume this to be a function returning a list of suitable subjects.
pre_subjects = s()
if not isinstance(pre_subjects, TREE_TYPES):
pre_subjects = [pre_subjects]
else:
pre_subjects = self.raw_subjects
return pre_subjects
@traitlets.default('key2idx')
def get_key2idx(self):
return {k: i for i, k in enumerate(self.algebra.canon2bin.values())}
@traitlets.default('signature')
def get_signature(self):
return [int(s) for s in self.algebra.signature]
@traitlets.default('cayley')
def get_cayley(self):
cayley_table = [[s if (s := self.algebra.cayley[eJ, eI])[-1] != 'e' else f"{s[:-1]}1"
for eI in self.algebra.canon2bin]
for eJ in self.algebra.canon2bin]
return cayley_table
@traitlets.default('pre_subjects')
def get_pre_subjects(self):
return self._get_pre_subjects()
@traitlets.default('subjects')
def get_subjects(self):
# Encode all the subjects
return walker(encode(self._get_pre_subjects(), root=True))
@traitlets.default('draggable_points')
def get_draggable_points(self):
# Extract the draggable points. TODO: pseudovectors only?
return walker(encode([s for s in self.pre_subjects if isinstance(s, MultiVector)]))
@traitlets.default('draggable_points_idxs')
def get_draggable_points_idxs(self):
# Extract the draggable points. TODO: pseudovectors only?
return [j for j, s in enumerate(self.pre_subjects) if isinstance(s, MultiVector)]
@traitlets.observe('draggable_points')
def _observe_draggable_points(self, change):
""" If draggable_points is changed, replace the raw_subjects in place. """
self.inplacereplace(self.pre_subjects, zip(self.draggable_points_idxs, change['new']))
self.subjects = self.get_subjects().copy()
@traitlets.validate("options")
def _valid_options(self, proposal):
options = proposal['value']
if 'camera' in options:
options['camera'] = list(encode(options['camera']))[0]
return options
[docs]
def inplacereplace(self, old_subjects, new_subjects: List[Tuple[int, dict]]):
"""
Given the old and the new subjects, replace the values inplace iff they have changed.
"""
for j, new_subject in new_subjects:
old_subject = old_subjects[j]
old_vals = old_subject._values
new_vals = new_subject['mv']
if len(old_vals) == len(self.algebra):
# If full mv, we can be quick.
for j, val in enumerate(new_vals):
if old_vals[j] != val:
old_vals[j] = val
else:
for j, k in enumerate(old_subject._keys):
val = new_vals[self.key2idx[k]]
if old_vals[j] != val:
old_vals[j] = val