Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

galaga-annotation

Semantic annotations and KaTeX rendering for galaga calculations.

Annotations are presentation metadata: they never change numerical values, expression evaluation, equality or hashing. The package is optional; importing galaga never imports galaga_annotation.

Install

For the Galaga 2 beta, opt into prereleases:

python -m pip install --pre "galaga-annotation>=2.0.0b1,<3"

Quick start

from galaga import Algebra, outer_product

import galaga_annotation as ga

algebra = Algebra(3, expr=True)
e1, e2, e3 = algebra.basis_vectors(expr=True)
area = outer_product(e1 + e2, e3).named("A")

view = ga.annotate(area, label="oriented area")
assert r"\overset{\text{oriented area}}" in view.latex()
assert view.plain is area

ga.annotate(value, ...) is the one-off spelling of ga.annotator(ga.on(...))(value). Annotated views are rendering-only: use view.plain or view.value for further calculations.

Reusable recipes

def highlight_grade(grade):
    return ga.annotator(ga.on(ga.grade(grade), background="#fff3cd"))


value = (0.5 + 2 * e1 - 3 * (e1 ^ e2)).named("C")
rendered = highlight_grade(1)(value).latex()
assert r"\colorbox{#fff3cd}{$2 e_{1}$}" in rendered

Recipes are immutable and reusable. Fluent builders such as .highlight(...), .label(...) and .mark(...) return new recipes and produce the same ordered plan as the functional ga.on(...) spelling.

Annotating operations and operands

lesson = ga.annotator(
    ga.on(ga.operand(0), label="vector sum", side="below"),
    ga.on(ga.operator("outer_product"), label="alternating product", marker="arrow"),
)
rendered = lesson(area).latex()
assert r"\underset{\text{vector sum}}" in rendered
assert r"\overset{\substack{\text{alternating product} \\ \downarrow}}" in rendered

Operator targets use the displayed glyph or function name when one exists. For notation with an implicit operator, such as geometric-product juxtaposition, the label spans the whole product occurrence.

Span markers include paired structural brackets: marker="overbracket" lowers to \overbracket{body}^{label}, while marker="underbracket" lowers to \underbracket{body}_{label}. Their directions are fixed; passing the contradictory explicit side is an error.

Cancellation is an inline marker, so it can cross exactly one selected subexpression while a foreground colour de-emphasises that same content:

v = ((e1 | e2) + (e1 ^ e2)).named("v")
cancel_inner = ga.annotator(
    ga.on(
        ga.subexpression(e1 | e2),
        color="lightgrey",
        marker="cancel",
    )
)
cancel_inner(v)

This lowers the selected subtree to \cancel{\textcolor{lightgrey}{e_1 \cdot e_2}}. The related bcancel and xcancel markers are also available. They are content decorations, so overlay=True is intentionally rejected.

To discover those operations from retained provenance rather than naming each subtree manually, build a reusable automatic recipe:

cancel_zero_operations = ga.cancel_zeros(color="#aaaf")
cancel_zero_operations(v)

ga.cancel_zeros() uses exact coefficient zero by default and crosses out the innermost zero-valued operation subtrees. Use an explicit absolute tolerance for numerical lessons, for example ga.cancel_zeros(atol=1e-12). It does not cancel literal zeros or symbolically rewrite the expression. Subtrees that depend on unresolved named symbols are skipped; independently evaluable descendants can still match. The underlying selector is available as ga.zero_subexpressions(atol=...) for composing a different rule.

Revealing named-variable definitions

Named intermediates keep a large expression readable. A variable annotation can reveal one provenance layer without expanding the expression itself:

u = (e1 + e2).named("u")
v = (e2 - e3).named("v")
B = (u ^ v).named("B")
x = (e1 + 2 * e3).named("x")
a = (B * x).named("a")

layered = ga.annotate(
    a,
    ga.on(ga.variable("B"), label_latex=B.latex(content="expr"), marker="rule"),
    ga.on(ga.variable("x"), label_latex=x.latex(content="expr"), marker="rule"),
)

The labels come from the named values' retained expressions rather than handwritten copies. Apply the same pattern to B to reveal u and v one layer further back. See examples/annotation/expression_provenance_layers.py for the complete teaching notebook.

Targeting a subtree

ga.subexpression(value, occurrence=...) finds a subtree by its recorded provenance tree instead of its operand position, so the rule survives unrelated edits to the enclosing expression. Matching is structural, not numeric.

rotor = (1 + e1 * e2).named("R")
sandwich = (rotor * e1 * ~rotor).named("w")
subtree = ga.annotate(sandwich, ga.on(ga.subexpression(~rotor), label="reverse factor"))
assert r"\widetilde{R}" in subtree.latex()

