Skill Tracer
Skills › Scientific Agent Skills › flowio › troubleshooting.md

troubleshooting.md

417 lines · skills/flowio/references/troubleshooting.md at 5f2bc03

Open on GitHub
1# FlowIO Troubleshooting and Safety
2
3Use the narrowest remedy that matches the observed failure. Keep the original
4file unchanged and record every recovery option used.
5

FlowIO Troubleshooting and Safety

Use the narrowest remedy that matches the observed failure. Keep the original file unchanged and record every recovery option used.

Raw lines 1 to 5
6## Confirm the Runtime First
7
8```bash
9uv run python -c "import flowio; print(flowio.__version__)"
10```
12This skill targets `1.4.0`. If the runtime differs, inspect that release's API
13and changelog before assuming examples are compatible.

Confirm the Runtime First

uv run python -c "import flowio; print(flowio.__version__)"

This skill targets 1.4.0. If the runtime differs, inspect that release's API and changelog before assuming examples are compatible.

Raw lines 6 to 14
15## Import Errors for Exceptions
17Symptom:
19```text
20ImportError: cannot import name 'FCSParsingError' from 'flowio'
21```
23Cause: Exception classes are not top-level exports.
25Correct:
27```python
28from flowio import FlowData
29from flowio.exceptions import FCSParsingError
30```

Import Errors for Exceptions

Symptom:

ImportError: cannot import name 'FCSParsingError' from 'flowio'

Cause: Exception classes are not top-level exports.

Correct:

from flowio import FlowData
from flowio.exceptions import FCSParsingError
Raw lines 15 to 31
32## `MultipleDataSetsError`
34Symptom: Opening a file with `FlowData(...)` reports that it contains multiple
35datasets.
37Use:
39```python
40from flowio import read_multiple_data_sets
42datasets = read_multiple_data_sets("legacy.fcs")
43```
45Do not manually treat `text["nextdata"]` as an absolute offset. The legacy
46format uses relative offsets, and FCS 3.1 deprecated this representation.

MultipleDataSetsError

Symptom: Opening a file with FlowData(...) reports that it contains multiple datasets.

Use:

from flowio import read_multiple_data_sets

datasets = read_multiple_data_sets("legacy.fcs")

Do not manually treat text["nextdata"] as an absolute offset. The legacy format uses relative offsets, and FCS 3.1 deprecated this representation.

Raw lines 32 to 47
48## `DataOffsetDiscrepancyError`
50Symptom: HEADER and TEXT identify different DATA byte locations.
52Default behavior is correct: stop rather than guessing.
54Triage:
561. Preserve the source file and calculate a checksum.
572. Confirm the file came directly from a known instrument/exporter.
583. Check vendor documentation or a known-good file from the same software.
594. Prefer TEXT offsets only when evidence supports them:
61 ```python
62 flow = FlowData(
63 "known-file.fcs",
64 ignore_offset_discrepancy=True,
65 )
66 ```
685. Use HEADER offsets only when evidence supports HEADER:
70 ```python
71 flow = FlowData(
72 "known-file.fcs",
73 use_header_offsets=True,
74 )
75 ```
776. Validate event count, channel ranges, distributions, and controls.
79Do not set both options reflexively or make them global defaults.

DataOffsetDiscrepancyError

Symptom: HEADER and TEXT identify different DATA byte locations.

Default behavior is correct: stop rather than guessing.

Triage:

  1. Preserve the source file and calculate a checksum.
  2. Confirm the file came directly from a known instrument/exporter.
  3. Check vendor documentation or a known-good file from the same software.
  4. Prefer TEXT offsets only when evidence supports them:

python flow = FlowData( "known-file.fcs", ignore_offset_discrepancy=True, )

  1. Use HEADER offsets only when evidence supports HEADER:

python flow = FlowData( "known-file.fcs", use_header_offsets=True, )

  1. Validate event count, channel ranges, distributions, and controls.

Do not set both options reflexively or make them global defaults.

Raw lines 48 to 80
81## Off-by-One DATA Offset
83Some producers report the final DATA byte as exclusive rather than inclusive.
84For a known instance:
86```python
87flow = FlowData(
88 "known-off-by-one.fcs",
89 ignore_offset_error=True,
91```
93FlowIO emits a warning stating that event data should be reviewed. Preserve
94that warning in logs and perform distribution/control checks.

Off-by-One DATA Offset

Some producers report the final DATA byte as exclusive rather than inclusive. For a known instance:

flow = FlowData(
    "known-off-by-one.fcs",
    ignore_offset_error=True,
)

FlowIO emits a warning stating that event data should be reviewed. Preserve that warning in logs and perform distribution/control checks.

Raw lines 81 to 95
96## Large Files With Zero HEADER DATA Offsets
98FCS 3.1 requires HEADER DATA offsets to be zero when a segment extends beyond
99the eight-digit HEADER limit; real offsets remain in TEXT. FlowIO recognizes
100this case.
102Do not enable `use_header_offsets=True` merely because the file is large.
103Doing so can select zeros instead of the valid TEXT values.

Large Files With Zero HEADER DATA Offsets

FCS 3.1 requires HEADER DATA offsets to be zero when a segment extends beyond the eight-digit HEADER limit; real offsets remain in TEXT. FlowIO recognizes this case.

Do not enable use_header_offsets=True merely because the file is large. Doing so can select zeros instead of the valid TEXT values.

Raw lines 96 to 104
105## `only_text=True` Followed by Array Failure
107Symptom: `as_array()` fails after metadata-only parsing.
109Cause: `only_text=True` intentionally leaves `events` unset.
111Reopen normally:
113```python
114metadata_only = FlowData("sample.fcs", only_text=True)
115# ...decide whether full loading is safe...
116with_events = FlowData("sample.fcs")
117events = with_events.as_array()
118```

only_text=True Followed by Array Failure

Symptom: as_array() fails after metadata-only parsing.

Cause: only_text=True intentionally leaves events unset.

Reopen normally:

metadata_only = FlowData("sample.fcs", only_text=True)
# ...decide whether full loading is safe...
with_events = FlowData("sample.fcs")
events = with_events.as_array()
Raw lines 105 to 119
120## Unexpected Metadata Lookup Results
122Symptom: `flow.text.get("$DATE")` returns `None`.
124Use normalized keys:
126```python
127flow.text.get("date")
128flow.text.get("cyt")
129flow.text.get("spillover", flow.text.get("spill"))
130```
132All parsed keys are lowercase and omit `$`.
134FlowIO 1.4.0 also removes literal `$` characters inside values. If exact TEXT
135fidelity is required, inspect the source bytes with a standards-aware tool
136rather than reconstructing metadata from `flow.text`.

Unexpected Metadata Lookup Results

Symptom: flow.text.get("$DATE") returns None.

Use normalized keys:

flow.text.get("date")
flow.text.get("cyt")
flow.text.get("spillover", flow.text.get("spill"))

All parsed keys are lowercase and omit $.

FlowIO 1.4.0 also removes literal $ characters inside values. If exact TEXT fidelity is required, inspect the source bytes with a standards-aware tool rather than reconstructing metadata from flow.text.

Raw lines 120 to 137
138## Unexpected Event Values
140Compare both representations:
142```python
143encoded = flow.as_array(preprocess=False)
144scaled = flow.as_array(preprocess=True)
145```
147Then inspect:
149- `flow.data_type`
150- `flow.channels[n]["pne"]`
151- `flow.channels[n]["png"]`
152- `flow.channels[n]["pnr"]`
153- `flow.text.get("timestep")`
154- `flow.time_index`
156FlowIO preprocessing divides by gain; it does not multiply by gain. It also
157does not apply compensation. If values differ from a higher-level application,
158check whether that application applied compensation, display transforms, or
159vendor-specific scaling.
161For integer DATA, `preprocess=False` still contains PnR-masked decoded values.
162Also check `flow.text.get(f"p{n}g")` (with the actual parameter number) against
163`flow.channels[n]["png"]`: a recognized Time channel is forced to gain one.

Unexpected Event Values

Compare both representations:

encoded = flow.as_array(preprocess=False)
scaled = flow.as_array(preprocess=True)

Then inspect:

  • flow.data_type
  • flow.channels[n]["pne"]
  • flow.channels[n]["png"]
  • flow.channels[n]["pnr"]
  • flow.text.get("timestep")
  • flow.time_index

FlowIO preprocessing divides by gain; it does not multiply by gain. It also does not apply compensation. If values differ from a higher-level application, check whether that application applied compensation, display transforms, or vendor-specific scaling.

For integer DATA, preprocess=False still contains PnR-masked decoded values. Also check flow.text.get(f"p{n}g") (with the actual parameter number) against flow.channels[n]["png"]: a recognized Time channel is forced to gain one.

Raw lines 138 to 164
165## Declared Counts Disagree With DATA
167`as_array()` infers rows from loaded DATA and does not enforce `$TOT`. Check
168`len(flow.events) == flow.event_count * flow.channel_count` before converting
169and compare the resulting shape. Do not silently replace the declared count
170or trust metadata-only inspection as a DATA validation. The bundled inspector
171rejects this mismatch with `--stats`.

Declared Counts Disagree With DATA

as_array() infers rows from loaded DATA and does not enforce $TOT. Check len(flow.events) == flow.event_count * flow.channel_count before converting and compare the resulting shape. Do not silently replace the declared count or trust metadata-only inspection as a DATA validation. The bundled inspector rejects this mismatch with --stats.

Raw lines 165 to 172
173## Channel Classification Looks Wrong
175`scatter_indices`, `fluoro_indices`, and `time_index` are label-based
176conveniences. Instrument/vendor labels can be unusual.
178Inspect all channel metadata:
180```python
181for parameter_number, channel in flow.channels.items():
182 print(parameter_number, channel)
183```
185Use an explicit, provenance-backed mapping for downstream analysis. Do not
186rename columns solely from guessed channel type.
188`flow.null_channels` contains PnN labels supplied through
189`null_channel_list`, not integer indices. Matching labels are omitted from the
190derived index lists but remain in the event array.

Channel Classification Looks Wrong

scatter_indices, fluoro_indices, and time_index are label-based conveniences. Instrument/vendor labels can be unusual.

Inspect all channel metadata:

for parameter_number, channel in flow.channels.items():
    print(parameter_number, channel)

Use an explicit, provenance-backed mapping for downstream analysis. Do not rename columns solely from guessed channel type.

flow.null_channels contains PnN labels supplied through null_channel_list, not integer indices. Matching labels are omitted from the derived index lists but remain in the event array.

Raw lines 173 to 191
192## Closed File Handle
194`FlowData` closes a file handle passed by the caller. Do not expect to reuse it:
196```python
197with open("sample.fcs", "rb") as handle:
198 flow = FlowData(handle)
199 # handle is already closed here by FlowIO 1.4.0
200```
202Prefer a path. In particular, pass a path to `read_multiple_data_sets()`;
203passing one handle fails when the utility tries to read the second dataset
204after the first `FlowData` closed it.

Closed File Handle

FlowData closes a file handle passed by the caller. Do not expect to reuse it:

with open("sample.fcs", "rb") as handle:
    flow = FlowData(handle)
    # handle is already closed here by FlowIO 1.4.0

Prefer a path. In particular, pass a path to read_multiple_data_sets(); passing one handle fails when the utility tries to read the second dataset after the first FlowData closed it.

