Skip to main content

Functional geometry as an immutable, decidable resolver graph.

Project description

fungeom

Functional geometry as an immutable, decidable resolver graph.

CI Python 3.13+ Coverage 100% License Apache-2.0


fungeom is a Python library for building geometry as a lazy, immutable graph you can reason about before you compute it. You compose points, vectors, frames, transforms, time-signals, and regions; ask whether the result can be resolved; and — when it can't — get back a reason, not an exception or a silent NaN.

Its one big idea: partiality is first-class. A geometric 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.

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, and why an unplaced frame is Unresolvable
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
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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fungeom-0.5.0.tar.gz (190.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fungeom-0.5.0-py3-none-any.whl (436.0 kB view details)

Uploaded Python 3

File details

Details for the file fungeom-0.5.0.tar.gz.

File metadata

  • Download URL: fungeom-0.5.0.tar.gz
  • Upload date:
  • Size: 190.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for fungeom-0.5.0.tar.gz
Algorithm Hash digest
SHA256 7e03f87e3eccd951541c5e61e5e11f8a7e121a5c0da83d599d1ddaaafd437650
MD5 edda419cbd62d343b2fa791c4ace7780
BLAKE2b-256 edcdd7fbc8aaded5826a05270ccbdca5f1167785faba468ecf7e4b8924dc6f81

See more details on using hashes here.

Provenance

The following attestation bundles were made for fungeom-0.5.0.tar.gz:

Publisher: release.yml on ryanrudes/fungeom

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fungeom-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: fungeom-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 436.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for fungeom-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a7b7aaf06f43bf7ef83d73cc84c46522057845c8f6bd407701414a8bf0e47b6a
MD5 bf1ead5e5f1cf7fc12e63bd1440b22e2
BLAKE2b-256 12d20194cae50da325d8cbd238dc0b2a414ec4c70772c6f89c251fc3eb9d56a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for fungeom-0.5.0-py3-none-any.whl:

Publisher: release.yml on ryanrudes/fungeom

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page