Pamoja.Kit 0.2.0

dotnet add package Pamoja.Kit --version 0.2.0
                    
NuGet\Install-Package Pamoja.Kit -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pamoja.Kit" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pamoja.Kit" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Pamoja.Kit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pamoja.Kit --version 0.2.0
                    
#r "nuget: Pamoja.Kit, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pamoja.Kit@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pamoja.Kit&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Pamoja.Kit&version=0.2.0
                    
Install as a Cake Tool

Pamoja.Kit

Plain-language helper math: smoothing, calibration, PID and thermostat control, a trigger with hysteresis, trend and surge prediction, rolling windows, unit conversions, geo, and robot motion. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.

read the guide documentation API reference

Install

dotnet add package Pamoja.Kit
using Pamoja.Kit;

This pulls in Pamoja.Native, the compiled engine. dotnet add package Pamoja is the whole framework in one package.

Example

The guide project's example, spliced here as it ran in CI.

From bindings/dotnet/samples/Pamoja.Guides/KitGuide.cs:

// The tower's level transmitter reports on a 4-20 mA loop: 4 mA is empty and 20 mA
// is full, so mid-scale is 12 mA, and a dead loop reads below empty rather than as
// empty.
Calibration level = Calibration.TwoPoint(4.0f, 0.0f, 20.0f, 100.0f);
(float mid, float empty, float dead) = (level.Apply(12.0f), level.Apply(4.0f), level.Apply(0.0f));
Console.WriteLine(Invariant($"level     12 mA reads {mid:F1}%, 4 mA reads {empty:F1}%, a dead loop {dead:F1}%"));

// One dropout in five readings: the median of the five ignores it, the mean does
// not.
using var median = new Median(5);
using var recent = new Window(5);
float held = 0.0f;
foreach (float milliamps in new[] { 12.0f, 12.0f, 0.0f, 12.0f, 12.0f })
{
    held = median.Update(milliamps);
    recent.Push(milliamps);
}

float heldPercent = level.Apply(held);
float meanPercent = level.Apply(recent.Mean()!.Value);
Console.WriteLine(Invariant(
    $"level     through a dropout the median holds {heldPercent:F1}%, the mean falls to {meanPercent:F1}%"));

// Water sloshing in the tower swings the reading. A smoother moves a quarter of
// the way from its last value toward each new reading, so the swing mostly cancels
// out.
using var smoother = new Smoother(0.25f);
using var swing = new Window(6);
foreach (float percent in new[] { 50.0f, 53.0f, 48.0f, 52.0f, 49.0f, 51.0f })
{
    smoother.Update(percent);
    swing.Push(percent);
}

Console.WriteLine(Invariant(
    $"level     sloshing readings from {swing.Min():F1}% to {swing.Max():F1}% smooth to {smoother.Value:F1}%"));

// A Kalman filter is told how noisy the sensor is and how fast the level can really
// move. When the pump starts and the level climbs from 50% to 60%, the one told the
// level barely moves takes the climb for noise and lags; the one told it moves
// keeps up.
using var expectsSteady = new Kalman(0.01f, 2.0f, 50.0f);
using var expectsMotion = new Kalman(0.5f, 2.0f, 50.0f);
foreach (float percent in new[] { 50.0f, 50.0f, 50.0f, 60.0f, 60.0f, 60.0f, 60.0f })
{
    expectsSteady.Update(percent);
    expectsMotion.Update(percent);
}

(float slow, float fast) = (expectsSteady.Estimate, expectsMotion.Estimate);
Console.WriteLine(Invariant(
    $"level     four readings into a rise to 60%, a Kalman filter expecting a steady level reads {slow:F1}%, one expecting motion {fast:F1}%"));

// An accelerometer on the tank watches the tower's lean. Standing still, only gravity
// pulls on it, so the direction of the pull, in g, gives the tilt.
Tilt atRest = Kit.TiltFromAccel(0.0, 0.007, 1.0);
Console.WriteLine(Invariant($"tower     at rest the accelerometer reads a lean of {atRest.Roll:F2} degrees"));

