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"
- Package source
- SPEC-015: Expression and Matrix Annotations
- ADR-142: Reusable Callable Annotators
- ADR-147: KaTeX Annotation Lowering
- ADR-148: CGA Object Classification
- ADR-149: Matrix Cell and Region Annotations
- ADR-150: Sign-Only Annotation Targets
- ADR-151: Content-Part Annotation Targets
- ADR-152: Subexpression Annotation Targets
- ADR-153: Configurable CGA Cocarrier Markers
- ADR-154: Independent External Span Overlays
- ADR-155: Internal Decoration Boundary and Lockstep Dependency Floors
- ADR-156: Headless Browser Geometry Contracts
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.0b2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| galaga_annotation-2.0.0b2.tar.gz | 72.2 kB | Without provenance | Details for galaga_annotation-2.0.0b2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| galaga_annotation-2.0.0b2-py3-none-any.whl | Python 3 | none | any | Details for galaga_annotation-2.0.0b2-py3-none-any.whl |
Total release size: 114.7 kB
Release files / galaga_annotation-2.0.0b2.tar.gz
| Download URL | galaga_annotation-2.0.0b2.tar.gz |
|---|---|
| Size | 72.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f1aac780937d93751e5d1478080721eab1234d9ee76fa013dc39ccd03f47a7d7
|
|
BLAKE2b-256 checksum How to use checksums |
f6bd9e5ef7a18bc7f7c12a86e8c19409036dd261ad63da0c067586d0dddf4dc8
|
| 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.0b2-py3-none-any.whl
| Download URL | galaga_annotation-2.0.0b2-py3-none-any.whl |
|---|---|
| Size | 42.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3f8294a8fcc0a358590dc63325fabb65b19ff5a4156f87eeeec6baf6fd19b0ec
|
|
BLAKE2b-256 checksum How to use checksums |
bd2e36af8ecc564eff30d2ff71ff16ebc08f20d6e214feb222a9c9372dc9c65e
|
| 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.0b2
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.