Metadata-Version: 2.4
Name: csv-session-logger
Version: 0.1.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pandas>=2.0
Requires-Dist: numpy>=1.24
Requires-Dist: matplotlib>=3.7
Requires-Dist: PyQt5>=5.15
Requires-Dist: argcomplete>=3.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"

# csv-session-logger

A small session-based CSV logger for robotics sims and bench tests, one file per run, channels aligned on wall-clock time.

## Install

```bash
pip install csv-session-logger
```

Requires Python 3.10+.

## Quick Start

```python
from csv_session_logger import Logger

log = Logger(session="bench_test", output_dir="logs/", fill="none")
log.register("voltage", unit="V")
log.register("current", unit="A")

log.log("voltage", 12.1)
log.log("current", 0.45)

path = log.save()
print(path)
```

Channels must be registered before logging. I made that explicit so you decide upfront what goes in the CSV instead of accumulating mystery columns mid-run.

## `log()` vs `log_many()`

Both append scalar samples to the buffer. The difference is timing.

- `**log(key, value)**`: one channel, one call. Each call records its own `t_wall` via `time.time()`. If you log `x` and then `y` separately, they land on slightly different timestamps. That is fine when channels are independent or arrive asynchronously (e.g. a slow sensor vs a fast control loop).
- `**log_many({...})**`: several channels in one call. All keys in the dict share a single `t_wall` timestamp, taken once at the start of the call. I use this when values belong to the same timestep and should line up in the CSV without NaN gaps, typical in a simulation loop or a synchronized sensor snapshot.

Simulation time is not special. If you want `t_sim`, register it like any other channel and include it in `log_many()`:

```python
log.register(["t_sim", "x", "x_dot"], ["s", "m", "m/s"])
log.log_many({"t_sim": t, "x": x, "x_dot": x_dot})
```

## Fill strategies

When channels are logged at different `t_wall` times, `save()` merges them onto one time index. Missing entries become NaN before the fill step runs.


| `fill`    | Behavior                                                                                                                                     |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `"none"`  | Leave NaN as-is. Use when you want to see exactly which channel was missing at each timestamp.                                               |
| `"ffill"` | Forward-fill with the last known value per column. Leading NaN (before the first sample) stay NaN.                                           |
| `"mean"`  | Linear interpolation between neighbours (`pandas.interpolate()`). Good for plotting continuous signals when samples are slightly misaligned. |


Pick the strategy at `Logger(...)` construction, it applies on every `save()`.

## Output format

The CSV index is `t_wall` (Unix seconds, float). Column headers use `"key [unit]"` when a unit was registered, otherwise plain `key`. Values are written with 8 decimal places.

Example (from a spring-mass-damper run):

```csv
t_wall,t_sim [s],x [m],x_dot [m/s],x_ref [m],error [m],F [N]
1781263948.99401498,0.00000000,0.00000000,0.00000000,0.00000000,0.00000000,0.00000000
1781263948.99402165,0.01000000,0.00000000,0.00000000,0.00000000,0.00000000,0.00000000
```

Auto-generated filenames look like `{session}_{YYYY-MM-DD_HH-MM-SS}.csv`. Pass `save(filename="my_run.csv")` to override.

## CLI

Entry point: `csl`

### `csl info <csv>`

Print channel names and duration (last `t_wall` minus first).

```bash
csl info logs/spring_mass_damper_2026-06-12_00-23-22.csv
```

### `csl plot <csv>`

Quick matplotlib plots. Time axis is `t_wall` relative to the first row (starts at 0).

```bash
# All channels, stacked subplots
csl plot logs/spring_mass_damper_2026-06-12_00-23-22.csv

# Subset by column name (-t / --time)
csl plot logs/spring_mass_damper_2026-06-12_00-23-22.csv -t "x [m]" "F [N]"

# XY plot: first column is X, rest are Y (-x / --xy)
csl plot logs/spring_mass_damper_2026-06-12_00-23-22.csv -x "t_sim [s]" "x [m]" "error [m]"
```

`-t` and `-x` cannot be used together.

## Full example: spring-mass-damper

Second-order plant with a P controller, Euler integration. Same as `examples/sim_example.py`:

```python
import numpy as np
from csv_session_logger import Logger

m, c, k, Kp = 1.0, 0.5, 2.0, 10.0
dt, t_end = 0.01, 20.0
n_steps = int(t_end / dt)

def x_ref(t: float) -> float:
    return 0.0 if t < 2.0 else 1.0

x, x_dot = 0.0, 0.0

log = Logger(session="spring_mass_damper", output_dir="logs/", fill="none")
log.register(["t_sim", "x", "x_dot", "x_ref", "error", "F"],
             ["s", "m", "m/s", "m", "m", "N"])

t = 0.0
for _ in range(n_steps):
    ref = x_ref(t)
    error = ref - x
    F = Kp * error

    x_ddot = (F - c * x_dot - k * x) / m
    x_dot += x_ddot * dt
    x += x_dot * dt

    log.log_many({
        "t_sim": t, "x": x, "x_dot": x_dot,
        "x_ref": ref, "error": error, "F": F,
    })
    t += dt

path = log.save()
log.summary()
```

Run it: `python examples/sim_example.py`, then `csl plot logs/spring_mass_damper_*.csv`.

## Why not rosbag / wandb / mlflow?

I haven't used these in practice, but for this use case I wanted something
minimal: register a few channels, log scalars in a loop, get one CSV. No
server, no extra setup, just a file I can open with pandas or `csl plot`.
