Functional geometry as an immutable, decidable resolver graph.
Project description
fungeom
A decidability substrate — immutable, lazily-evaluated resolver graphs where partiality is first-class. Geometry is instance #1.
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)orUnresolvable(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 likea.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
- Wiki — narrative guides: the core concepts, each layer, and how to add a primitive.
docs/reference.md— the complete combinator table, architecture, and design notes.- What belongs here —
docs/substrate-membership.md: fungeom as a general decidability substrate (geometry instance #1, time #2), and the honest-referential-transparency rule for what is admitted vs. parked. - Deep dives —
docs/time.md,docs/collections.md,docs/regions.md.
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fungeom-0.6.0.tar.gz.
File metadata
- Download URL: fungeom-0.6.0.tar.gz
- Upload date:
- Size: 192.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13cf9577306d99836599f9832f4f7184b91e70f7ee6751a83942b457af9e2b24
|
|
| MD5 |
d485b0a6a20f9d92fc399cb2d3b76b22
|
|
| BLAKE2b-256 |
508b89f6ac4ad785a4c0c49cc3e82935061ea7a7ed940361fdbf56c4ac782288
|
Provenance
The following attestation bundles were made for fungeom-0.6.0.tar.gz:
Publisher:
release.yml on ryanrudes/fungeom
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fungeom-0.6.0.tar.gz -
Subject digest:
13cf9577306d99836599f9832f4f7184b91e70f7ee6751a83942b457af9e2b24 - Sigstore transparency entry: 2018635863
- Sigstore integration time:
-
Permalink:
ryanrudes/fungeom@d260dc476e797b36a77036f0a84eead2f78becfe -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/ryanrudes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d260dc476e797b36a77036f0a84eead2f78becfe -
Trigger Event:
push
-
Statement type:
File details
Details for the file fungeom-0.6.0-py3-none-any.whl.
File metadata
- Download URL: fungeom-0.6.0-py3-none-any.whl
- Upload date:
- Size: 438.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
343b6e0e598aef4719eacc20b88943d91b39365b882e5c173bb53c8d114db14e
|
|
| MD5 |
fe2e7598c38b087de1f777c808908473
|
|
| BLAKE2b-256 |
9bdfccd668c1ecea28a242f6804b8072e40c2b6a59fefefa26587f3d3ccbf5a6
|
Provenance
The following attestation bundles were made for fungeom-0.6.0-py3-none-any.whl:
Publisher:
release.yml on ryanrudes/fungeom
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fungeom-0.6.0-py3-none-any.whl -
Subject digest:
343b6e0e598aef4719eacc20b88943d91b39365b882e5c173bb53c8d114db14e - Sigstore transparency entry: 2018635995
- Sigstore integration time:
-
Permalink:
ryanrudes/fungeom@d260dc476e797b36a77036f0a84eead2f78becfe -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/ryanrudes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d260dc476e797b36a77036f0a84eead2f78becfe -
Trigger Event:
push
-
Statement type: