{"id":42287318,"url":"https://github.com/stestagg/simplekalman","last_synced_at":"2026-01-27T09:21:47.087Z","repository":{"id":334121385,"uuid":"1140147802","full_name":"stestagg/simplekalman","owner":"stestagg","description":"A high-level kalman filter library","archived":false,"fork":false,"pushed_at":"2026-01-23T00:59:48.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-23T15:48:20.711Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stestagg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-22T22:18:07.000Z","updated_at":"2026-01-23T00:59:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/stestagg/simplekalman","commit_stats":null,"previous_names":["stestagg/simplekalman"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/stestagg/simplekalman","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stestagg%2Fsimplekalman","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stestagg%2Fsimplekalman/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stestagg%2Fsimplekalman/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stestagg%2Fsimplekalman/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stestagg","download_url":"https://codeload.github.com/stestagg/simplekalman/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stestagg%2Fsimplekalman/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28810481,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T07:41:26.337Z","status":"ssl_error","status_checked_at":"2026-01-27T07:41:08.776Z","response_time":168,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-27T09:21:46.488Z","updated_at":"2026-01-27T09:21:47.079Z","avatar_url":"https://github.com/stestagg.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# simplekalman — a high-level Kalman filter library for Python\n\n## Philosophy\n\nKalman filters are *conceptually simple*:\n\n* sensors produce readings over time\n* we combine those readings into a best-guess\n* we keep a confidence score for that guess\n* we can predict forward when no new readings arrive\n\nThe reason people struggle isn’t the algorithm — it’s the **mixing of concerns**:\n\n* domain semantics (GPS vs AR vs IMU)\n* timing \u0026 ordering (late/out-of-order samples)\n* frames \u0026 units\n* noise / trust tuning\n* opaque paper-derived variable names (hx, F, H, Q, R, P)\n* and finally… matrices\n\n**simplekalman**'s design goals are to separate a kalman filter into three layers:\n\n1. The **user-oriented layer** - Understanding at this level should not require knowledge of Kalman filter internals or terminology. Users should be able to express their problem in terms of sensors, observations, and desired estimates.\n2. The **conceptual implementation layer** - This layer handles data primitives, timing, data conversion, and has the logic of the filter, but expressed as operations on objects, rather than the underlying math.\n3. The **numeric kernel layer** - This layer implements the actual mathematical operations (matrices, Jacobians, etc.).\n\n\nWhy each layer is important:\n *  **user** - Even if you understand all of the detail of Kalman Filters, it's easy to get trivial things wrong when dealing with conversions, normalization etc.  An abstracted layer that deals with the semantics of the problem makes it easier to get the base setup and inputs/outputs correct.\n  * **conceptual** - The intended logic of the implemenation can be hard to understand when it's encoded as a series of matrix operations.  This layer should, (assuming the underlying kernels/operations are correct) be understandable and easily verifiablel\n  * **numeric** - Individual operations and matrix operations are well understood, and are easy to verify in isolation.\n\n\n\u003e **Sensors → Observations → Filter → Predictions**\n\nNo matrices. No Jacobians. No “hx() / F / H / Q / R” leaking into user code.\n\nWe want an API where a user describes their problem in *plain measurement terms* and the library does the numeric work underneath.\n\n---\n\n## Core concepts (the user-facing vocabulary)\n\n### Sensor\n\nA **Sensor** is a configured source of information:\n\n* it has an identity (`name`)\n* it declares what it measures (e.g. `\"YAW\"`, `\"POSITION_2D\"`, `\"POSE_2D\"`)\n* it declares its units (including delta/rate semantics)\n* it declares how noisy it is (standard deviation)\n* it optionally declares how to handle outliers / late data\n\nA sensor is not “a physical thing” (gyro, GPS chip).\nIt is an **abstract measurement stream**.\n\nExamples:\n\n* `\"YAW\"` in `deg/s` (yaw rate)\n* `\"POSE_2D\"` in `m/sample` + `deg/sample` (delta pose per sample)\n* `\"POSITION_2D\"` in `m` (absolute position)\n\n---\n\n### Observation\n\nAn **Observation** is one timestamped reading from a Sensor.\n\nIt's just data + metadata:\n\n* `time`\n* sensor name\n* measurement `values`\n* optional per-sample `accuracy` (can override sensor's default standard deviation)\n\nObservations are *events*. You pass them into the filter as they arrive.\n\n---\n\n### Estimate\n\nAn **Estimate** is what the user wants out: the current best guess of motion.\n\nWe treat “prediction” as **a kind of estimate**:\n\n* an estimate produced by integrating forward between samples\n* or refined by incorporating observations\n\nSo users interact with **one runtime object**:\n\n* `kf.prediction` (the current estimate snapshot)\n\nThe estimate is a *noun*: “this is what we believe right now”.\n\n---\n\n### KalmanFilter\n\n`KalmanFilter` is the thing you use.\n\n* You configure it with an Estimate type (e.g. `MotionEstimate`)\n* You declare your sensors\n* You feed it observations\n* You read out predictions (estimates)\n\nIt owns:\n\n* current prediction\n* sensor registry\n* timing/orchestration behavior\n* robustness policies\n\nUsers do **not** supply mathematical models.\nThey supply **sensor semantics + noise** and select a **motion-between-samples assumption**.\n\n---\n\n## Design goals\n\n### 1) Human-first interface\n\nUsers should not need to know KF internals to use the library correctly.\n\nThe API must support a mental model like:\n\n\u003e “I have sensors. They produce readings. I want a motion estimate.”\n\n### 2) Noise as real-world “trust”\n\nNoise parameters represent:\n\n\u003e the standard deviation (1σ) of observation error vs reality\n\u003e in the same units as the measurement\n\nThis keeps tuning intuitive.\n\n### 3) Units encode meaning\n\nWe prefer a single abstraction for “is this absolute, delta, or rate?”:\n\n✅ **Units strings carry baseline semantics**\n\n* `\"deg\"` → absolute yaw at a time\n* `\"deg/s\"` → yaw rate\n* `\"deg/sample\"` → yaw delta per sample\n* `\"m\"` → absolute position\n* `\"m/sample\"` → translation delta per sample\n* `\"m/s\"` → velocity\n\nSo users don’t have to learn a separate `form=ABSOLUTE|DELTA|RATE`.\n\n(Internally the library may decode these to the appropriate handling.)\n\n### 4) Motion-between-samples is explicit\n\nA Kalman filter must assume *something* happens between observations.\nThis is unavoidable, so we expose it directly in the most human terms possible:\n\n\u003e “How should motion behave between samples?”\n\nPresets:\n\n* `UNCHANGING` — stays where it was unless corrected\n* `SMOOTH` — tends to keep moving steadily\n* `AGILE` — can change quickly\n* `INERTIAL` — rate/delta sensors can drive between-sample motion\n\nThis setting belongs to the **Estimate** configuration, not the whole library.\n\n### 5) Policies can be per-sensor\n\nIt makes sense to treat different sensors differently:\n\n* GPS might need aggressive outlier rejection\n* AR deltas might be mostly reliable but occasionally catastrophic\n* yaw-rate streams should often be accepted continuously\n\nSo:\n\n* `outlier_handling` can live on sensors\n* `late_data_policy` can live on sensors\n\nSome late-data modes (reorder/replay) require filter history support, but the *declaration* can still be sensor-level.\n\n### 6) Diagnostics are optional and pluggable\n\nDiagnostics are useful, but should not distract from the core API.\n\nIf included, they should be:\n\n* off by default\n* implemented as a lightweight protocol / callback interface\n* called by the filter at key events (predict/update/reject/reset)\n\n---\n\n# Sensor specification design\n\n## Canonical internal model (what everything compiles to)\n\nUser input compiles down to a fully explicit spec:\n\n```python\nSensorSpec(\n  name=\"arkit_delta_pose\",\n  fields={\n    \"x\": FieldSpec(kind=\"distance\", semantics=\"delta_per_sample\", unit=\"m/sample\", sigma=0.05),\n    \"y\": FieldSpec(kind=\"distance\", semantics=\"delta_per_sample\", unit=\"m/sample\", sigma=0.05),\n    \"yaw\": FieldSpec(kind=\"angle\", semantics=\"delta_per_sample\", unit=\"rad/sample\", sigma=0.5),\n  }\n)\n```\n\nKey: **field names exist even if the user didn't specify them**. For `measures=\"POSITION_2D\"` you get default fields `x, y`. For `POSE_2D` default fields are `x, y, yaw`.\n\nThe \"sensor gives deltas\" semantics isn't something the user must restate for each field — it's implied by the **measure schema** (and optionally reinforced by unit strings).\n\n## Measure schemas\n\nEach measure schema declares its fields with a `kind` per field:\n\n| Measure Schema | Fields |\n|----------------|--------|\n| `POSITION_2D` | `x(distance), y(distance)` |\n| `POSE_2D` | `x(distance), y(distance), yaw(angle)` |\n| `YAW` | `yaw(angle)` |\n\nRate vs absolute vs delta is determined by units (e.g. `deg` vs `deg/s` vs `deg/sample`), not by the measure schema.\n\nThis makes domain-map mode robust and keeps user code clean.\n\n## Three user input forms (compile-time sugar)\n\n### A) Single units + single sigma (broadcast)\n\nGood for homogeneous components:\n\n```python\nSensor(name=\"gps\", measures=\"POSITION_2D\", units=\"m\", standard_deviation=2.5)\n```\n\nRules:\n\n* expand units to each field (`x, y`)\n* expand sigma to each field\n* semantics for each field comes from `measures` (absolute vs delta vs rate)\n\n### B) Domain map (distance vs angle, etc.)\n\nGood for mixed-dimension observations like pose:\n\n```python\nSensor(\n  name=\"arkit_delta_pose\",\n  measures=\"POSE_2D\",\n  units={\"distance\": \"m/sample\", \"angle\": \"rad/sample\"},\n  standard_deviation={\"distance\": 0.05, \"angle\": 0.5},\n)\n```\n\nRules:\n\n* each field has a `kind` (distance/angle/…); map applies by `kind`\n* if a field's kind is missing from the dict → error (or require a fallback)\n\nThis is the sweet spot for \"don't make me repeat it\" while still supporting mixed-dimension observations.\n\n### C) Full per-field spec\n\nPower users / unusual sensors:\n\n```python\nSensor(\n  name=\"arkit_delta_pose\",\n  measures=\"POSE_2D\",\n  fields={\n    \"x\": {\"units\": \"m/sample\", \"standard_deviation\": 0.03},\n    \"y\": {\"units\": \"m/sample\", \"standard_deviation\": 0.06},\n    \"yaw\": {\"units\": \"rad/sample\", \"standard_deviation\": 0.4},\n  },\n)\n```\n\nRules:\n\n* explicit wins\n* you can still allow partial field specs with fallback to (A) or (B)\n\n## Validation / resolution logic\n\nResolution precedence (simple, deterministic):\n\n1. per-field override (`fields[\"x\"].units`)\n2. domain map (`units[\"distance\"]`)\n3. scalar broadcast (`units=\"m\"`)\n\nSame precedence applies for standard deviation.\n\n---\n\n# High-level API (as it stands)\n\n## Creating a filter\n\nBob wants to fuse three sensors into one `MotionEstimate`.\n\n```python\nfrom simplekalman import KalmanFilter, MotionEstimate, Outliers, LateData, Sensor\n\nkf = KalmanFilter(\n    estimate=MotionEstimate(\n        motion_between_samples=\"SMOOTH\",   # UNCHANGING | SMOOTH | AGILE | INERTIAL\n        stale_after_s=2.0,                 # optional freshness threshold\n    ),\n    sensors=[\n        Sensor(\n            name=\"gps\",\n            measures=\"POSITION_2D\",\n            units=\"m\",\n            standard_deviation=2.5,\n            outliers=Outliers.REJECT_LIKELY,\n            late_data=LateData.REORDER_WITHIN(seconds=0.25),\n        ),\n        Sensor(\n            name=\"yaw_rate\",\n            measures=\"YAW\",\n            units=\"deg/s\",\n            standard_deviation=1.0,\n            outliers=Outliers.ACCEPT_ALL,\n            late_data=LateData.IGNORE,\n        ),\n        Sensor(\n            name=\"arkit_delta_pose\",\n            measures=\"POSE_2D\",\n            units={\"distance\": \"m/sample\", \"angle\": \"deg/sample\"},\n            standard_deviation={\"distance\": 0.05, \"angle\": 0.5},\n            outliers=Outliers.REJECT_LIKELY,\n            late_data=LateData.IGNORE,\n        ),\n    ],\n)\n```\n\nNotes:\n\n* `Sensor(...)` is intentionally *abstract* (“what is measured”) not physical (“gyro”, “GPS module”).\n* Units communicate whether the observation is absolute, per-second, or per-sample.\n\n---\n\n## Feeding observations\n\nUser code pushes observations as data arrives:\n\n```python\nkf.observe(\"gps\", time=1000.00, values={\"x\": 1.2, \"y\": -3.4})\nkf.observe(\"yaw_rate\", time=1000.01, values={\"yaw\": 12.0})\nkf.observe(\"arkit_delta_pose\", time=1000.02, values={\"x\": 0.03, \"y\": 0.00, \"yaw\": 0.2})\n```\n\nOptional per-sample quality can override sensor defaults:\n\n```python\nkf.observe(\"gps\", time=1000.00, values={\"x\": 1.2, \"y\": -3.4}, accuracy={\"x\": 1.0, \"y\": 1.0})\n```\n\n(Exact fields depend on `measures=...`; the library validates them.)\n\nKey rule:\n\n* observations are timestamped\n* filter moves forward in time automatically as needed\n\n---\n\n## Getting predictions / estimates\n\nThe primary output is always an estimate snapshot:\n\n```python\nmotion = kf.prediction\n\nprint(motion.time)\nprint(motion.position)\nprint(motion.velocity)\nprint(motion.heading)\nprint(motion.confidence)   # shape TBD\n```\n\nOptional convenience:\n\n```python\nfuture = kf.predict(t=1001.00)\n```\n\n---\n\n# Roles and ownership\n\n## Sensor owns\n\n* identity\n* what it measures\n* units\n* standard deviation (trust)\n* outlier and late-data policy\n\nIt does **not** own the math.\n\n## MotionEstimate owns\n\n* what fields exist (position/velocity/heading etc.)\n* motion_between_samples preset\n* freshness rules (`stale_after_s` / per-field later)\n\nIt does **not** own the fusion logic.\n\n## KalmanFilter owns\n\n* runtime state (“current belief”)\n* sensor registry\n* observation ingestion\n* ordering rules\n* prediction step execution\n* update step execution\n* choosing internal filter method (EKF/UKF/etc.) *without exposing it*\n\n---\n\n# Things explicitly *not* exposed (by design)\n\nUsers do not write:\n\n* matrices\n* Jacobians\n* `Q` / `R` / `P` directly\n* measurement functions like `hx(x)`\n* frame transforms as math primitives\n\nInstead users provide:\n\n* **meaning**\n* **units**\n* **standard deviation**\n* and choose a motion style preset\n\nInternals can still be very sophisticated — but that sophistication stays below the surface.\n\n---\n\n# Next steps (what this document enables)\n\nThis manifesto locks in the high-level architecture:\n\n✅ clean separation of sensor semantics from math\n✅ estimate/prediction as a single user-visible “belief” snapshot\n✅ units-driven observation interpretation\n✅ motion-between-samples as an intuitive choice\n✅ per-sensor policies for robustness\n\nFrom here, we can design the lower layers:\n\n1. **Mid-layer orchestration**\n\n* “predict then update” flow\n* outlier gating hooks\n* late observation handling\n\n2. **Bottom numeric kernel**\n\n* vector/covariance operations\n* angle wrapping\n* stable linear algebra\n* UKF/EKF implementation details hidden behind primitives\n\nThis doc is the baseline: any lower-level design must preserve the user story:\n\n\u003e *Configure sensors and an estimate, feed observations, read predictions — without learning Kalman math.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstestagg%2Fsimplekalman","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstestagg%2Fsimplekalman","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstestagg%2Fsimplekalman/lists"}