// In wind the tower sways, and the sway's own acceleration swings the accelerometer's
// tilt. A gyro's rate of turn does not swing, but it drifts. A complementary filter
// trusts the gyro from one tenth of a second to the next and the accelerometer over
// time.
using var lean = new Complementary(0.98f, (float)atRest.Roll);
using var gusts = new Window(5);
foreach ((float rate, float tilt) in new[] { (0.4f, 2.1f), (-0.6f, -1.3f), (0.5f, 1.8f), (-0.3f, -0.9f), (0.1f, 1.2f) })
{
    lean.Update(rate, tilt, 0.1f);
    gusts.Push(tilt);
}

float steadyLean = lean.Estimate;
Console.WriteLine(Invariant(
    $"tower     in wind the accelerometer swings from {gusts.Min():F1} to {gusts.Max():F1} degrees; fused with the gyro the lean reads {steadyLean:F1}"));

// The refill pump starts at 40% and stops at 60%: on/off control with a band either
// side of 50. Starting when the level falls is the direction Heating names.
using var pump = Thermostat.Heating(50.0f, 10.0f);
var states = new List<string>();
foreach (float percent in new[] { 50.0f, 39.0f, 45.0f, 61.0f })
{
    string running = pump.Update(percent) ? "on" : "off";
    states.Add(Invariant($"{percent:F0}% {running}"));
}

Console.WriteLine($"pump      {string.Join(", ", states)}");

// The high-level float switch bounces as the water sloshes at the top. It has to
// read full three times running before the pump controller believes it.
using var floatSwitch = new Debounce(3, false);
(int rawChanges, int settledChanges, bool lastRaw) = (0, 0, false);
foreach (bool raw in new[] { true, false, true, true, true, false, true })
{
    rawChanges += raw != lastRaw ? 1 : 0;
    lastRaw = raw;
    bool before = floatSwitch.State;
    settledChanges += floatSwitch.Update(raw) != before ? 1 : 0;
}

string full = floatSwitch.State ? "full" : "not full";
Console.WriteLine($"float     {rawChanges} raw changes settled into {settledChanges}: the tower reads {full}");

// The low-water alarm is sent once when the level drops under 20% and not again
// until it has come back above 25%, however long it hovers near the line.
using var lowWater = Trigger.Below(20.0f, 5.0f);
foreach (float percent in new[] { 24.0f, 19.0f, 18.0f, 21.0f, 19.0f, 26.0f })
{
    switch (lowWater.Update(percent))
    {
        case Edge.Set:
            Console.WriteLine(Invariant($"alarm     low water at {percent:F0}%: alarm sent"));
            break;
        case Edge.Cleared:
            Console.WriteLine(Invariant($"alarm     back to {percent:F0}%: all clear sent"));
            break;
    }
}

// A booster pump holds the mains at 3.0 bar. The PID asks for a speed, the ramp lets
// the pump change by at most 25% a second so the pipes never take a water hammer,
// and within 0.05 bar of 3.0 the reading counts as on target.
using var pressureHold = Pid.WithLimits(40.0f, 8.0f, 0.0f, 0.0f, 100.0f);
using var softStart = new Ramp(0.0f, 25.0f);
foreach (float bar in new[] { 1.0f, 1.8f, 2.5f, 2.9f, 3.02f })
{
    float steady = Kit.Deadband(bar, 3.0f, 0.05f);
    float asked = pressureHold.Update(3.0f, steady, 1.0f);
    float given = softStart.Update(asked);
    Console.WriteLine(Invariant(
        $"booster   at {bar:F2} bar the PID asks for {asked:F0}%, the pump is given {given:F0}%"));
}

// The booster's controller hangs in the pump house above the mains. The pump house
// thermometer reads Fahrenheit, and a pipe colder than the air's dew point sweats.
float air = Units.FahrenheitToCelsius(84.0f);
double dew = Kit.DewPoint(air, 78.0);
string sweats = 18.0 < dew ? "sweat" : "stay dry";
Console.WriteLine(Invariant(
    $"pumphouse 84 F is {air:F1} C, and at 78% humidity it dews at {dew:F1} C, so the 18 C mains {sweats}"));

