skills/flowio/references/troubleshooting.md
Use the narrowest remedy that matches the observed failure. Keep the original file unchanged and record every recovery option used.
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.
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
MultipleDataSetsErrorSymptom: 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.
DataOffsetDiscrepancyErrorSymptom: HEADER and TEXT identify different DATA byte locations.
Default behavior is correct: stop rather than guessing.
Triage:
Preserve the source file and calculate a checksum.
Confirm the file came directly from a known instrument/exporter.
Check vendor documentation or a known-good file from the same software.
Prefer TEXT offsets only when evidence supports them:
flow = FlowData(
"known-file.fcs",
ignore_offset_discrepancy=True,
)
Use HEADER offsets only when evidence supports HEADER:
flow = FlowData(
"known-file.fcs",
use_header_offsets=True,
)
Validate event count, channel ranges, distributions, and controls.
Do not set both options reflexively or make them global defaults.
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.
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.
only_text=True Followed by Array FailureSymptom: 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()
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.
Compare both representations:
encoded = flow.as_array(preprocess=False)
scaled = flow.as_array(preprocess=True)
Then inspect:
flow.data_typeflow.channels[n]["pne"]flow.channels[n]["png"]flow.channels[n]["pnr"]flow.text.get("timestep")flow.time_indexFlowIO 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.
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.
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.
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.
create_fcs() Receives a PathIncorrect:
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.
create_fcs() Receives a 2-D ArrayThe 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.
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.
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.
PnEWarning During CreationFlowIO 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.
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.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.
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.
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.
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:
only_text=True for inventory.Do not advertise "processing in chunks" as a FlowIO feature.
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:
FlowData.The bundled inspector defaults to metadata-only parsing and a size limit, but it is not a malware sandbox.
TEXT and ANALYSIS can include:
Before logging, exporting, or sharing:
--include-text unless necessary.Removing selected TEXT fields does not by itself establish regulatory de-identification.
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.