It needs a tracked value (expr=True). Use occurrence=n to pick one of several matches, or the default "all".

Annotating signs

ga.sign(blade) selects the displayed sign of a term on its own, so a rule can highlight a + or - without touching the coefficient or blade. ga.signs(*blades) selects several.

value = (0.5 + 2 * e1 - 3 * (e1 ^ e2)).named("A")
signed = ga.annotate(value, ga.on(ga.sign(e1 ^ e2), background="#fff3cd"))
assert r"\colorbox{#fff3cd}{$-$}" in signed.latex()
assert r"2 e_{1}" in signed.latex()

A sign is only selectable when it is visible, so a positive leading term matches nothing. Sign decorations reuse the ordinary styles and markers and compose with grade fills and joined term spans.

Term targets exclude their sign by default, and that boundary is shared by the fill, marker, and label measurement. Opt into a signed term explicitly:

unsigned = ga.term(e2)
signed = ga.term(e2, include_sign=True)

For signed, a visible leading or non-leading +/- is included in every visual layer. A first positive term still has no displayed +, so none is invented. Grade targets and ordinary term targets leave the sign outside. To extend a joined fill through its leading sign independently, select that sign with the same background; the renderer fuses the two targets into one continuous box and gives a callout split from that rule the same extent.

Targeting the name, expression, and value

A tracked value can display as name = expr = value. ga.content(kind) selects one part, where kind is "name", "expr", or "value".

parts = ga.annotate(
    area,
    ga.on(ga.content("expr"), background="#fff3cd"),
    ga.on(ga.content("value"), color="royalblue"),
)
full = parts.latex(content="full")
assert r"\colorbox{#fff3cd}{$" in full
assert r"\textcolor{royalblue}{" in full
assert r"\quad = \quad" in full

A part target respects the active content setting: ga.content("name") matches nothing when only the value side is displayed.

Presenter composition

from galaga import presets

presented = presets.presenters.lengyel()(ga.annotate(area, label="area"))
assert isinstance(presented, ga.Annotated)
assert r"\overset{\text{area}}" in presented.latex()

annotator_presenter = ga.AnnotationPresenter(base=presets.presenters.functional())
composed = annotator_presenter(ga.annotate(area, label="area"))
assert isinstance(composed, ga.Annotated)
assert composed.rules == presented.rules

Ordinary presenters compose through the core adapter hook, and AnnotationPresenter applies a base presenter explicitly. Both preserve the annotation rules and the captured presentation.

Matrix cell and region annotations

Matrix representations from the optional galaga_matrix companion use the same rules and styles. Targets select a cell, row, column, index list, or rectangular block by zero-based coordinates, and a region label anchors to the region's first cell.

import importlib.util

if importlib.util.find_spec("galaga_matrix") is not None:
    import numpy as np
    from galaga_matrix import MatrixRepr

    matrix = MatrixRepr(np.arange(4).reshape(2, 2))
    view = ga.annotate(
        matrix,
        ga.on(ga.cell(0, 0), background="#e8f5e9", label="origin"),
        ga.on(ga.block(rows=slice(1, 2), columns=slice(0, 2)), border="seagreen"),
    )
    assert r"\overset{\text{origin}}" in view.latex()
    assert r"\fcolorbox{seagreen}{transparent}" in view.latex()

Out-of-range coordinates raise ValueError; a valid but empty selection follows the rule's missing policy. The matrix adapter is optional, and importing galaga_annotation never imports galaga_matrix.

CGA object highlights

from galaga import Algebra, outer_product, presets
from galaga.cga import ConformalModel

cga = ConformalModel(Algebra(config=presets.lengyel_cga(), expr=True), expr=True)
dipole = outer_product(cga.up((0.75, 1.0, 2.0)), cga.up((-0.25, 0.1, 0.2)))

object_highlight = ga.highlight_cga(cga)
assert ga.classify_cga(dipole, cga).kind == "dipole"
object_view = object_highlight(dipole)
assert object_view.latex().count(r"\colorbox{#b8e6bf}") == 1
assert object_view.latex().count(r"\colorbox{#d8c4ee}") == 1
assert object_view.latex().count(r"\overbrace") == 4
assert r"\overgroup" not in object_view.latex()

round_point = cga.up((0.5, -0.75, 1.25)).without_expr()
p = cga.up((0.75, 1.0, 2.0))
q = cga.up((-0.25, 0.1, 0.2))
r = cga.up((0.0, 1.0, 0.0))
s = cga.up((0.0, 0.0, 1.0))
circle = outer_product(p, q, r).without_expr()
sphere = outer_product(p, q, r, s).without_expr()

