Metadata-Version: 2.4
Name: intomind
Version: 1.4.2
Summary: The IntoMind instrument API: the protocol, the recording, its provenance, and the analyses that read it back
Author-email: IntoMind <support@intomind.com>
License-Expression: AGPL-3.0-only
Project-URL: Homepage, https://intomind.com
Project-URL: Documentation, https://docs.intomind.com
Project-URL: Source, https://github.com/intomind/api
Keywords: eeg,bci,neuroscience,bluetooth,instrument
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: bleak
Requires-Dist: numpy
Provides-Extra: analysis
Requires-Dist: scipy; extra == "analysis"
Provides-Extra: mat
Requires-Dist: h5py; extra == "mat"
Provides-Extra: serial
Requires-Dist: pyserial; extra == "serial"
Provides-Extra: all
Requires-Dist: scipy; extra == "all"
Requires-Dist: h5py; extra == "all"
Requires-Dist: pyserial; extra == "all"
Dynamic: license-file

# intomind

The IntoMind instrument API. One library, every IntoMind device.

```python
from intomind.client import display_names, scan
from intomind.session import Session

found = await scan()                   # every device that answered, as heard
labels = display_names(found)          # each one's name, numbered when two share one
chosen = next(d for d in found if labels[d.address] == "Ada's IntoMind One")
s = Session()
await s.connect(chosen)                # only the one picked
device = s.devices[0]
```

It owns the link to a device, the protocol spoken over it, the recording
of what it streams, the provenance of that recording, the formats it can
be read out as, and the analyses that read it back. It knows nothing
about any particular device beyond what that device tells it, and nothing
about any application built on top of it.

## What is in here

| | |
|---|---|
| `protocol.py` | the wire, and nothing else |
| `client.py` | one device: discovery, control, streaming, the clock |
| `session.py` | discovery and the set of connected devices |
| `instrument.py` | instrument state and decoding, independent of any protocol |
| `experiments.py` | the recorder, and the cued protocols built on it |
| `provenance.py` | what a capture records about how it was produced |
| `analysis.py` | the analyses a capture supports |
| `export.py` | a capture as npz, EDF, BDF, csv, tsv, or MATLAB |
| `model.py` | the device's encoder, on the host, and head training |
| `adapters.py` | support for hardware this library was not written for |
| `montage.py` | where the electrodes are, as data rather than as a sentence |
| `placement.py` | how a device was placed, and what that implies |
| `timebase.py` | putting events and samples on one ruler |
| `controller.py` | game controllers, as a second input stream on that ruler |
| `audio.py` | spoken cues |
| `headmap.py` | scalp coordinates, for drawing |

## Capabilities are the device's answer, never this library's assumption

A device declares what it is over the protocol, and everything follows
from that declaration. The channel count, the converter's resolution and
reference, the rates it offers, whether it can detect lead-off, whether it
carries a model: all of it is asked and none of it is assumed.

```python
device.info.channels            # what the device said
device.can("model")             # a capability bit, not a version number
device.profile()                # controls composed from the above
```

A table of specific hardware in this library is a defect. Hardware that
needs one brings an adapter, which is a separate package, and whatever it
offers appears as `device.extra` on the devices it claims. See
`adapters.py`.

## The timeline is never silently repaired

Every sample carries the device's own index and the device time of its own
conversion. A forward step in that index is a loss with an exact count. A
step that is not forward is a break whose extent is not a number, and this
library says so rather than returning a count that would be a fiction.

A recording writes its gaps down. Nothing splices.

## The contract is tested, not assumed

`contract/conformance.json` is emitted by the firmware from the codec the
device itself runs. Every message this library decodes, it decodes to the
fields that file states, and every malformed one it refuses for the reason
that file states. An implementation that drifts from the device fails a
test rather than a bench session.

```
python3 tests/run_all.py
```

No device, no network, no pytest. That is the command, and there is no
other.

## The model

The device's encoder turns four seconds of signal into an embedding of 76
numbers. Its weights are not published: they reach a device only as a
signed image. The device sends its embeddings instead, so a head is
trained on a host and uploaded:

```python
from intomind.model import train_head

await device.start()
windows = await device.collect_embeddings(100)    # about seven minutes
blob = train_head([w.embedding for w in windows], labels, name="focus",
                  encoder_id=windows[0].encoder_id)
await device.upload_head(blob, slot=2, select=True)   # slot 1 ships with the open sex head
await device.set_predictions(True)
```

A head needs more windows than the embedding has numbers. A head is
weights, never code. What it produces on the host is what it produces on
the device, because the device evaluates exactly those integers.

## Exports

```python
from intomind import export

export.export(label, "bdf")        # any name in export.FORMATS
export.export_npz(label)           # or by name, same six keys back
```

Six formats, one shape, and every one of them refuses a capture that does
not verify. `npz` and `bdf` return every sample to the count it was
recorded at, measured per file rather than assumed. `edf` is sixteen bits
and says so. `mat` is MATLAB v7.3, which needs `h5py` and says which line
to type when it is missing.

Lead-off is in every format, and only where the recording says detection
was running: a lead-off status of zero is an attached electrode there and
nothing at all anywhere else. `csv` and `tsv` add a `chN_leadoff` column per
channel, 1 where the electrode was off, 0 where it was attached, and empty
where nothing was checking. `bdf` and `edf` label each stretch an electrode
was off `BAD_LEADOFF` and the channel, which MNE treats as bad. `npz` and
`mat` hold the flag itself as `leadoff_active`. Every summary says in
`contact` whether contact was checked, and a recording made before the flag
was kept says "not known".

In `bdf` and `edf`, every sample is written at the time it was recorded.
Where samples were lost, the file holds one placeholder for each of them,
under a `BAD_ACQ_SKIP` annotation that covers the hole. MNE reads that label
as its own: it filters each side of a hole separately, rejects an epoch that
overlaps one, and returns the hole as NaN when asked. A reader that does not
know the label shows a hole as a flat line at the last value before it.
These files are whole records of about a second, and the last record is
completed after the last sample with placeholders under the same label, as
MNE's own writer completes it, so no recorded sample is cut. `tail_filled`
says how many placeholders that took, and `samples_dropped` is 0.

The placeholder repeats the last recorded value, as MNE's own writer does.
NaN is not possible, because these formats store whole numbers. A value that
stands apart from the signal, such as the lowest number the file can store,
is a step thousands of microvolts deep, and a filter or a spectrum that does
not read the label takes it for signal. The last value adds no step and
costs no precision. Zero is possible, but each signal's range would have to
stretch to reach it, and most recordings sit thousands of microvolts from
zero, so every recorded sample in a 16-bit file would be rounded more
coarsely. A build that prefers zero sets `edf.GAP_FILL_UV = 0.0` and takes
on that cost.

A recording whose sample count starts again within its first second begins
with samples from before the recording started. `bdf` and `edf` start at
the reset, and `samples_before_restart` says how many samples were left
out. A count that steps back later is a break whose length is not known,
and they refuse it rather than invent one.

## Where captures live

`$INTOMIND_CAPTURES`, or `~/.local/share/intomind/captures`. An
application that keeps its own collection says so once:

```python
from intomind import provenance
provenance.use_captures_dir("~/my-study/captures")
```

Nothing is inferred from where a file happens to sit.

## License

Free software under the GNU Affero General Public License, version 3. We
also license it commercially, on fair terms shaped by your use case: write
to contact@intomind.com. See `LICENSING.md`, and `CONTRIBUTING.md` before a
first pull request.

IntoMind is a trademark of IntoMind, Inc. The license covers the code, not
the name.
