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

workflows.md

391 lines · skills/flowio/references/workflows.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# FlowIO Workflows
2
3These patterns target `flowio==1.4.0`. Examples below were exercised with
4synthetic files; placeholder filenames and `source_events` must be supplied for
5real data. Keep raw files immutable and write derived outputs separately.
6

FlowIO Workflows

These patterns target flowio==1.4.0. Examples below were exercised with synthetic files; placeholder filenames and source_events must be supplied for real data. Keep raw files immutable and write derived outputs separately.

Raw lines 1 to 6
7## 1. Inventory a File Without Loading Events
8
9Use `only_text=True` when the goal is file structure, labels, or acquisition
10metadata:
12```python
13from pathlib import Path
15from flowio import FlowData
17path = Path("sample.fcs")
18flow = FlowData(path, only_text=True)
20summary = {
21 "file": path.name,
22 "size_bytes": path.stat().st_size,
23 "fcs_version": flow.version,
24 "data_type": flow.data_type,
25 "event_count": flow.event_count,
26 "channel_count": flow.channel_count,
27 "pnn_labels": flow.pnn_labels,
28 "pns_labels": flow.pns_labels,
29 "date": flow.text.get("date"),
30 "instrument": flow.text.get("cyt"),
32print(summary)
33```
35Do not call `as_array()` on this object.
36`only_text=True` skips DATA loading but still parses ANALYSIS.
38For an inventory that also handles multi-dataset files and emits JSON, use:
40```bash
41FLOWIO_SKILL_DIR="skills/flowio" # Set to the installed skill directory.
42uv run --no-project --with "flowio==1.4.0" \
43 python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs
44```

1. Inventory a File Without Loading Events

Use only_text=True when the goal is file structure, labels, or acquisition metadata:

from pathlib import Path

from flowio import FlowData

path = Path("sample.fcs")
flow = FlowData(path, only_text=True)

summary = {
    "file": path.name,
    "size_bytes": path.stat().st_size,
    "fcs_version": flow.version,
    "data_type": flow.data_type,
    "event_count": flow.event_count,
    "channel_count": flow.channel_count,
    "pnn_labels": flow.pnn_labels,
    "pns_labels": flow.pns_labels,
    "date": flow.text.get("date"),
    "instrument": flow.text.get("cyt"),
}
print(summary)

Do not call as_array() on this object. only_text=True skips DATA loading but still parses ANALYSIS.

For an inventory that also handles multi-dataset files and emits JSON, use:

FLOWIO_SKILL_DIR="skills/flowio"  # Set to the installed skill directory.
uv run --no-project --with "flowio==1.4.0" \
  python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs
Raw lines 7 to 45
Runs code scripts/inspect_fcs.py :43
46## 2. Load Encoded or Metadata-Scaled Events
48```python
49from flowio import FlowData
51flow = FlowData("sample.fcs")
53# Gain/log/time scaling from FCS metadata.
54scaled = flow.as_array(preprocess=True)
56# Decoded DATA values without gain/log/time scaling (integer masks remain).
57encoded = flow.as_array(preprocess=False)
59assert scaled.shape == (flow.event_count, flow.channel_count)
60assert encoded.shape == scaled.shape
61```
63Check shape against both declared counts: FlowIO infers the number of rows from
64DATA rather than validating `$TOT`. Integer DATA has already been masked to its
65PnR range even with `preprocess=False`. Neither array is compensated or gated.
66Name output variables to preserve this distinction; avoid generic names such
67as `processed`.

2. Load Encoded or Metadata-Scaled Events

from flowio import FlowData

flow = FlowData("sample.fcs")

# Gain/log/time scaling from FCS metadata.
scaled = flow.as_array(preprocess=True)

# Decoded DATA values without gain/log/time scaling (integer masks remain).
encoded = flow.as_array(preprocess=False)

assert scaled.shape == (flow.event_count, flow.channel_count)
assert encoded.shape == scaled.shape