round_point_view = object_highlight(round_point)
circle_view = object_highlight(circle)
sphere_view = object_highlight(sphere)
assert ga.classify_cga(round_point, cga).kind == "round point"
assert ga.classify_cga(circle, cga).kind == "circle"
assert ga.classify_cga(sphere, cga).kind == "sphere"
assert all(label in round_point_view.latex() for label in ("origin", "position", "infinity"))
assert all(
    label in circle_view.latex()
    for label in ("carrier plane", "flat line", "cocarrier direction", "cocarrier moment")
)
component_circle_view = ga.highlight_cga(cga, decomposition="components")(circle)
assert all(
    label in component_circle_view.latex()
    for label in ("plane part", "center part", "flat part", "flat weight")
)
assert "origin part" in sphere_view.latex()

classify_cga currently targets three-dimensional Euclidean CGA. It recognizes round points, flat points, dipoles, lines, circles, planes and spheres, while distinguishing grade-one dual planes from dual spheres and rejecting non-simple candidates or models with a different spatial dimension. Its predicates and cga_parts() selections are stable under ordinary nonzero projective rescaling. highlight_cga(model) defaults to an incidence decomposition, using carrier/cocarrier geometry where a specialized layout is available. highlight_cga(model, decomposition="components") keeps the component-role decomposition, which explains the four Lengyel round/flat bulk/weight families with object-specific labels. For circles, the incidence view labels the carrier plane, flat line, cocarrier direction, and cocarrier moment shown in Lengyel's classification. The incidence view of a dipole shows its carrier line and flat point as contiguous highlights, with "cocarrier normal" and "cocarrier position" overbraces overlaid above subsets of those highlights. Term-span markers choose overlay lowering automatically. When a callout overlaps a highlighted span, its measurement includes a zero-width transparent copy of the fill's padding and border, including any nested fills, smashed toward the expression so only the marker's outward side grows. The bracket or label therefore sits outside the highlight instead of crossing into it, while the fill's visible box and the marker's horizontal extent stay unchanged. clearance remains available when a lesson wants additional separation from the terms. Their cyan colour applies to the bracket chrome only, and the "carrier line"/"flat point" labels use darkened matching shades via label_color. cga_parts exposes the Lengyel component families for custom recipes.

See examples/annotation/span_composition.py for equal, nested, disjoint and crossing intervals, including one highlight with independent callouts above and below.

The cocarrier callout style is selectable with over_marker: "overbrace" (the default), "overbracket", "overline", or "overgroup". Braces and brackets are preferred for teaching layouts; group accents remain available when their distinct appearance is intentional. ga.highlight_object is an alias of ga.highlight_cga.

overbraced = ga.highlight_object(cga, over_marker="overbrace")(dipole)
rendered = overbraced.latex(content="value")
assert r"\overbrace" in rendered
assert r"\overgroup" not in rendered

Browser geometry tests

String and standalone-KaTeX tests run with the normal package suite. To verify actual marker and label alignment in Marimo's KaTeX CSS using pinned headless Chromium, run:

make test-galaga-annotation-browser

The target installs Playwright's browser artifact through UV on first use; it does not control or depend on a desktop browser.

Metadata

Release files for galaga-annotation 2.0.0b3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for galaga-annotation 2.0.0b3
File Size Uploaded Provenance Actions
galaga_annotation-2.0.0b3.tar.gz 72.5 kB Without provenance Details for galaga_annotation-2.0.0b3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for galaga-annotation 2.0.0b3
File Interpreter ABI Platform Actions
galaga_annotation-2.0.0b3-py3-none-any.whl Python 3 none any Details for galaga_annotation-2.0.0b3-py3-none-any.whl

Total release size: 115.0 kB

Release files / galaga_annotation-2.0.0b3.tar.gz

Download URL galaga_annotation-2.0.0b3.tar.gz
Size 72.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1cf92e3c28ecbb005fb3732868c4aa1ab3685f6ce9c710b41f2e580adec7293b
BLAKE2b-256 checksum
How to use checksums
23410e0fb723e49ee3a2178f2a8a04f9c963d51829e3f1cd87229e2ca66865f3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / galaga_annotation-2.0.0b3-py3-none-any.whl

Download URL galaga_annotation-2.0.0b3-py3-none-any.whl
Size 42.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
00ee28527baac1d385a049117df8d5e000764ce0576ffd95ce19c905667ffa3f
BLAKE2b-256 checksum
How to use checksums
913338990263c5cf3ce3b787ad60843fa17fc80d1eb7abb0e0e7bc4e9cb07620
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for galaga-annotation 2.0.0b3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page