Raw lines 192 to 205
206## Duplicate or Empty DataFrame Columns
208PnN values may be duplicated or malformed, and PnS is optional. Validate and
209make names unique before constructing a DataFrame. Retain the original PnN/PnS
210lists in provenance.
212See `workflows.md` for a deterministic `unique_labels()` helper that also
213avoids collisions with existing names such as `FL1-A__2`. Keep the original
214PnN mapping for compensation metadata.

Duplicate or Empty DataFrame Columns

PnN values may be duplicated or malformed, and PnS is optional. Validate and make names unique before constructing a DataFrame. Retain the original PnN/PnS lists in provenance.

See workflows.md for a deterministic unique_labels() helper that also avoids collisions with existing names such as FL1-A__2. Keep the original PnN mapping for compensation metadata.

Raw lines 206 to 215
216## `create_fcs()` Receives a Path
218Incorrect:
220```python
221create_fcs("output.fcs", values, labels)
222```
224Correct:
226```python
227with open("output.fcs", "xb") as handle:
228 create_fcs(handle, values.ravel(order="C"), labels)
229```
231The first argument is a binary file handle, not a path.

create_fcs() Receives a Path

Incorrect:

create_fcs("output.fcs", values, labels)

Correct:

with open("output.fcs", "xb") as handle:
    create_fcs(handle, values.ravel(order="C"), labels)

The first argument is a binary file handle, not a path.

Raw lines 216 to 232
233## `create_fcs()` Receives a 2-D Array
235The writer expects flattened event data. Validate before flattening:
237```python
238if values.ndim != 2:
239 raise ValueError("expected events x channels")
240if values.shape[1] != len(labels):
241 raise ValueError("label count mismatch")
243flat = values.astype("float32", copy=False).ravel(order="C")
244```
246A wrong flattening order silently changes event/channel alignment. Use C order,
247where all channel values for one event are adjacent.

create_fcs() Receives a 2-D Array

The writer expects flattened event data. Validate before flattening:

if values.ndim != 2:
    raise ValueError("expected events x channels")
if values.shape[1] != len(labels):
    raise ValueError("label count mismatch")

flat = values.astype("float32", copy=False).ravel(order="C")

A wrong flattening order silently changes event/channel alignment. Use C order, where all channel values for one event are adjacent.

Raw lines 233 to 248
249## Number of Data Points Is Not a Multiple of Channels
251Cause: The flattened event-data length does not divide evenly by the channel
252label count.
254Check:
256```python
257if flat.size % len(labels) != 0:
258 raise ValueError("incomplete event row")
259```
261Prefer checking the original two-dimensional shape before flattening.

Number of Data Points Is Not a Multiple of Channels

Cause: The flattened event-data length does not divide evenly by the channel label count.

Check:

if flat.size % len(labels) != 0:
    raise ValueError("incomplete event row")

Prefer checking the original two-dimensional shape before flattening.

Raw lines 249 to 262
263## PnN and PnS Count Mismatch
265`opt_channel_names` must have the same length as `channel_names`. Use `None` or
266`""` for an individual missing PnS label, or omit the whole argument.

PnN and PnS Count Mismatch

opt_channel_names must have the same length as channel_names. Use None or "" for an individual missing PnS label, or omit the whole argument.

Raw lines 263 to 267
268## `PnEWarning` During Creation
270FlowIO writes floating-point FCS output, which requires PnE `0,0`. Supplying
271nonzero PnE metadata produces a warning and FlowIO writes `0,0`.
273Do not suppress the warning and assume encoded log-amplified semantics were
274preserved. Export the desired numerical representation explicitly and document
275it.

PnEWarning During Creation

FlowIO writes floating-point FCS output, which requires PnE 0,0. Supplying nonzero PnE metadata produces a warning and FlowIO writes 0,0.

Do not suppress the warning and assume encoded log-amplified semantics were preserved. Export the desired numerical representation explicitly and document it.