Check shape against both declared counts: FlowIO infers the number of rows from DATA rather than validating $TOT. Integer DATA has already been masked to its PnR range even with preprocess=False. Neither array is compensated or gated. Name output variables to preserve this distinction; avoid generic names such as processed.

Raw lines 46 to 68
69## 3. Build a DataFrame
71This example assumes pandas is already present in the project environment.
72Add and lock an environment-compatible pandas release only when tabular
73integration is required; FlowIO itself does not depend on pandas. The tested
74pandas 3.0.6 stack is for standalone FlowIO. FlowKit has separate dependency
75constraints, so retain the compatible pandas version in its environment.
77FCS files can contain duplicate or empty labels. Make DataFrame names unique
78instead of silently assuming PnN values are valid columns:
80```python
81import pandas as pd
82from flowio import FlowData
85def unique_labels(labels):
86 bases = [value.strip() or f"channel_{i + 1}" for i, value in enumerate(labels)]
87 reserved = set(bases) # Do not steal a later detector's existing name.
88 used = set()
89 result = []
90 for base in bases:
91 name = base
92 if name in used:
93 suffix = 2
94 while f"{base}__{suffix}" in reserved or f"{base}__{suffix}" in used:
95 suffix += 1
96 name = f"{base}__{suffix}"
97 used.add(name)
98 result.append(name)
99 return result
102flow = FlowData("sample.fcs")
103columns = unique_labels(flow.pnn_labels)
104assert len(columns) == len(set(columns))
105values = flow.as_array(preprocess=True)
106if values.shape != (flow.event_count, flow.channel_count):
107 raise ValueError("DATA shape disagrees with declared event/channel counts")
108frame = pd.DataFrame(
109 values,
110 columns=columns,
113frame.attrs.update(
114 {
115 "flowio_version": "1.4.0",
116 "fcs_version": flow.version,
117 "event_semantics": "gain/log/time scaled; uncompensated; ungated",
118 "source_pnn_labels": flow.pnn_labels,
119 "source_pns_labels": flow.pns_labels,
120 }
122```
124For example, `["A", "A", "A__2"]` becomes `["A", "A__3", "A__2"]`.
125The position-to-source-label mapping is authoritative; never use renamed export
126labels to align a spillover matrix.
128DataFrame attributes are not preserved by every export format. Store a
129sidecar JSON provenance record for durable pipelines.

3. Build a DataFrame

This example assumes pandas is already present in the project environment. Add and lock an environment-compatible pandas release only when tabular integration is required; FlowIO itself does not depend on pandas. The tested pandas 3.0.6 stack is for standalone FlowIO. FlowKit has separate dependency constraints, so retain the compatible pandas version in its environment.

FCS files can contain duplicate or empty labels. Make DataFrame names unique instead of silently assuming PnN values are valid columns:

import pandas as pd
from flowio import FlowData


def unique_labels(labels):
    bases = [value.strip() or f"channel_{i + 1}" for i, value in enumerate(labels)]
    reserved = set(bases)  # Do not steal a later detector's existing name.
    used = set()
    result = []
    for base in bases:
        name = base
        if name in used:
            suffix = 2
            while f"{base}__{suffix}" in reserved or f"{base}__{suffix}" in used:
                suffix += 1
            name = f"{base}__{suffix}"
        used.add(name)
        result.append(name)
    return result


flow = FlowData("sample.fcs")
columns = unique_labels(flow.pnn_labels)
assert len(columns) == len(set(columns))
values = flow.as_array(preprocess=True)
if values.shape != (flow.event_count, flow.channel_count):
    raise ValueError("DATA shape disagrees with declared event/channel counts")
frame = pd.DataFrame(
    values,
    columns=columns,
)

frame.attrs.update(
    {
        "flowio_version": "1.4.0",
        "fcs_version": flow.version,
        "event_semantics": "gain/log/time scaled; uncompensated; ungated",
        "source_pnn_labels": flow.pnn_labels,
        "source_pns_labels": flow.pns_labels,
    }
)