// A power cut stops the borehole pump. From the hourly level, the countdown says how
// long until the tower reaches its 20% reserve.
using var reserve = new Depletion(20.0f);
uint? hoursLeft = null;
foreach (float percent in new[] { 80.0f, 76.0f, 72.0f })
{
    hoursLeft = reserve.Update(percent);
}

Console.WriteLine($"outage    at the rate it is falling, the tower reaches 20% in {hoursLeft} hours");

// With the outlet shut overnight the level should hold. A steady fall is a leak.
using var overnight = new Trend(6);
foreach (float percent in new[] { 78.0f, 77.6f, 77.1f, 76.7f, 76.2f, 75.8f })
{
    overnight.Push(percent);
}

float slope = overnight.Slope!.Value;
Console.WriteLine(Invariant($"leak      with the outlet shut the level falls {-slope:F2}% an hour"));

// A burst main shows as pressure falling faster than any demand could pull it.
using var burst = Surge.Falling(0.5f);
foreach (float bar in new[] { 3.0f, 2.9f, 1.7f })
{
    if (burst.Update(bar) is float fall)
    {
        Console.WriteLine(Invariant($"burst     the pressure fell {fall:F1} bar in one reading"));
    }
}

// The flow meter's readings set their own baseline. A hydrant opened stands out,
// and so does a reading the meter could not make.
using var flow = new Anomaly(3.0f, 8);
using var normal = new Window(8);
int flagged = 0;
foreach (float cubicMeters in new[] { 12.1f, 11.8f, 12.4f, 12.0f, 11.9f, 12.2f, 12.0f, 12.3f })
{
    flagged += flow.Check(cubicMeters) ? 1 : 0;
    normal.Push(cubicMeters);
}

Console.WriteLine(Invariant(
    $"meter     {normal.Count} readings from {normal.Min():F1} to {normal.Max():F1} m3/h, {flagged} flagged"));
static string Verdict(bool standsOut) => standsOut ? "stands out" : "passes";
bool hydrant = flow.Check(30.5f);
bool failed = flow.Check(float.NaN);
Console.WriteLine($"meter     a reading of 30.5 m3/h {Verdict(hydrant)}; a failed reading {Verdict(failed)}");

// The tanker truck delivers inside a 20 km district around its depot.
var depot = new Coordinate(-1.5177, 37.2634);
var village = new Coordinate(-1.4480, 37.3390);
double km = Kit.DistanceBetween(depot, village) / 1000.0;
double bearing = Kit.BearingBetween(depot, village);
Console.WriteLine(Invariant($"truck     the village is {km:F1} km from the depot, bearing {bearing:F0} degrees"));
using var district = new Geofence(depot, 20_000.0);
var roadOut = new Coordinate(-1.3000, 37.4500);
var further = new Coordinate(-1.2500, 37.5000);
string[] crossings = new[] { depot, village, roadOut, further, village }
    .Select(fix => district.Update(fix).ToString().ToLowerInvariant())
    .ToArray();
Console.WriteLine($"truck     {string.Join(", ", crossings)}");

The same capability in every language

Language Package Reference
Rust pamoja-kit reference, docs.rs, install
TypeScript @pamoja/kit reference, install
Python pamoja-kit reference, install
C# Pamoja.Kit reference, install

Documentation

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on Pamoja.Kit:

Package Downloads
Pamoja

The whole pamoja framework in one package: every capability of one memory-safe Rust core, behind an idiomatic C# facade, for IoT, robotics, and drones.

Pamoja.Sim

Noisy and replay sensors, a recording actuator, a simulated robot that dead-reckons its pose, and a link that loses sends on a pattern.

Pamoja.Profile

Named, ready-to-run device profiles from plain data or a JSON manifest, run as a node in every language.

Pamoja.Profiles

Profiles and robotics: A node written down as a JSON file, rules between nodes as another, and the naming and encoding rules a robot's topics follow, with no ROS 2 or Zenoh installed.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 180 9/24/2026
0.1.18 142 9/11/2026
0.1.17 132 9/6/2026
0.1.16 127 9/5/2026
0.1.15 124 9/5/2026