Raw lines 268 to 276
277## Metadata Missing After `write_fcs()`
279`write_fcs(metadata=...)` does not merge the supplied dictionary with all source
280TEXT metadata.
282- `metadata=None` preserves selected defaults (`cyt`, `date`,
283 `spillover`/`spill`).
284- `metadata={}` writes the minimum generated metadata.
285- A custom dictionary writes those custom fields instead of the selected
286 defaults.
288Reopen the output and inspect `text`.
290For floating-point input, also compare:
292```python
293source_raw = source.as_array(preprocess=False)
294source_scaled = source.as_array(preprocess=True)
295output_raw = output.as_array(preprocess=False)
296output_scaled = output.as_array(preprocess=True)
297```
299Default `write_fcs()` preservation does not retain PnG or `timestep`. Raw
300values can match while scaled values differ.

Metadata Missing After write_fcs()

write_fcs(metadata=...) does not merge the supplied dictionary with all source TEXT metadata.

  • metadata=None preserves selected defaults (cyt, date, spillover/spill).
  • metadata={} writes the minimum generated metadata.
  • A custom dictionary writes those custom fields instead of the selected defaults.

Reopen the output and inspect text.

For floating-point input, also compare:

source_raw = source.as_array(preprocess=False)
source_scaled = source.as_array(preprocess=True)
output_raw = output.as_array(preprocess=False)
output_scaled = output.as_array(preprocess=True)

Default write_fcs() preservation does not retain PnG or timestep. Raw values can match while scaled values differ.

Raw lines 277 to 301
302## Output Values Differ Slightly
304FlowIO writes single-precision floats. Small round-trip differences are
305expected.
307Use:
309```python
310import numpy as np
312np.testing.assert_allclose(
313 reopened.as_array(preprocess=False),
314 expected,
315 rtol=1e-6,
316 atol=1e-6,
318```
320Set tolerances according to the data scale and scientific requirements.

Output Values Differ Slightly

FlowIO writes single-precision floats. Small round-trip differences are expected.

Use:

import numpy as np

np.testing.assert_allclose(
    reopened.as_array(preprocess=False),
    expected,
    rtol=1e-6,
    atol=1e-6,
)

Set tolerances according to the data scale and scientific requirements.

Raw lines 302 to 321
322## Big-Endian Writer Portability
324FlowIO 1.4.0 declares little-endian output but writes `array('f')` bytes in the
325runtime's native byte order. This skill has not verified export behavior on
326big-endian hardware. Treat big-endian writing as unverified and validate with
327an independent reader before relying on the output.

Big-Endian Writer Portability

FlowIO 1.4.0 declares little-endian output but writes array('f') bytes in the runtime's native byte order. This skill has not verified export behavior on big-endian hardware. Treat big-endian writing as unverified and validate with an independent reader before relying on the output.

Raw lines 322 to 328
329## Memory Exhaustion
331FlowIO loads the DATA segment, and `as_array()` allocates another float64 array.
332It has no chunked reader.
334Before loading:
336```python
337estimated_array_bytes = event_count * channel_count * 8
338```
340This estimate covers only the `as_array()` result.
342`create_fcs()` also converts NumPy inputs to an `array('f')` buffer. For large
343writes, include roughly four bytes per flattened value for that buffer, unless
344the input is already `array('f')`.
346Mitigations:
348- Use `only_text=True` for inventory.
349- Reject unexpectedly large files before parsing.
350- Avoid creating multiple full arrays simultaneously.
351- Delete references to no-longer-needed arrays before the next file.
352- Use a resource-limited worker or a different streaming-capable tool when the
353 file cannot fit safely.
355Do not advertise "processing in chunks" as a FlowIO feature.

Memory Exhaustion

FlowIO loads the DATA segment, and as_array() allocates another float64 array. It has no chunked reader.

Before loading:

estimated_array_bytes = event_count * channel_count * 8

This estimate covers only the as_array() result.

create_fcs() also converts NumPy inputs to an array('f') buffer. For large writes, include roughly four bytes per flattened value for that buffer, unless the input is already array('f').