For example, ["A", "A", "A__2"] becomes ["A", "A__3", "A__2"]. The position-to-source-label mapping is authoritative; never use renamed export labels to align a spillover matrix.

DataFrame attributes are not preserved by every export format. Store a sidecar JSON provenance record for durable pipelines.

Raw lines 69 to 130
131## 4. Export CSV Safely
133CSV loses most FCS metadata and type information. Export a sidecar record:
135```python
136import json
137from pathlib import Path
139import pandas as pd
140from flowio import FlowData
142source = Path("sample.fcs")
143flow = FlowData(source)
144values = flow.as_array(preprocess=False)
145if values.shape != (flow.event_count, flow.channel_count):
146 raise ValueError("DATA shape disagrees with declared event/channel counts")
148frame = pd.DataFrame(
149 values,
150 columns=unique_labels(flow.pnn_labels), # Helper from the previous workflow.
152csv_path = Path("sample.events.csv")
153with csv_path.open("x", encoding="utf-8", newline="") as handle:
154 frame.to_csv(handle, index=False)
156provenance = {
157 "source_file": source.name,
158 "flowio_version": "1.4.0",
159 "fcs_version": flow.version,
160 "event_semantics": "decoded DATA values; preprocess=False; integer range masks retained",
161 "pnn_labels": flow.pnn_labels,
162 "pns_labels": flow.pns_labels,
163 "event_count": flow.event_count,
164 "channel_count": flow.channel_count,
166with Path("sample.events.json").open("x", encoding="utf-8") as handle:
167 handle.write(json.dumps(provenance, indent=2) + "\n")
168```
170Before sharing either output, review labels and metadata for subject or sample
171identifiers.

4. Export CSV Safely

CSV loses most FCS metadata and type information. Export a sidecar record:

import json
from pathlib import Path

import pandas as pd
from flowio import FlowData

source = Path("sample.fcs")
flow = FlowData(source)
values = flow.as_array(preprocess=False)
if values.shape != (flow.event_count, flow.channel_count):
    raise ValueError("DATA shape disagrees with declared event/channel counts")

frame = pd.DataFrame(
    values,
    columns=unique_labels(flow.pnn_labels),  # Helper from the previous workflow.
)
csv_path = Path("sample.events.csv")
with csv_path.open("x", encoding="utf-8", newline="") as handle:
    frame.to_csv(handle, index=False)

provenance = {
    "source_file": source.name,
    "flowio_version": "1.4.0",
    "fcs_version": flow.version,
    "event_semantics": "decoded DATA values; preprocess=False; integer range masks retained",
    "pnn_labels": flow.pnn_labels,
    "pns_labels": flow.pns_labels,
    "event_count": flow.event_count,
    "channel_count": flow.channel_count,
}
with Path("sample.events.json").open("x", encoding="utf-8") as handle:
    handle.write(json.dumps(provenance, indent=2) + "\n")

Before sharing either output, review labels and metadata for subject or sample identifiers.

Raw lines 131 to 172
173## 5. Batch Metadata Inventory
175This pattern reads no event arrays:
177```python
178from pathlib import Path
180from flowio import read_multiple_data_sets
181from flowio.exceptions import FlowIOException
183records = []
184failures = []
186for path in sorted(Path("fcs").rglob("*.fcs")):
187 try:
188 datasets = read_multiple_data_sets(path, only_text=True)
189 for dataset_index, flow in enumerate(datasets):
190 records.append(
191 {
192 "file": str(path),
193 "dataset_index": dataset_index,
194 "fcs_version": flow.version,
195 "event_count": flow.event_count,
196 "channel_count": flow.channel_count,
197 "pnn_labels": flow.pnn_labels,
198 }
199 )
200 except (FlowIOException, OSError, ValueError) as error:
201 failures.append({"file": str(path), "error": str(error)})
202```
204Do not automatically retry failed files with every relaxed offset option.
205Triage each error type and file source.

