Skip to main content

fungeom

A decidability substrate — immutable, lazily-evaluated resolver graphs where partiality is first-class. Geometry is instance #1.

CI Python 3.13+ Coverage 100% License Apache-2.0


fungeom is a decidability substrate — a Python library for building computation as a lazy, immutable graph you can reason about before you compute it. You compose typed values, ask whether the result can be resolved, and — when it can't — get back a reason, not an exception or a silent NaN.

Geometry is instance #1 and the most developed: you build points, vectors, frames, transforms, time-signals, and regions, and every question about them is decidable. Time is instance #2 — the same machinery, one dimension down — and the two compose (a signal is a value that moves). The substrate isn't geometry-specific; geometry is just the first thing modeled in it, and "anything honestly decidable" is the goal.

Its one big idea: partiality is first-class. A question with no answer (a point in a frame that was never placed, a direction from a zero-length vector, a marker occluded mid-capture) is an honest Unresolvable with an explanation that propagates through everything built on top of it — never a crash, never an invented number. And that partiality is the bottom of an uncertainty lattice: binary (Resolvable / Unresolvable) today, but designed to grade toward resolvable-within-ε and beyond — a refinement of fungeom's own partiality, not a bolt-on. What belongs in the substrate is therefore selected on honesty and referential transparency, not on the kind of math (what belongs here).

from fungeom import Point3, Frame, Resolvable, Unresolvable

gripper = Frame.detached("gripper")          # a sub-assembly, not yet placed in the world
tip = Point3.at(0, 0, 0.1, frame=gripper)    # a point in the gripper's frame — built lazily

match tip.decide():                          # ask whether it can be resolved...
    case Resolvable(point):  print(point.coord)
    case Unresolvable(why):  print(why)      # "frame 'gripper' is not grounded to the world"

Nothing above is computed until you ask. decide() returns evidence — the value if it resolves, or the reason if it doesn't — and resolve() is just decide().unwrap(), so the two can never disagree.

Why fungeom

  • Decidable, not crash-prone. Every resolver answers decide() → Resolvable(value) or Unresolvable(reason). Partiality propagates: a midpoint is resolvable only if both ends are, and the reason flows across type boundaries unchanged.
  • Lazy & immutable. Geometry is a graph of frozen values; every op returns a new node and computes nothing until decide()/resolve(). You can even render the graph to see where an unresolvability lives.
  • Everything is a resolver — even scalars. A scale factor, an interpolation parameter, a vector's norm are all first-class nodes, so values flow across types and dividing by a resolves-to-zero scalar is Unresolvable, not a runtime error.
  • One class per primitive. You both construct from it (classmethods like Vec3.of, Point3.at) and compose with it (fluent methods like a.midpoint(b)). No builders, no visitors.

Install

pip install fungeom

Requires Python 3.13+; the runtime deps (numpy, scipy, rich, shapely) come with it.

For development — from a checkout, with the dev extras (ruff, mypy, pytest):

git clone https://github.com/ryanrudes/fungeom && cd fungeom
uv pip install -e '.[dev]'

The surface, at a glance

Layer Primitives What it's for
Geometry Scalar, Vec2/3, Direction3, Transform, Frame, Point3, Plane, Line, Ray, Segment (+ 2D siblings) points, frames, rigid motion — the classic kit, made decidable
Logic Bool three-valued predicates with strict propagation
Time Duration, Instant, Interval, Coverage, Timeline, Sampling, TimeMap, TimeWarp durations, clocks, gappy supports, alignment
Signals ScalarSignal, Vec3Signal, …, PlaneSignal, BoolSignal, FaceSignal values that vary over time — partial functions of a clock (incl. a moving patch)
Collections …Bundle, …BundleSignal, Roster, RosterMap keyed sets (marker clouds) and sets-over-time, occlusion-aware
Regions Region2, Face, Point2Bundle bounded planar areas, the balance margin, bounded contact patches

The complete combinator table (every constructor, every op, and its exact partiality) lives in docs/reference.md.

A taste — contact detection, end to end

Everything composes and stays lazy. Here is the spine of a real motion-capture task — when is a foot in contact with the ground? — built without ever inventing a number:

clearances = ground_cloud.fit_plane().signed_distance(foot_cloud)  # per-marker height, over time
contact = clearances.min().le(0.0)                                 # a three-valued BoolSignal
print(contact.when_true().resolve())   # the contact interval(s)
print(contact.first_true().resolve())  # touchdown
print(contact.last_true().resolve())   # release

If a marker drops out, the predicate is Unresolvable there — undefined, never silently False. See examples/10_contact_over_time.py for the runnable version.

Examples

Runnable, commented scripts in examples/ (each is exercised by the test suite, so they stay current). Start at the top and work down:

Script Shows
01_quickstart construct → compose → resolve; scalars flowing across types
02_coordinate_frames a kinematic chain; grounding, why an unplaced frame is Unresolvable, and reading a point's coordinates back in a frame
03_decidability_and_partiality value-dependent partialities, reasons, propagation; predicates as decidable Bools
04_visualizing_resolvers rendering the lazy graph to see where an unresolvability lives
05_time_and_clocks the temporal layer: durations/instants, intervals & coverage with gaps
06_signals_over_time signals as partial functions of time; at/resample/reparameterize; slerp on a manifold
07_aligning_and_warping recovering the time map between two recordings from landmarks
08_point_clouds_over_time a Point3Bundle and a cloud over time — an occluded marker is honestly Unresolvable
09_regions_and_patches the 2D region algebra, the balance margin, and a bounded-patch Face
10_contact_over_time the contact spine end-to-end; touchdown & release from marker data
11_free_variables the unknown as a first-class leaf — author a patch as data over free markers, then bind/resolve_in their positions
python examples/01_quickstart.py

Learn more

Development

uv pip install -e '.[dev]'
pytest --cov=fungeom   # tests + 100% coverage gate
ruff check . && ruff format --check .
mypy                   # strict

CI runs all four on every push; the same checks are available as pre-commit hooks (pre-commit install && pre-commit install --hook-type pre-push). Every primitive and combinator, with the checks it has passed, is tracked in CHECKLIST.md.

License

Apache-2.0.

Metadata

Release files for fungeom 0.8.1

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

Source distribution (sdist)

Source distribution for fungeom 0.8.1
File Size Uploaded
fungeom-0.8.1.tar.gz 217.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fungeom 0.8.1
File Interpreter ABI Platform
fungeom-0.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 673.5 kB

Release files / fungeom-0.8.1.tar.gz

Download URL fungeom-0.8.1.tar.gz
Size 217.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3f806e52091dd53ae3b9e321ec9dbcc988f680ccca0d387878b0c5d714ef8387
BLAKE2b-256 checksum
How to use checksums
aa0ca3b251688ceff2841f96f4ac07c001cf70ef7e7b85a1ca71b08a67d08f0c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release files / fungeom-0.8.1-py3-none-any.whl

Download URL fungeom-0.8.1-py3-none-any.whl
Size 456.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d902acd97014db39de238448ff3f06e515c546a1e23f46d1d034207eae0b00e0
BLAKE2b-256 checksum
How to use checksums
fd4ec4b0d567393d79d7fe762ea91f117fd53b13d6553fbc476f07ed943d4134
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.8.1 This release

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

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