Metadata-Version: 2.4
Name: stimgrid
Version: 0.5.0
Summary: Fit stimulation pulses to what a stimulator can actually produce, and find the nearest exactly charge-balanced pulse.
Author-email: Sanjeev Muthusamy <sanjeevmuthusamy@bastionoid.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/sanjeevmuthusamy/stimgrid
Project-URL: Website, https://bastionoid.com
Project-URL: Issues, https://github.com/sanjeevmuthusamy/stimgrid/issues
Project-URL: Discussions, https://github.com/sanjeevmuthusamy/stimgrid/discussions
Keywords: neuroscience,electrophysiology,neurostimulation,electrical stimulation,microstimulation,charge balance,charge-balanced,charge density,Shannon criterion,biphasic pulse,asymmetric pulse,stimulation safety,multi-electrode array,MEA,microelectrode,neural interface,brain-computer interface,DAC quantization,Intan,RHS2116,Multi Channel Systems,STG4000,STG5,Tucker-Davis Technologies,IZ2,Cortical Labs
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# stimgrid

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23694989.svg)](https://doi.org/10.5281/zenodo.23694989) [![PyPI](https://img.shields.io/pypi/v/stimgrid.svg)](https://pypi.org/project/stimgrid/) [![tests](https://github.com/sanjeevmuthusamy/stimgrid/actions/workflows/tests.yml/badge.svg)](https://github.com/sanjeevmuthusamy/stimgrid/actions/workflows/tests.yml)

Your stimulator cannot produce every pulse you write down. It sets current in fixed steps and timing on
a fixed clock, so every value gets rounded. For an asymmetric pulse (for example a short strong first
phase and a long weak recharge phase) that rounding quietly breaks the charge balance you designed. A
rate of 130 Hz may also become 129.87 Hz.

stimgrid shows what your device will actually deliver, and finds the nearest pulse on the device's grid
whose two phases carry exactly equal charge. Pure Python, no dependencies.

For electrical neurostimulation and microstimulation on multi-electrode arrays (MEAs), microelectrodes
and neural interfaces, with presets for the Intan RHS2116, Multi Channel Systems STG4000 and STG5,
Tucker-Davis Technologies IZ2 and Cortical Labs CL, and support for any other current-controlled
stimulator from its current and timing steps.

**Research use only.** stimgrid works from the resolution you give it (or a preset's published
figures). Check the result on your own hardware and settings.

## Install

Requires Python 3.10 or newer, and nothing else.

```bash
pip install git+https://github.com/sanjeevmuthusamy/stimgrid
```

Or from a downloaded copy, inside the `stimgrid` folder:

```bash
pip install .
```

This gives you the `stimgrid` command. You can also run it without installing, from the folder, as
`PYTHONPATH=src python -m stimgrid ...`.

## Quick start

1. **Find your device.** `stimgrid devices` lists the built-in presets and where their figures come
   from. If yours is not there, give its own steps instead (see [Devices](#devices)).
2. **Check your pulse.** Give the first phase, the second phase and the rate:

   ```bash
   stimgrid quick --device mcs-stg4000 --amplitude-ua 7.3 --width-us 117 \
     --second-amplitude-ua 1.9 --second-width-us 450 --frequency-hz 130
   ```

3. **Read the result** (see [Reading the output](#reading-the-output)). Use the `balanced` pulse, or one
   of the options, in your stimulation software, or [export it](#export-to-your-stimulation-software)
   as a file that Intan RHX or MCS MC_Stimulus can load. Already have such a file?
   [`stimgrid check`](#check-an-existing-file) tells you whether it is balanced.
4. **Save it for later or for a lab mate** as a [request file](#request-files), and rerun it with
   `stimgrid fit my_pulse.json`.

Leave out `--second-amplitude-ua` and `--second-width-us` for a symmetric pulse; the second phase then
copies the first.

## Reading the output

For the quick start command above:

```
device: mcs-stg4000 (1.6 mA version): 20 µs time steps, 0.2 µA steps, max 1600 µA
  plan      7.3 µA x 117 µs | gap 0 µs | 1.9 µA x 450 µs
  rounded   7.4 µA x 120 µs | gap 0 µs | 2 µA x 460 µs
            charge 0.888 nC (+3.97% vs plan), net -0.032 nC, imbalance 3.4783%
  balanced  7.2 µA x 120 µs | gap 0 µs | 1.8 µA x 480 µs
            charge 0.864 nC (+1.16% vs plan), net 0 nC, imbalance 0.0000%
  option 1  7 µA x 120 µs | gap 0 µs | 2 µA x 420 µs
            charge 0.84 nC (-1.65% vs plan), net 0 nC, imbalance 0.0000%
  ...
  rate      frequency_hz 130 Hz -> 129.87 Hz (385 steps per period, -0.100%)

WARNING SG201 simple rounding leaves the pulse unbalanced (imbalance 3.4783%, net -0.032 nC per pulse)
NOTE    SG204 exact balance at the rounded widths (120 µs and 460 µs) would need 1.93 µA on the second phase, between the 0.2 µA steps 1.8 and 2 µA

best pulse: {"amplitude_ua": 7.2, "width_us": 120, "second_amplitude_ua": 1.8, "second_width_us": 480}
result: OK
```

| Line | Meaning |
|---|---|
| `device` | The grid being used: time step, current step and maximum current |
| `plan` | The pulse you asked for: first phase, gap, second phase |
| `rounded` | What the device delivers if you type your plan in as it is |
| `balanced` | The closest pulse whose two phases carry exactly equal charge. **Use this one** |
| `option 1-3` | Other exactly balanced pulses, next closest first |
| `charge` | Charge in the first phase, and how far it moved from your plan |
| `net` | Charge left on the electrode by every pulse; 0 means balanced |
| `imbalance` | The difference between the two phases' charges, as a share of the larger one |
| `rate` | The rate you asked for, and the rate the device's clock really gives |
| `WARNING` / `ERROR` | Findings, explained in [Findings](#findings) |
| `best pulse` | The pulse to use, in a form you can paste into a file or program |
| `result` | `OK`, or `ERROR` if the pulse breaks a device limit or cannot be balanced |

Here the planned pulse would leave 0.032 nC on the electrode with every pulse; the balanced one leaves
none and keeps charge per phase within 1.2% of the plan.

## Another example: Intan RHS2116

An asymmetric pulse on an Intan RHS2116 with 0.1 µA steps, driven at 30,000 samples/s:

```bash
stimgrid quick --device intan-rhs2116 --step-ua 0.1 --sample-rate-hz 30000 \
  --amplitude-ua 7.3 --width-us 117 --second-amplitude-ua 1.9 --second-width-us 450 --frequency-hz 40
```

```
device: intan-rhs2116 (step 0.1 µA, 30000 samples/s): 33.3333 µs time steps, 0.1 µA steps, max 25.5 µA
  plan      7.3 µA x 117 µs | gap 0 µs | 1.9 µA x 450 µs
  rounded   7.3 µA x 133.333 µs | gap 0 µs | 1.9 µA x 466.667 µs
            charge 0.97333 nC (+13.96% vs plan), net 0.08667 nC, imbalance 8.9041%
  balanced  7.2 µA x 100 µs | gap 0 µs | 1.8 µA x 400 µs
            charge 0.72 nC (-15.70% vs plan), net 0 nC, imbalance 0.0000%
  option 1  7.2 µA x 133.333 µs | gap 0 µs | 1.8 µA x 533.333 µs
            charge 0.96 nC (+12.40% vs plan), net 0 nC, imbalance 0.0000%
  ...
WARNING SG201 simple rounding leaves the pulse unbalanced (imbalance 8.9041%, net 0.08667 nC per pulse)
WARNING SG203 the balanced pulse changes charge per phase by -15.7%
NOTE    SG204 exact balance at the rounded widths (133.333 µs and 466.667 µs) would need 2.086 µA on the second phase, between the 0.1 µA steps 2 and 2.1 µA
```

Simple rounding would leave 0.087 nC behind on every pulse. stimgrid lists exactly balanced pulses
instead, closest to your plan first, and warns when the grid forces charge per phase to move. The
SG204 note says why: at the rounded widths, balance would need a current between the device's steps.

### Checked against Intan RHX

The `intan-rhs2116` preset was compared with the source code of Intan RHX 3.5.2, Intan's own
acquisition and stimulation software. RHX uses a time step of 1 / sample rate and a current step equal
to the chosen step size, and it sends each phase as the nearest whole number of samples and current
steps, with halves rounded up. That is the same rule stimgrid uses.

The one difference is in RHX's stimulation dialog, which rounds typed values from a step shortened to
three decimals (33.333 µs at 30,000 samples/s). Across 300,000 test values, 42 landed one sample
apart, all at 30,000 samples/s and all within about 0.01 µs of a half-sample. For example, 383.33 µs
gives 11 samples (366.67 µs) in stimgrid and 12 samples (400 µs) in RHX. At 20,000 and 25,000
samples/s the two agreed on every value. If you type widths into RHX, use the `rounded` or `balanced`
widths stimgrid prints; they are whole samples, so RHX keeps them as they are.

This is a comparison with RHX's source code, not a measurement on hardware.

## How it works

1. **Rounded.** Each amplitude, phase width and gap is rounded (halves up) to the device's steps and
   clamped to its ranges. This is what you get if you type the plan in and let the device round it.
2. **Balanced.** stimgrid searches small changes to both phases, mostly the second phase's width and
   amplitude, the usual balancing knobs. On a gridded device, balance is checked exactly, as whole
   current steps times whole time steps, so "balanced" means equal to the last bit. On a device with
   continuous amplitude it also tries the first-phase amplitude that keeps your planned charge exactly.
3. **Ranking.** Exactly balanced candidates are ranked by closeness to your plan, with first-phase charge
   counted double because it sets the effect. The best and three alternatives are shown.
4. **Rates.** Each rate is rounded to a whole number of time steps per period, and the delivered rate is
   reported.

## Command options

`stimgrid quick` takes the pulse as flags. Amplitudes are in µA, times in µs, rates in Hz.

| Flag | Meaning |
|---|---|
| `--device NAME` | A preset from `stimgrid devices` |
| `--step-ua`, `--sample-rate-hz`, `--range-ua` | Preset settings (see [Devices](#devices)) |
| `--time-step-us`, `--amplitude-step-ua` | Your own device's steps, without a preset (or to override one) |
| `--max-amplitude-ua`, `--max-charge-per-phase-nc` | Your own device's limits |
| `--amplitude-ua`, `--width-us` | First phase (required) |
| `--second-amplitude-ua`, `--second-width-us` | Second phase (defaults to the same as the first) |
| `--gap-us` | Gap between the phases |
| `--monophasic` | One phase only |
| `--frequency-hz`, `--intra-burst-hz`, `--burst-rate-hz` | Rates to check against the time grid |
| `--keep-first` | Keep the rounded first phase as it is; balance with the second phase only |
| `--max-imbalance 0.001` | Accept up to 0.1% imbalance as balanced (the default is exact) |

Both `quick` and `fit` also take:

| Flag | Meaning |
|---|---|
| `--format text` | Readable report (the default) |
| `--format json` | The full report as JSON, for scripts |
| `--format pulse` | Only the best pulse as JSON |
| `--strict` | Treat warnings as failures (exit code 1), for automated checks |

`stimgrid --help` and `stimgrid quick --help` list them too.

## Export to your stimulation software

Instead of typing the fitted pulse into your stimulation software, `--export` writes it as a file that
software can load. It works with both `quick` and `fit`:

```bash
stimgrid fit examples/intan_asymmetric.json --export intan-rhx --out stim.xml --channels A-000,A-001 --pulses 10
stimgrid fit examples/mcs_stg4000_asymmetric.json --export mcs-ascii --out pulse.txt --pulses 100
```

The report still prints as usual, and the file is written next to it. With `--out -` only the file
contents are printed. Nothing is exported if the fit has errors.

| Flag | Meaning |
|---|---|
| `--export intan-rhx` | Intan RHX stimulation settings (`.xml`), for the RHS Stim/Record Controller |
| `--export mcs-ascii` | MC_Stimulus II (STG4000) or MC_Stimulus III (STG5) ASCII import, current mode |
| `--out FILE` | Where to write it (required; `-` prints it) |
| `--use best` | Which pulse: `best` (the balanced one; the default), `rounded`, `balanced` or `option-1` to `option-3` |
| `--channels` | RHX channel names such as `A-000,B-015` (default `A-000`), or MCS channel numbers such as `1,2` (default `1`) |
| `--pulses N` | Pulses in the train (default 1). Above 1 it needs `--frequency-hz` (or `--intra-burst-hz`) |
| `--first-phase negative` | Sign of the first phase: `negative` (the default) or `positive` |

**Intan RHX.** Load the file with *File > Load Stimulation Settings* (not *Load Settings*, which expects
a full RHX settings file and warns that no signal groups were found). It sets, for each listed channel:
stimulation on, the shape (biphasic, or biphasic with interphase delay when there is a gap), the
polarity, both phase durations and amplitudes, and for a train the number of pulses and the train
period. Trigger, amp settle and charge recovery are left out, so the values already set in RHX stay.
Other channels are not touched.

- The file names the sample rate and step size it was fitted for. RHX ignores every stimulation setting
  in the file, with only a warning, if these differ from the running system, so use the same
  `--sample-rate-hz` (20,000, 25,000 or 30,000) and `--step-ua` as in RHX.
- Durations are written in µs to six decimals. RHX converts them to the nearest whole sample, which is
  the sample count stimgrid fitted.
- stimgrid will not write what RHX would reject: phases or gaps over 5,000 µs, more than 256 pulses, or a
  train period shorter than the pulse.

**MCS MC_Stimulus.** Import the file in MC_Stimulus II or III. Each pulse is written as rows of current
(µA) and duration (µs): the first phase, the gap if there is one, the second phase, then zero current
for the rest of the period when a rate is given. Every pulse in the train is written out, because the
import format has no repeat count.

- Importing overwrites the open worksheet, and channels that are not in the file come out empty. Save
  your current setup first.
- The import takes times in multiples of 20 µs on both the STG4000 and the STG5, although the STG5
  itself has a 5 µs time step. If an STG5 pulse is not on 20 µs steps, stimgrid says so; fit it again
  with `--time-step-us 20`.
- The manuals do not say which sign is cathodic on your electrode. `--first-phase` sets the sign of
  the first phase; check the waveform preview in MC_Stimulus.

Check the loaded values in the vendor software before stimulating. The file formats were written from
the vendors' published descriptions. An `intan-rhx` file for the Intan example (10 pulses at 40 Hz) was
loaded in the RHX 3.5.2 RHS demo at 30,000 samples/s and 0.1 µA steps, and RHX showed the exported
values exactly (7.2 µA × 100 µs, 1.8 µA × 400 µs, cathodic first, 10 pulses, 25 ms period) and
reported the charge as balanced. No `mcs-ascii` file has been loaded in MC_Stimulus yet.

## Check an existing file

`stimgrid check` reads a stimulation file you already have and reports, channel by channel, what the
stimulator will deliver and whether each pulse is charge-balanced. It reads the same two formats that
export writes: Intan RHX stimulation settings (`.xml`, saved with *File > Save Stimulation Settings*) and
MC_Stimulus II / III ASCII files (format 4, current mode).

```bash
stimgrid check stim.xml
```

```
file: stim.xml: Intan RHX, 30000 samples/s, 0.1 µA steps
A-000:
  in file   7.3 µA x 117 µs | gap 0 µs | 1.9 µA x 450 µs  (100 pulses every 25000 µs)
  delivered 7.3 µA x 133.333 µs | gap 0 µs | 1.9 µA x 466.667 µs
            charge 0.97333 nC (+13.96% vs file), net 0.08667 nC, imbalance 8.9041%
  balanced  7.2 µA x 100 µs | gap 0 µs | 1.8 µA x 400 µs
            charge 0.72 nC (-15.70% vs file), net 0 nC, imbalance 0.0000%
  WARNING SG106 width_us 117 becomes 133.333 after rounding (+14.0%)
  WARNING SG203 the balanced pulse changes charge per phase by -15.7%
  NOTE    SG204 exact balance at the rounded widths (133.333 µs and 466.667 µs) would need 2.086 µA on the second phase, between the 0.1 µA steps 2 and 2.1 µA
  net charge over the whole channel: 8.667 nC (NOT balanced)
A-001: off: stimulation is off

summary: 2 channel(s): 1 checked (1 not balanced), 1 off, 0 skipped
result: NOT OK
```

- **in file** is what the file says; **delivered** is that pulse rounded to the device's steps, the way
  RHX and MC_Stimulus do it (nearest step, halves up). Here RHX turns 117 µs into 4 samples at
  30,000 samples/s, and the pulse leaves 0.087 nC behind every time, 8.7 nC over the train.
- **balanced** is the nearest exactly balanced pulse, shown only when the delivered pulse is not balanced.
- Channels that are off, use a shape stimgrid does not check (such as triphasic), or leave values out of
  the file (RHX then keeps what it already has) are listed but not checked.
- It also flags what would stop the file working: in RHX, a phase over 5,000 µs, more than 256 pulses,
  or a train period shorter than the pulse (RHX then skips the channel with only a warning); in
  MC_Stimulus, times that are not multiples of 20 µs.

The exit code is 0 when every checked channel is balanced and nothing would stop the file working,
and 1 otherwise, so `stimgrid check` can run in a script before each experiment.

| Flag | Meaning |
|---|---|
| `--format json` | The full result as JSON |
| `--max-imbalance 0.001` | Accept up to 0.1% imbalance as balanced |
| `--strict` | Also fail on warnings |
| `--sample-rate-hz`, `--step-ua` | Intan RHX: only needed if the file leaves them out |
| `--device mcs-stg4000` or `mcs-stg5`, `--range-ua` | MC_Stimulus: your stimulator and range (default: STG4000 1.6 mA for an MC_Stimulus II file, STG5 1.6 mA for III) |

In an MC_Stimulus file, stimgrid splits each channel into pulses: a phase, then an opposite-sign phase
straight after it or after one zero-current row (the gap). Identical pulses are counted together. A
phase with no opposite phase after it is reported as monophasic, and so not balanced.

## Whole protocols

A request file can describe a whole protocol instead of one pulse: a list of steps, each a single
pulse, a train, a set of bursts or a rest. `stimgrid fit` fits every step's pulse and adds what only
shows across steps: how long each step really takes once its rates fall on whole time steps, how many
pulses each channel gets, and the charge per phase and net charge over the whole protocol.

```json
{
  "stimgrid": 1,
  "device": {"preset": "mcs-stg4000"},
  "protocol": [
    {"name": "train", "pulse": {"amplitude_ua": 7.3, "width_us": 117, "second_amplitude_ua": 1.9, "second_width_us": 450},
     "pulses": 1300, "frequency_hz": 130, "channels": [1, 2]},
    {"name": "pause", "rest_s": 10},
    {"name": "theta", "pulse": {"amplitude_ua": 20, "width_us": 100},
     "bursts": 10, "pulses": 4, "intra_burst_hz": 100, "burst_rate_hz": 5, "channels": [1]}
  ],
  "repeat": 3
}
```

```bash
stimgrid fit examples/mcs_stg4000_protocol.json
```

```
device: mcs-stg4000 (1.6 mA version): 20 µs time steps, 0.2 µA steps
protocol: 3 step(s), run 3 times, using the best pulse of each step

step 1 train: 1,300 pulses at 130 Hz on channels 1, 2
  plan      7.3 µA x 117 µs | gap 0 µs | 1.9 µA x 450 µs
  using     7.2 µA x 120 µs | gap 0 µs | 1.8 µA x 480 µs
            charge 0.864 nC (+1.16% vs plan), net 0 nC, imbalance 0.0000%
  rate      frequency_hz 130 Hz -> 129.87 Hz (-0.100%)
  time      10 s planned, 10.01 s delivered (+0.100%)
  charge    1.123 µC per phase over 1,300 pulses, net 0 nC (balanced)
...

time: 1 min 6 s planned, 1 min 6.03 s delivered (+0.045%)
channel 1: 4,020 pulses, 3.61 µC per phase, net 0 nC (balanced)
channel 2: 3,900 pulses, 3.37 µC per phase, net 0 nC (balanced)
result: OK
```

Here 130 Hz on the STG4000's 20 µs grid becomes 129.87 Hz, so the 10 s train takes 10.01 s, and the
whole protocol runs 30 ms longer than planned.

| Step field | Meaning |
|---|---|
| `pulse` | The pulse, as in a single-pulse request |
| `pulses`, `frequency_hz` | A train: how many pulses, at what rate (the rate is needed for more than one) |
| `bursts`, `pulses`, `intra_burst_hz`, `burst_rate_hz` | Bursts: how many, pulses in each, the rate inside a burst and bursts per second |
| `rest_s` | A rest, in seconds (a step of its own, without a pulse) |
| `channels` | Channel names or numbers the step uses, for the per-channel totals (optional) |
| `ramp` | Ramp a train's amplitude up at the start and down at the end (optional; see [Amplitude ramps](#amplitude-ramps)) |
| `name` | A label for the report (optional) |

At the top level, `repeat` runs the whole list more than once, and `use` (or `--use`) picks the pulse each
step uses: `best` (the balanced pulse, the default) or `rounded` (the planned pulse simply rounded,
to see what it would leave behind). `max_imbalance`, `keep_first`, `search_steps` and `electrode` work as
in a single-pulse request.

- A train's time is pulses × period, its last period included; a burst step's is bursts × burst period.
  Rests are taken as given.
- A burst that does not fit in one burst period is an error (SG601).
- A train with a [ramp](#amplitude-ramps) is balanced only if every ramp level is.
- The exit code is 0 when no step has an error and every pulse used is balanced, so
  `stimgrid fit protocol.json --use rounded` fails if simple rounding would leave charge behind.
- `--format json` gives every step and total. `--format pulse` and `--export` work on one pulse only, so
  they are not available for a protocol.

### Amplitude ramps

A train can start gently and stop gently: `ramp` scales both phases' amplitudes from a fraction of the
full pulse up to it over the first pulses, and back down over the last ones.

```json
{"name": "ramped train", "pulse": {"amplitude_ua": 20, "width_us": 100, "second_amplitude_ua": 5, "second_width_us": 400},
 "pulses": 200, "frequency_hz": 40, "channels": ["A-000"],
 "ramp": {"up_pulses": 5, "down_pulses": 5, "start_fraction": 0.15}}
```

The full pulse balances exactly on an Intan RHS2116 with 0.1 µA steps, but the smaller ramp pulses
have to be rounded too, and three of them no longer balance. stimgrid fits each ramp level on its own:

```bash
stimgrid fit examples/intan_ramp_protocol.json --use rounded
```

```
  ramp      from 15%, up over 5 pulses and down over 5 pulses: 5 level(s), 3 NOT balanced
        15%  3 µA x 100 µs | 0.8 µA x 400 µs, net -0.02 nC (NOT balanced)
        32%  6.4 µA x 100 µs | 1.6 µA x 400 µs, net 0 nC
        49%  9.8 µA x 100 µs | 2.5 µA x 400 µs, net -0.02 nC (NOT balanced)
        66%  13.2 µA x 100 µs | 3.3 µA x 400 µs, net 0 nC
        83%  16.6 µA x 100 µs | 4.2 µA x 400 µs, net -0.02 nC (NOT balanced)
```

With the default `best`, every level uses its own balanced pulse instead (15% becomes 2.8 µA × 100 µs
then 0.7 µA × 400 µs), and the step's net charge is 0.

| Ramp field | Meaning |
|---|---|
| `up_pulses` | Pulses that ramp up at the start of the train |
| `down_pulses` | Pulses that ramp down at the end (optional) |
| `start_fraction` | Where the ramp starts and ends, as a fraction of the full amplitudes (above 0 and below 1; default 0.1) |

- Ramp pulse *i* of `up_pulses` *n* runs at `start + (1 − start) × i / n`, so the first is at
  `start_fraction` and the train reaches the full pulse right after the ramp. The ramp down mirrors it.
- Both phases scale together; widths stay as planned before fitting.
- Ramps are for trains. Findings from the ramp levels are gathered into one line per kind, naming the
  levels they apply to. A level whose pulse becomes longer than one period is an error (SG602).
- In Python: `Step(pulse, pulses=200, frequency_hz=40, ramp=Ramp(up_pulses=5, down_pulses=5, start_fraction=0.15))`.

## Charge density and Shannon k

Give the electrode's size and `fit`, `quick` and `check` also report the charge density of the larger
phase and its Shannon *k* value, `k = log10(Q) + log10(Q / A)`, with *Q* the charge per phase in µC and
*A* the electrode's geometric area in cm² ([Shannon, 1992](https://doi.org/10.1109/10.126616)).

```bash
stimgrid quick --device mcs-stg4000 --amplitude-ua 100 --width-us 200 --electrode-diameter-um 30
```

```
  electrode 706.858 µm²: 20 nC per phase, 2829 µC/cm², Shannon k 1.753 (limit 1.75 allows 19.94 nC)

WARNING SG501 Shannon k 1.753 is above 1.75 (20 nC per phase, 2829 µC/cm² on 706.858 µm²); at most 19.94 nC per phase stays within it
```

| Flag | Meaning |
|---|---|
| `--electrode-diameter-um` | Diameter of a disc electrode |
| `--electrode-area-um2` | Or its geometric area |
| `--k-limit` | Warn above this *k* (default 1.75) |

- `fit` and `quick` report the best pulse; `check` reports the pulse the file delivers.
- Going over the limit is a warning (SG501), so the exit code stays 0 unless you add `--strict`.
- A request file can carry it too: `"electrode": {"diameter_um": 30, "k_limit": 1.75}`. Flags on
  `stimgrid fit` replace the file's values.
- Shannon's line was fitted to damage data from macroelectrodes. For small microelectrodes it is not a
  reliable guide, and the limits that apply depend on the electrode material and the tissue. Treat *k*
  as one input to your own judgement, not as a safety limit.

## Devices

| Preset | Time step | Amplitude | Source |
|---|---|---|---|
| `intan-rhs2116` | 1 / sample rate (`--sample-rate-hz`, default 30,000) | `--step-ua` of 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5 or 10 µA, up to 255 steps | Intan RHS2116 datasheet |
| `cortical-labs-cl` | 20 µs | continuous (no step published), up to 3 µA, 3 nC per phase | Cortical Labs CL API docs (StimDesign) |
| `mcs-stg4000` | 20 µs, up to 25 kHz | `--range-ua 1600`: 0.2 µA steps; `--range-ua 16000`: 2 µA steps | MCS STG 4002/4004/4008 datasheets |
| `mcs-stg5` | 5 µs, up to 100 kHz | `--range-ua 160`: 0.1 µA; `1600`: 1 µA; `16000`: 10 µA steps | MCS STG5 brochure |
| `tdt-iz2` | 1 / RZ sample rate (`--sample-rate-hz`, default 24,414.0625) | continuous (no step published); `--range-ua 300` (IZ2, IZ2M) or `3000` (IZ2H, IZ2MH) | TDT IZ2 manual, IZ2M/IZ2MH spec sheet |

All presets are current mode. The MCS range is the model version you bought (STG4000) or the range you
select (STG5). The STG4000 160 µA version is left out because its official datasheet could not be
confirmed.

**Not included yet**, because the vendor does not publish a current step or a timing step:

| Device | Published | Missing |
|---|---|---|
| Axion Maestro Pro / Edge | ±250 µA, ±1.2 V, phase at least 100 µs | current step, time step |
| MaxWell MaxOne / MaxTwo | 32 stimulation units, ±1.0 V | current range and step, time step |
| Blackrock CereStim | phase 44–65,535 µs, 4 Hz–5 kHz, 100 µA steps (macro model) | micro model step, time step |
| A-M Systems 4100 | 100 mA with 1 µA resolution, pulses from 1 µs | time step |

If you have one of these, describe it with your own numbers (below). To have it added as a preset,
[request a device](https://github.com/sanjeevmuthusamy/stimgrid/issues/new?template=device-request.yml)
with the values from its manual or datasheet; the same form works for any stimulator not listed.

`stimgrid devices` prints each preset's source. The RHS2116 has no pulse timers; the host sets every
edge, so its timing grid depends on your host software (Intan RHX matches it; see
[Checked against Intan RHX](#checked-against-intan-rhx)). Any other stimulator can be described by its
own numbers:

```bash
stimgrid quick --time-step-us 10 --amplitude-step-ua 0.5 --max-amplitude-ua 100 \
  --amplitude-ua 20 --width-us 105 --second-amplitude-ua 4 --second-width-us 520
```

Fields: `time_step_us` (required), `amplitude_step_ua` (leave out for continuous), `max_amplitude_ua`,
`min_phase_us`, `max_phase_us`, `max_charge_per_phase_nc`, `max_frequency_hz`.

## Request files

```json
{
  "stimgrid": 1,
  "device": {"preset": "intan-rhs2116", "step_ua": 0.1, "sample_rate_hz": 30000},
  "pulse": {"amplitude_ua": 7.3, "width_us": 117, "second_amplitude_ua": 1.9, "second_width_us": 450},
  "rates": {"frequency_hz": 40},
  "max_imbalance": 0,
  "keep_first": false,
  "electrode": {"diameter_um": 15}
}
```

`electrode` is optional (see [Charge density and Shannon k](#charge-density-and-shannon-k)).

```bash
stimgrid fit examples/intan_asymmetric.json
stimgrid fit examples/intan_asymmetric.json --format json
cat request.json | stimgrid fit - --format pulse
```

A preset takes its options (`step_ua`, `sample_rate_hz` or `range_ua`, as in the table) and any device
field to override, for example `{"preset": "intan-rhs2116", "step_ua": 0.1, "max_amplitude_ua": 10}` or
`{"preset": "mcs-stg5", "range_ua": 160}`. Invalid input names the exact
place, for example `pulse: width_us must be a positive number`.

A request file with `protocol` instead of `pulse` describes a whole protocol; see
[Whole protocols](#whole-protocols).

`--format pulse` prints only the best pulse, in the common shape (`amplitude_ua`, `width_us`,
`second_amplitude_ua`, `second_width_us`, `gap_us`), ready to paste into a protocol file or another tool.

## Findings

| Code | Level | Meaning |
|---|---|---|
| SG101 | error | An amplitude is above the device's maximum |
| SG102 | error | A phase width is outside the device's range |
| SG103 | error | An amplitude is below one current step |
| SG104 | error | Charge per phase is above the device's maximum |
| SG105 | warning | The interphase gap rounds to zero |
| SG106 | warning | Rounding moves a value by more than 10% |
| SG201 | warning | Simple rounding leaves the pulse unbalanced |
| SG202 | error | No balanced pulse was found within the search and the device's limits |
| SG203 | warning | The balanced pulse changes charge per phase by more than 5% |
| SG204 | note | Exact balance at the rounded widths would need a second-phase amplitude the device can't give (between steps, below one step or above the maximum) |
| SG301 | warning | A rate changes by more than 0.1% on whole periods |
| SG303 | error | The pulse is longer than one period at that rate |
| SG304 | error | A rate is above the device's maximum |
| SG401 | error | `check`: a value in an RHX file is outside what RHX accepts |
| SG402 | error | `check`: a phase has zero duration or amplitude |
| SG403 | error | `check`: an RHX train period is shorter than the pulse, so RHX skips the channel |
| SG404 | error | `check`: an MC_Stimulus time is not a multiple of 20 µs, or not above zero |
| SG501 | warning | The Shannon *k* value is above the limit (only with an electrode size) |
| SG601 | error | Protocol: a burst is longer than one burst period |
| SG602 | error | Protocol: a ramp level's pulse is longer than one period |

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Fitted with no errors (and no warnings, with `--strict`); for `check`, every checked channel balanced |
| 1 | Errors (or warnings, with `--strict`); for `check`, also any channel not balanced |
| 2 | Bad input or usage |
| 3 | Internal error |

## From Python

```python
from stimgrid import Pulse, Rates, fit, intan_rhs2116

report = fit(Pulse(7.3, 117, second_amplitude_ua=1.9, second_width_us=450),
             intan_rhs2116(step_ua=0.1, sample_rate_hz=30000), Rates(frequency_hz=40))
print(report.balanced.as_pulse(), report.balanced.imbalance)
```

To export from Python:

```python
from stimgrid import export

device = intan_rhs2116(step_ua=0.1, sample_rate_hz=30000)
report = fit(Pulse(7.3, 117, second_amplitude_ua=1.9, second_width_us=450), device, Rates(frequency_hz=40))
with open("stim.xml", "w") as fh:
    fh.write(export(report, device, "intan-rhx", channels=["A-000"], pulses=10))
```

To check a file from Python:

```python
from stimgrid import check_file

result = check_file("stim.xml")
for channel in result.unbalanced:
    print(channel.channel, channel.net_charge_nc)
```

With an electrode:

```python
from stimgrid import Electrode, mcs_stg4000

report = fit(Pulse(100, 200), mcs_stg4000(), electrode=Electrode(diameter_um=30))
print(report.density.density_uc_cm2, report.density.shannon_k, report.density.max_charge_nc)
result = check_file("stim.xml", electrode=Electrode(area_um2=2000))
```

A protocol:

```python
from stimgrid import load_protocol, fit_protocol

protocol, device, options = load_protocol("examples/mcs_stg4000_protocol.json")
report = fit_protocol(protocol, device, **options)
print(report.delivered_us / 1e6, [(c.channel, c.pulses, c.net_charge_nc) for c in report.channels()])
```

## Limits of this tool

- It models one pulse shape: rectangular phases with an optional gap. It does not model DAC settling,
  electrode impedance, channel-to-channel mismatch or the RHS2116's trim settings.
- Rates are assumed to fall on the same time grid as phase widths.
- Continuous amplitude means the device's real amplitude resolution is unknown; the reported amplitude
  may need rounding again on the hardware. If you know your device's real resolution, use a custom
  device with `amplitude_step_ua` (or `--amplitude-step-ua`) set to it, and stimgrid fits to that.

## Contact

- **Questions and ideas:** [GitHub Discussions](https://github.com/sanjeevmuthusamy/stimgrid/discussions)
- **Bugs:** [GitHub Issues](https://github.com/sanjeevmuthusamy/stimgrid/issues)
- **A stimulator to add:** [request a device](https://github.com/sanjeevmuthusamy/stimgrid/issues/new?template=device-request.yml)
- **Anything else, or anything you'd rather not post publicly:** email sanjeevmuthusamy@bastionoid.com
- **Website:** [bastionoid.com](https://bastionoid.com), where stimgrid is made alongside the Bastionoid platform

## Citing

If you use stimgrid in published work, please cite it. GitHub's "Cite this repository" button gives the
reference from `CITATION.cff`:

> Muthusamy S. stimgrid (version 0.5.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.23694989

This DOI always resolves to the latest version; each release also has its own DOI on Zenodo.

## Tests

```bash
python -m unittest discover -s tests
```

## Licence

MIT. See `LICENSE`.

## Trademarks

Intan, RHX, Multi Channel Systems, MC_Stimulus, Tucker-Davis Technologies, Synapse, Cortical Labs and
other product names are trademarks of their owners. They are used only to say which devices and
software stimgrid works with. stimgrid is an independent project, not affiliated with or endorsed by
any of these companies.