5. Batch Metadata Inventory

This pattern reads no event arrays:

from pathlib import Path

from flowio import read_multiple_data_sets
from flowio.exceptions import FlowIOException

records = []
failures = []

for path in sorted(Path("fcs").rglob("*.fcs")):
    try:
        datasets = read_multiple_data_sets(path, only_text=True)
        for dataset_index, flow in enumerate(datasets):
            records.append(
                {
                    "file": str(path),
                    "dataset_index": dataset_index,
                    "fcs_version": flow.version,
                    "event_count": flow.event_count,
                    "channel_count": flow.channel_count,
                    "pnn_labels": flow.pnn_labels,
                }
            )
    except (FlowIOException, OSError, ValueError) as error:
        failures.append({"file": str(path), "error": str(error)})

Do not automatically retry failed files with every relaxed offset option. Triage each error type and file source.

Raw lines 173 to 206
207## 6. Read Legacy Multi-Dataset Files
209Pass a path, not an open handle. FlowIO 1.4.0 closes caller-provided handles,
210so the utility cannot reuse one for the second dataset.
212```python
213from flowio import read_multiple_data_sets
215datasets = read_multiple_data_sets(
216 "legacy.lmd",
217 only_text=False,
220for dataset_index, flow in enumerate(datasets):
221 encoded = flow.as_array(preprocess=False)
222 print(dataset_index, encoded.shape, flow.text.get("nextdata"))
223```
225If a known off-by-one vendor file requires recovery:
227```python
228datasets = read_multiple_data_sets(
229 "known-vendor-file.lmd",
230 ignore_offset_error=True,
232```
234Capture the FlowIO warning and validate event values before using them.

6. Read Legacy Multi-Dataset Files

Pass a path, not an open handle. FlowIO 1.4.0 closes caller-provided handles, so the utility cannot reuse one for the second dataset.

from flowio import read_multiple_data_sets

datasets = read_multiple_data_sets(
    "legacy.lmd",
    only_text=False,
)

for dataset_index, flow in enumerate(datasets):
    encoded = flow.as_array(preprocess=False)
    print(dataset_index, encoded.shape, flow.text.get("nextdata"))

If a known off-by-one vendor file requires recovery:

datasets = read_multiple_data_sets(
    "known-vendor-file.lmd",
    ignore_offset_error=True,
)

Capture the FlowIO warning and validate event values before using them.

Raw lines 207 to 235
236## 7. Write a New FCS File From a 2-D Array
238```python
239from pathlib import Path
241import numpy as np
242from flowio import create_fcs
244events = np.asarray(source_events, dtype=np.float32)
245channel_names = ["FSC-A", "SSC-A", "FITC-A"]
246stain_names = ["Forward scatter", "Side scatter", "CD3"]
248if events.ndim != 2:
249 raise ValueError("source_events must be a 2-D events x channels array")
250if events.shape[1] != len(channel_names):
251 raise ValueError("array columns and channel_names differ")
252if len(stain_names) != len(channel_names):
253 raise ValueError("PnS and PnN label counts differ")
254if not np.isfinite(events).all():
255 raise ValueError("decide how to handle NaN/inf before FCS export")
257destination = Path("derived.fcs")
258if destination.exists():
259 raise FileExistsError(destination)
261with destination.open("xb") as handle:
262 create_fcs(
263 handle,
264 events.ravel(order="C"),
265 channel_names,
266 opt_channel_names=stain_names,
267 metadata_dict={
268 "date": "30-SEP-2026",
269 "src": "Derived event matrix",
270 },
271 )
272```
274Mode `"xb"` prevents accidental overwrite. Use `"wb"` only when replacement
275is intentional and controlled.

7. Write a New FCS File From a 2-D Array

