pamoja.kit
Idiomatic helper-math facade.
The helpers are named for the goal rather than the technique, with the real algorithm one layer down: smooth a noisy reading, hold a value with a PID, warn before a tank runs dry, and notice when a tracked point leaves its area.
They are synchronous and allocation-free in the core, so this module re-exports
the generated classes rather than wrapping them. What it adds is a
Coordinate for the geo helpers, so a fix travels as one value instead of
a pair of loose floats, and a Boundary enum for the crossing states.
1"""Idiomatic helper-math facade. 2 3The helpers are named for the goal rather than the technique, with the real 4algorithm one layer down: smooth a noisy reading, hold a value with a PID, warn 5before a tank runs dry, and notice when a tracked point leaves its area. 6 7They are synchronous and allocation-free in the core, so this module re-exports 8the generated classes rather than wrapping them. What it adds is a 9:class:`Coordinate` for the geo helpers, so a fix travels as one value instead of 10a pair of loose floats, and a :class:`Boundary` enum for the crossing states. 11""" 12 13from __future__ import annotations 14 15import enum 16from typing import NamedTuple 17 18from pamoja._native import Anomaly, Median, Trend, Window 19from pamoja._native import window_capacity as _window_capacity 20from pamoja._native import Calibration, Debounce, Depletion, Kalman, Pid, Ramp, Smoother, Surge, Thermostat 21from pamoja._native import Geofence as _NativeGeofence 22from pamoja._native import bearing_between as _bearing_between 23from pamoja._native import deadband 24from pamoja._native import distance_between as _distance_between 25 26#: How many readings a windowed helper keeps. 27WINDOW_CAPACITY = _window_capacity() 28 29__all__ = [ 30 "Window", 31 "WINDOW_CAPACITY", 32 "Trend", 33 "Median", 34 "Anomaly", 35 "Boundary", 36 "Calibration", 37 "Coordinate", 38 "Debounce", 39 "Depletion", 40 "Geofence", 41 "Kalman", 42 "Pid", 43 "Ramp", 44 "Smoother", 45 "Surge", 46 "Thermostat", 47 "bearing_between", 48 "deadband", 49 "distance_between", 50] 51 52 53class Coordinate(NamedTuple): 54 """A latitude and longitude in degrees.""" 55 56 #: Degrees north of the equator, negative for south. 57 latitude: float 58 #: Degrees east of the prime meridian, negative for west. 59 longitude: float 60 61 62class Boundary(str, enum.Enum): 63 """Where a fix sits relative to a :class:`Geofence`, including a crossing.""" 64 65 #: The fix is inside the fence and was inside before, or is the first fix inside. 66 INSIDE = "Inside" 67 #: The fix is outside the fence and was outside before, or is the first fix outside. 68 OUTSIDE = "Outside" 69 #: The fix just crossed from inside to outside: the moment to raise a breach alert. 70 EXITED = "Exited" 71 #: The fix just crossed from outside back inside. 72 ENTERED = "Entered" 73 74 75class Geofence: 76 """Keeps a tracked point inside an area, and notices when it leaves. 77 78 A fence is a centre and a radius; feeding it successive fixes reports whether 79 each is inside or outside and, crucially, the single fix that crossed, so an 80 alert fires once on the crossing rather than on every fix while away. 81 82 Example:: 83 84 pen = Geofence(Coordinate(-1.2921, 36.8219), 50.0) 85 pen.update(Coordinate(-1.2921, 36.8219)) # Boundary.INSIDE 86 pen.update(Coordinate(-1.2930, 36.8219)) # Boundary.EXITED 87 """ 88 89 __slots__ = ("_native",) 90 91 def __init__(self, center: Coordinate, radius_m: float) -> None: 92 """Create a circular fence around a centre fix. 93 94 :param center: The centre of the fence. 95 :param radius_m: The fence radius, in metres. 96 """ 97 self._native = _NativeGeofence(center.latitude, center.longitude, radius_m) 98 99 def update(self, point: Coordinate) -> Boundary: 100 """Feed a fix in and report where it sits, including a single crossing. 101 102 :param point: The latest fix. 103 :returns: The boundary state for this fix. 104 """ 105 return Boundary(self._native.update(point.latitude, point.longitude)) 106 107 def contains(self, point: Coordinate) -> bool: 108 """Report whether a fix lies inside, without recording a crossing. 109 110 :param point: The fix to test. 111 :returns: ``True`` if the fix is inside the fence. 112 """ 113 return self._native.contains(point.latitude, point.longitude) 114 115 116def distance_between(origin: Coordinate, destination: Coordinate) -> float: 117 """Return the great-circle distance between two coordinates, in metres. 118 119 :param origin: The coordinate to measure from. 120 :param destination: The coordinate to measure to. 121 :returns: The distance in metres. 122 """ 123 return _distance_between( 124 origin.latitude, origin.longitude, destination.latitude, destination.longitude 125 ) 126 127 128def bearing_between(origin: Coordinate, destination: Coordinate) -> float: 129 """Return the initial bearing from one coordinate to another, in degrees. 130 131 :param origin: The coordinate to measure from. 132 :param destination: The coordinate to measure to. 133 :returns: The bearing in degrees, clockwise from north. 134 """ 135 return _bearing_between( 136 origin.latitude, origin.longitude, destination.latitude, destination.longitude 137 )
A rolling window of the most recent readings, with the stats over them.
Fits a line through recent readings, so a slow drift shows before it matters.
Rejects a single wild reading, where an average would let it pull the answer.
Flags a reading that stands out from the ones around it.
63class Boundary(str, enum.Enum): 64 """Where a fix sits relative to a :class:`Geofence`, including a crossing.""" 65 66 #: The fix is inside the fence and was inside before, or is the first fix inside. 67 INSIDE = "Inside" 68 #: The fix is outside the fence and was outside before, or is the first fix outside. 69 OUTSIDE = "Outside" 70 #: The fix just crossed from inside to outside: the moment to raise a breach alert. 71 EXITED = "Exited" 72 #: The fix just crossed from outside back inside. 73 ENTERED = "Entered"
Where a fix sits relative to a Geofence, including a crossing.
Turns a raw sensor count into the units the reading is actually in.
54class Coordinate(NamedTuple): 55 """A latitude and longitude in degrees.""" 56 57 #: Degrees north of the equator, negative for south. 58 latitude: float 59 #: Degrees east of the prime meridian, negative for west. 60 longitude: float
A latitude and longitude in degrees.
Stops a flickering input from acting until it has settled.
Warns before a falling level runs out, by projecting its rate of fall.
76class Geofence: 77 """Keeps a tracked point inside an area, and notices when it leaves. 78 79 A fence is a centre and a radius; feeding it successive fixes reports whether 80 each is inside or outside and, crucially, the single fix that crossed, so an 81 alert fires once on the crossing rather than on every fix while away. 82 83 Example:: 84 85 pen = Geofence(Coordinate(-1.2921, 36.8219), 50.0) 86 pen.update(Coordinate(-1.2921, 36.8219)) # Boundary.INSIDE 87 pen.update(Coordinate(-1.2930, 36.8219)) # Boundary.EXITED 88 """ 89 90 __slots__ = ("_native",) 91 92 def __init__(self, center: Coordinate, radius_m: float) -> None: 93 """Create a circular fence around a centre fix. 94 95 :param center: The centre of the fence. 96 :param radius_m: The fence radius, in metres. 97 """ 98 self._native = _NativeGeofence(center.latitude, center.longitude, radius_m) 99 100 def update(self, point: Coordinate) -> Boundary: 101 """Feed a fix in and report where it sits, including a single crossing. 102 103 :param point: The latest fix. 104 :returns: The boundary state for this fix. 105 """ 106 return Boundary(self._native.update(point.latitude, point.longitude)) 107 108 def contains(self, point: Coordinate) -> bool: 109 """Report whether a fix lies inside, without recording a crossing. 110 111 :param point: The fix to test. 112 :returns: ``True`` if the fix is inside the fence. 113 """ 114 return self._native.contains(point.latitude, point.longitude)
Keeps a tracked point inside an area, and notices when it leaves.
A fence is a centre and a radius; feeding it successive fixes reports whether each is inside or outside and, crucially, the single fix that crossed, so an alert fires once on the crossing rather than on every fix while away.
Example::
pen = Geofence(Coordinate(-1.2921, 36.8219), 50.0)
pen.update(Coordinate(-1.2921, 36.8219)) # Boundary.INSIDE
pen.update(Coordinate(-1.2930, 36.8219)) # Boundary.EXITED
92 def __init__(self, center: Coordinate, radius_m: float) -> None: 93 """Create a circular fence around a centre fix. 94 95 :param center: The centre of the fence. 96 :param radius_m: The fence radius, in metres. 97 """ 98 self._native = _NativeGeofence(center.latitude, center.longitude, radius_m)
Create a circular fence around a centre fix.
Parameters
- center: The centre of the fence.
- radius_m: The fence radius, in metres.
100 def update(self, point: Coordinate) -> Boundary: 101 """Feed a fix in and report where it sits, including a single crossing. 102 103 :param point: The latest fix. 104 :returns: The boundary state for this fix. 105 """ 106 return Boundary(self._native.update(point.latitude, point.longitude))
Feed a fix in and report where it sits, including a single crossing.
Parameters
- point: The latest fix. :returns: The boundary state for this fix.
108 def contains(self, point: Coordinate) -> bool: 109 """Report whether a fix lies inside, without recording a crossing. 110 111 :param point: The fix to test. 112 :returns: ``True`` if the fix is inside the fence. 113 """ 114 return self._native.contains(point.latitude, point.longitude)
Report whether a fix lies inside, without recording a crossing.
Parameters
- point: The fix to test.
:returns:
Trueif the fix is inside the fence.
Estimates a true value from noisy readings, trusting the model and the sensor in proportion to how noisy each is.
Holds a value at a setpoint by trading off present, past, and predicted error.
Limits how fast a value may change, so a load is never slammed.
Smooths a noisy reading by weighting each new sample against the running value.
Notices a step change between successive readings, such as a burst pipe.
Switches a load on and off around a setpoint, with hysteresis to stop chatter.
Creates a cooling thermostat, which switches on when the reading rises.
129def bearing_between(origin: Coordinate, destination: Coordinate) -> float: 130 """Return the initial bearing from one coordinate to another, in degrees. 131 132 :param origin: The coordinate to measure from. 133 :param destination: The coordinate to measure to. 134 :returns: The bearing in degrees, clockwise from north. 135 """ 136 return _bearing_between( 137 origin.latitude, origin.longitude, destination.latitude, destination.longitude 138 )
Return the initial bearing from one coordinate to another, in degrees.
Parameters
- origin: The coordinate to measure from.
- destination: The coordinate to measure to. :returns: The bearing in degrees, clockwise from north.
Suppresses movement within width of center, so noise does not act.
117def distance_between(origin: Coordinate, destination: Coordinate) -> float: 118 """Return the great-circle distance between two coordinates, in metres. 119 120 :param origin: The coordinate to measure from. 121 :param destination: The coordinate to measure to. 122 :returns: The distance in metres. 123 """ 124 return _distance_between( 125 origin.latitude, origin.longitude, destination.latitude, destination.longitude 126 )
Return the great-circle distance between two coordinates, in metres.
Parameters
- origin: The coordinate to measure from.
- destination: The coordinate to measure to. :returns: The distance in metres.