Mitigations:

  • Use only_text=True for inventory.
  • Reject unexpectedly large files before parsing.
  • Avoid creating multiple full arrays simultaneously.
  • Delete references to no-longer-needed arrays before the next file.
  • Use a resource-limited worker or a different streaming-capable tool when the file cannot fit safely.

Do not advertise "processing in chunks" as a FlowIO feature.

Raw lines 329 to 356
357## Untrusted FCS Files
359An FCS file is structured binary input. A malformed file can consume excessive
360memory/CPU or exploit defects in any parser.
362For files from untrusted uploaders:
3641. Enforce an input-size limit before `FlowData`.
3652. Parse in an isolated, resource-limited process/container.
3663. Keep strict offset checks.
3674. Use a read-only copy and a dedicated output directory.
3685. Do not overwrite the source.
3696. Log parser version, warnings, and a cryptographic checksum.
3707. Keep dependencies patched and test upgrades against representative files.
372Metadata-derived array limits do not cap parser allocation or whole-process
373RSS. The bundled inspector also rejects non-list modes and non-I/F/D types,
374but upstream parsing still runs before those metadata checks.
376The bundled inspector defaults to metadata-only parsing and a size limit, but
377it is not a malware sandbox.

Untrusted FCS Files

An FCS file is structured binary input. A malformed file can consume excessive memory/CPU or exploit defects in any parser.

For files from untrusted uploaders:

  1. Enforce an input-size limit before FlowData.
  2. Parse in an isolated, resource-limited process/container.
  3. Keep strict offset checks.
  4. Use a read-only copy and a dedicated output directory.
  5. Do not overwrite the source.
  6. Log parser version, warnings, and a cryptographic checksum.
  7. Keep dependencies patched and test upgrades against representative files.

Metadata-derived array limits do not cap parser allocation or whole-process RSS. The bundled inspector also rejects non-list modes and non-I/F/D types, but upstream parsing still runs before those metadata checks.

The bundled inspector defaults to metadata-only parsing and a size limit, but it is not a malware sandbox.

Raw lines 357 to 378
379## Privacy and Clinical Metadata
381TEXT and ANALYSIS can include:
383- Subject or patient identifiers
384- Sample/tube identifiers
385- Acquisition dates and times
386- Operator names
387- Institution and instrument identifiers
388- Free-text comments
390Before logging, exporting, or sharing:
392- Use an allowlist of metadata keys.
393- Avoid `--include-text` unless necessary.
394- Keep identifiers out of filenames where possible.
395- Apply the project's de-identification and access-control policy.
396- Verify the rewritten file, CSV, JSON, logs, and error messages.
398Removing selected TEXT fields does not by itself establish regulatory
399de-identification.

Privacy and Clinical Metadata

TEXT and ANALYSIS can include:

  • Subject or patient identifiers
  • Sample/tube identifiers
  • Acquisition dates and times
  • Operator names
  • Institution and instrument identifiers
  • Free-text comments

Before logging, exporting, or sharing:

  • Use an allowlist of metadata keys.
  • Avoid --include-text unless necessary.
  • Keep identifiers out of filenames where possible.
  • Apply the project's de-identification and access-control policy.
  • Verify the rewritten file, CSV, JSON, logs, and error messages.

Removing selected TEXT fields does not by itself establish regulatory de-identification.

Raw lines 379 to 400
401## Minimal Integrity Record
403```python
404import hashlib
405from pathlib import Path
408def sha256_file(path: Path) -> str:
409 digest = hashlib.sha256()
410 with path.open("rb") as handle:
411 for chunk in iter(lambda: handle.read(1024 * 1024), b""):
412 digest.update(chunk)
413 return digest.hexdigest()
414```
416Record the checksum alongside FlowIO version, parse flags, warnings, event
417semantics, and validation results.

Minimal Integrity Record

import hashlib
from pathlib import Path


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()

Record the checksum alongside FlowIO version, parse flags, warnings, event semantics, and validation results.

Raw lines 401 to 417