from pathlib import Path

import numpy as np
from flowio import create_fcs

events = np.asarray(source_events, dtype=np.float32)
channel_names = ["FSC-A", "SSC-A", "FITC-A"]
stain_names = ["Forward scatter", "Side scatter", "CD3"]

if events.ndim != 2:
    raise ValueError("source_events must be a 2-D events x channels array")
if events.shape[1] != len(channel_names):
    raise ValueError("array columns and channel_names differ")
if len(stain_names) != len(channel_names):
    raise ValueError("PnS and PnN label counts differ")
if not np.isfinite(events).all():
    raise ValueError("decide how to handle NaN/inf before FCS export")

destination = Path("derived.fcs")
if destination.exists():
    raise FileExistsError(destination)

with destination.open("xb") as handle:
    create_fcs(
        handle,
        events.ravel(order="C"),
        channel_names,
        opt_channel_names=stain_names,
        metadata_dict={
            "date": "30-SEP-2026",
            "src": "Derived event matrix",
        },
    )

Mode "xb" prevents accidental overwrite. Use "wb" only when replacement is intentional and controlled.

Raw lines 236 to 276
277## 8. Rewrite Metadata Without Changing Event Count
279`write_fcs()` is useful for selected metadata replacement:
281```python
282from pathlib import Path
284from flowio import FlowData
286source = Path("source.fcs")
287destination = Path("deidentified.fcs")
288if destination.exists():
289 raise FileExistsError(destination)
291flow = FlowData(source)
292flow.write_fcs(
293 destination,
294 metadata={
295 "src": "Deidentified derivative",
296 "cyt": flow.text.get("cyt", "Unknown instrument"),
297 },
299```
301Passing a metadata dictionary replaces FlowIO's default selected metadata. It
302does not merge with all source TEXT fields. This is useful for minimizing
303metadata, but inspect the reopened file rather than assuming which fields
304remain.
306`write_fcs()` converts the representation to FCS 3.1 single-precision float.
307It is not a lossless metadata patcher. For floating-point sources it can retain
308encoded values while dropping PnG or `timestep`, so compare both
309`preprocess=False` and `preprocess=True` arrays after reopening.

8. Rewrite Metadata Without Changing Event Count

write_fcs() is useful for selected metadata replacement:

from pathlib import Path

from flowio import FlowData

source = Path("source.fcs")
destination = Path("deidentified.fcs")
if destination.exists():
    raise FileExistsError(destination)

flow = FlowData(source)
flow.write_fcs(
    destination,
    metadata={
        "src": "Deidentified derivative",
        "cyt": flow.text.get("cyt", "Unknown instrument"),
    },
)

Passing a metadata dictionary replaces FlowIO's default selected metadata. It does not merge with all source TEXT fields. This is useful for minimizing metadata, but inspect the reopened file rather than assuming which fields remain.

write_fcs() converts the representation to FCS 3.1 single-precision float. It is not a lossless metadata patcher. For floating-point sources it can retain encoded values while dropping PnG or timestep, so compare both preprocess=False and preprocess=True arrays after reopening.

Raw lines 277 to 310
311## 9. Round-Trip Validation
313Validate every file you create:
315```python
316import numpy as np
317from flowio import FlowData
319check = FlowData("derived.fcs")
321assert check.version == "3.1"
322assert check.data_type == "F"
323assert check.event_count == events.shape[0]
324assert check.channel_count == events.shape[1]
325assert check.pnn_labels == channel_names
327roundtrip = check.as_array(preprocess=False)
328np.testing.assert_allclose(
329 roundtrip,
330 events,
331 rtol=1e-6,
332 atol=1e-6,
334```
336Also verify:
338- Expected PnS labels
339- Expected custom metadata
340- No unintended subject identifiers
341- Representative channel distributions
342- Spillover label order, if stored
343- Readability in the intended downstream tool

9. Round-Trip Validation

Validate every file you create:

import numpy as np
from flowio import FlowData

check = FlowData("derived.fcs")

assert check.version == "3.1"
assert check.data_type == "F"
assert check.event_count == events.shape[0]
assert check.channel_count == events.shape[1]
assert check.pnn_labels == channel_names

roundtrip = check.as_array(preprocess=False)
np.testing.assert_allclose(
    roundtrip,
    events,
    rtol=1e-6,
    atol=1e-6,
)

Also verify:

  • Expected PnS labels
  • Expected custom metadata
  • No unintended subject identifiers
  • Representative channel distributions
  • Spillover label order, if stored
  • Readability in the intended downstream tool
Raw lines 311 to 344
345## 10. Compute Inspection Statistics
347The bundled inspector computes finite-value statistics only when explicitly
348requested:
350```bash
351FLOWIO_SKILL_DIR="skills/flowio" # Set to the installed skill directory.
353# Metadata-scaled values
354uv run --no-project --with "flowio==1.4.0" \
355 python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs --stats
357# Encoded values
358uv run --no-project --with "flowio==1.4.0" \
359 python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs --stats --raw
361# Save JSON without overwriting an existing file
362uv run --no-project --with "flowio==1.4.0" \
363 python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs \
364 --output sample.inspect.json
365```
367Statistics require full event loading and another float64 array. Estimate
368memory before using `--stats` on a large file. The inspector's
369`--max-array-bytes` guard limits the estimated float64 array; this estimate
370does not include the decoded event container, temporary arrays, or metadata.
371It uses declared counts before loading; inconsistent loaded counts are rejected
372before the float64 allocation. A resource-limited process is needed for a hard
373peak-memory cap.

10. Compute Inspection Statistics

The bundled inspector computes finite-value statistics only when explicitly requested:

FLOWIO_SKILL_DIR="skills/flowio"  # Set to the installed skill directory.

# Metadata-scaled values
uv run --no-project --with "flowio==1.4.0" \
  python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs --stats

# Encoded values
uv run --no-project --with "flowio==1.4.0" \
  python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs --stats --raw

# Save JSON without overwriting an existing file
uv run --no-project --with "flowio==1.4.0" \
  python "$FLOWIO_SKILL_DIR/scripts/inspect_fcs.py" sample.fcs \
  --output sample.inspect.json

Statistics require full event loading and another float64 array. Estimate memory before using --stats on a large file. The inspector's --max-array-bytes guard limits the estimated float64 array; this estimate does not include the decoded event container, temporary arrays, or metadata. It uses declared counts before loading; inconsistent loaded counts are rejected before the float64 allocation. A resource-limited process is needed for a hard peak-memory cap.

Raw lines 345 to 374
Runs code scripts/inspect_fcs.py :355
375## 11. Hand Off to Higher-Level Analysis
377Use FlowIO to preserve file semantics, then hand the data to a cytometry
378analysis library when the task includes compensation or gating.
380A defensible handoff records:
382- Whether FlowIO preprocessing was used
383- Whether the compensation matrix came from `spill`/`spillover` or an external
384 control
385- Channel/PnN order used for matrix alignment
386- Transform names and parameters
387- Gate definitions and software version
389Do not apply FlowIO preprocessing blindly before a higher-level library. Check
390whether that library expects encoded, compensated, or already transformed
391values to avoid double scaling.

11. Hand Off to Higher-Level Analysis

Use FlowIO to preserve file semantics, then hand the data to a cytometry analysis library when the task includes compensation or gating.

A defensible handoff records:

  • Whether FlowIO preprocessing was used
  • Whether the compensation matrix came from spill/spillover or an external control
  • Channel/PnN order used for matrix alignment
  • Transform names and parameters
  • Gate definitions and software version

Do not apply FlowIO preprocessing blindly before a higher-level library. Check whether that library expects encoded, compensated, or already transformed values to avoid double scaling.

Raw lines 375 to 391