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

fcs_semantics.md

325 lines · skills/flowio/references/fcs_semantics.md at 5f2bc03

Open on GitHub
1# FCS Semantics for FlowIO
2
3Use this reference when the task depends on what values or metadata mean, not
4just how to call the API.
5

FCS Semantics for FlowIO

Use this reference when the task depends on what values or metadata mean, not just how to call the API.

Raw lines 1 to 5
6## Scope
7
8FlowIO reads FCS 2.0, 3.0, and 3.1 and writes a constrained FCS 3.1
9representation. It is a file-format library, not a complete flow-cytometry
10analysis system.
12Before downstream biological interpretation, decide whether the workflow also
13requires:
15- Spillover compensation
16- Logicle, biexponential, or arcsinh transformation
17- Quality-control filtering
18- Singlet, viability, or population gating
19- Batch correction or normalization
21FlowIO does none of these. Its optional preprocessing is limited to scaling
22defined by FCS acquisition metadata.

Scope

FlowIO reads FCS 2.0, 3.0, and 3.1 and writes a constrained FCS 3.1 representation. It is a file-format library, not a complete flow-cytometry analysis system.

Before downstream biological interpretation, decide whether the workflow also requires:

  • Spillover compensation
  • Logicle, biexponential, or arcsinh transformation
  • Quality-control filtering
  • Singlet, viability, or population gating
  • Batch correction or normalization

FlowIO does none of these. Its optional preprocessing is limited to scaling defined by FCS acquisition metadata.

Raw lines 6 to 23
24## File Segments
26An FCS dataset can contain:
28- **HEADER**: Version and byte offsets for other segments.
29- **TEXT**: Required and optional keyword/value metadata.
30- **DATA**: Event values.
31- **ANALYSIS**: Optional keyword/value results.
33FlowIO exposes these through `header`, `text`, `events`, and `analysis`.
35FCS 3.1 deprecated storing multiple datasets in one file through `$NEXTDATA`,
36but FlowIO can read legacy multi-dataset files with
37`read_multiple_data_sets()`.

File Segments

An FCS dataset can contain:

  • HEADER: Version and byte offsets for other segments.
  • TEXT: Required and optional keyword/value metadata.
  • DATA: Event values.
  • ANALYSIS: Optional keyword/value results.

FlowIO exposes these through header, text, events, and analysis.

FCS 3.1 deprecated storing multiple datasets in one file through $NEXTDATA, but FlowIO can read legacy multi-dataset files with read_multiple_data_sets().

Raw lines 24 to 38
39## TEXT Keyword Normalization
41The FCS standard treats keyword names as case-insensitive. Standard keywords
42are written with `$`, but FlowIO normalizes parsed keys:
441. Strip the leading `$`.
452. Convert the key to lowercase.
463. Keep the value as a string.
48Examples:
50- `$DATE` becomes `text["date"]`
51- `$P1N` becomes `text["p1n"]`
52- `$SPILLOVER` becomes `text["spillover"]`
53- `$NEXTDATA` becomes `text["nextdata"]`
55Use:
57```python
58date = flow.text.get("date")
59spill = flow.text.get("spillover", flow.text.get("spill"))
60nextdata = int(flow.text.get("nextdata", "0"))
61```
63Avoid:
65```python
66date = flow.text.get("$DATE") # Always misses in FlowIO's normalized mapping.
67```
69Custom, non-standard keys are also lowercased. The normalized mapping may
70contain subject, sample, operator, institution, instrument serial-number, and
71free-text fields. Treat an unrestricted metadata dump as potentially
72identifying.
74FlowIO 1.4.0 implements the first step by removing every `$` character from the
75decoded segment before splitting keys and values. A literal `$` inside a value
76is therefore lost (`"a$b"` becomes `"ab"`). Do not use the parsed mapping as a
77lossless metadata archive.

TEXT Keyword Normalization

The FCS standard treats keyword names as case-insensitive. Standard keywords are written with $, but FlowIO normalizes parsed keys:

  1. Strip the leading $.
  2. Convert the key to lowercase.
  3. Keep the value as a string.

Examples:

  • $DATE becomes text["date"]
  • $P1N becomes text["p1n"]
  • $SPILLOVER becomes text["spillover"]
  • $NEXTDATA becomes text["nextdata"]

Use:

date = flow.text.get("date")
spill = flow.text.get("spillover", flow.text.get("spill"))
nextdata = int(flow.text.get("nextdata", "0"))

Avoid:

date = flow.text.get("$DATE")  # Always misses in FlowIO's normalized mapping.

Custom, non-standard keys are also lowercased. The normalized mapping may contain subject, sample, operator, institution, instrument serial-number, and free-text fields. Treat an unrestricted metadata dump as potentially identifying.

FlowIO 1.4.0 implements the first step by removing every $ character from the decoded segment before splitting keys and values. A literal $ inside a value is therefore lost ("a$b" becomes "ab"). Do not use the parsed mapping as a lossless metadata archive.

Raw lines 39 to 78
79## Parameters, Labels, and Indices
81FCS parameter numbers begin at 1:
83- P1N, P2N, ... are required primary labels.
84- P1S, P2S, ... are optional descriptive/stain labels.
85- PnR records a parameter range.
86- PnE records logarithmic amplification information.
87- PnG records gain.
89FlowIO presents these through two index systems:
91- `flow.channels[1]`, `flow.channels[2]`, ... use one-based parameter numbers.
92- NumPy columns and all `*_indices` attributes use zero-based indices.
94When reporting a channel, label both values if ambiguity matters:
96```python
97for array_index, pnn in enumerate(flow.pnn_labels):
98 parameter_number = array_index + 1
99 pns = flow.pns_labels[array_index]
100 print(parameter_number, array_index, pnn, pns)
101```
103PnS is optional. FlowIO inserts `""` for missing PnS entries so that
104`pns_labels` and `pnn_labels` have the same length.
106FlowIO infers scatter, fluorescence, and time indices from channel labels.
107Treat these as conveniences, not a substitute for checking the instrument
108panel. Vendor-specific labels may not classify as expected.
110`null_channel_list` accepts PnN labels and excludes matching channels from the
111derived fluorescence/scatter/time index lists. It does not remove columns from
112the event data. `flow.null_channels` stores the supplied labels unchanged; it
113does not contain zero-based indices and can retain labels that did not match a
114channel.

Parameters, Labels, and Indices

FCS parameter numbers begin at 1:

  • P1N, P2N, ... are required primary labels.
  • P1S, P2S, ... are optional descriptive/stain labels.
  • PnR records a parameter range.
  • PnE records logarithmic amplification information.
  • PnG records gain.

FlowIO presents these through two index systems:

  • flow.channels[1], flow.channels[2], ... use one-based parameter numbers.
  • NumPy columns and all *_indices attributes use zero-based indices.

When reporting a channel, label both values if ambiguity matters:

for array_index, pnn in enumerate(flow.pnn_labels):
    parameter_number = array_index + 1
    pns = flow.pns_labels[array_index]
    print(parameter_number, array_index, pnn, pns)

PnS is optional. FlowIO inserts "" for missing PnS entries so that pns_labels and pnn_labels have the same length.

FlowIO infers scatter, fluorescence, and time indices from channel labels. Treat these as conveniences, not a substitute for checking the instrument panel. Vendor-specific labels may not classify as expected.

null_channel_list accepts PnN labels and excludes matching channels from the derived fluorescence/scatter/time index lists. It does not remove columns from the event data. flow.null_channels stores the supplied labels unchanged; it does not contain zero-based indices and can retain labels that did not match a channel.

Raw lines 79 to 115
116## Encoded and Preprocessed Event Values

Encoded and Preprocessed Event Values

Raw lines 116 to 117
118### Encoded representation
120`flow.events` is a decoded flattened one-dimensional sequence, ordered by event and
121then channel. It is usually an `array.array`; mixed-width integer channels use
122a Python `list`:
124```text
125event_1_channel_1, event_1_channel_2, ..., event_2_channel_1, ...
126```
128`flow.as_array(preprocess=False)` reshapes that representation to:
130```text
131(event_count, channel_count)
132```
134For integer DATA, FlowIO applies the PnR range mask during decoding, before
135`as_array()`. Values with high bits outside that range do not retain those bits
136in `flow.events` or `preprocess=False`; preserve the original file for byte fidelity.
137The number of array rows is inferred from DATA and may disagree with `$TOT`.
138Verify shape/counts explicitly.
140The returned NumPy array is `float64`, even when the encoded file uses integer
141or single-precision event data.

Encoded representation

flow.events is a decoded flattened one-dimensional sequence, ordered by event and then channel. It is usually an array.array; mixed-width integer channels use a Python list:

event_1_channel_1, event_1_channel_2, ..., event_2_channel_1, ...

flow.as_array(preprocess=False) reshapes that representation to:

(event_count, channel_count)

For integer DATA, FlowIO applies the PnR range mask during decoding, before as_array(). Values with high bits outside that range do not retain those bits in flow.events or preprocess=False; preserve the original file for byte fidelity. The number of array rows is inferred from DATA and may disagree with $TOT. Verify shape/counts explicitly.

The returned NumPy array is float64, even when the encoded file uses integer or single-precision event data.

Raw lines 118 to 142
143### FlowIO preprocessing
145`flow.as_array(preprocess=True)` performs the following operations.
147#### Time scaling
149When a time channel and nonempty `timestep` keyword exist:
151```text
152time_scaled = time_encoded * timestep
153```
155An empty or whitespace-only `timestep` is treated as `1.0` in FlowIO 1.4.0.
157#### Logarithmically stored channels
159For PnE `(decades, log_zero)` and PnR `range`:
161```text
162linear_value = 10 ** (decades * encoded_value / range) * log_zero
163```
165The conversion is applied when `decades > 0`. FlowIO replaces a zero
166`log_zero` with `1.0` when decades is nonzero.
168#### Gain
170For PnG `gain`:
172```text
173gain_scaled = value / gain
174```
176FlowIO skips division when gain is zero or one. For a recognized non-null
177channel named `Time` (case-insensitive), FlowIO forces the parsed channel
178gain to `1.0`, regardless of TEXT PnG; `timestep` scaling still applies.
180The operations are metadata-driven. Bad metadata can therefore produce bad
181scaled values even when the DATA bytes were parsed correctly.

FlowIO preprocessing

flow.as_array(preprocess=True) performs the following operations.

Time scaling

When a time channel and nonempty timestep keyword exist:

time_scaled = time_encoded * timestep

An empty or whitespace-only timestep is treated as 1.0 in FlowIO 1.4.0.

Logarithmically stored channels

For PnE (decades, log_zero) and PnR range:

linear_value = 10 ** (decades * encoded_value / range) * log_zero

The conversion is applied when decades > 0. FlowIO replaces a zero log_zero with 1.0 when decades is nonzero.

Gain

For PnG gain:

gain_scaled = value / gain

FlowIO skips division when gain is zero or one. For a recognized non-null channel named Time (case-insensitive), FlowIO forces the parsed channel gain to 1.0, regardless of TEXT PnG; timestep scaling still applies.

The operations are metadata-driven. Bad metadata can therefore produce bad scaled values even when the DATA bytes were parsed correctly.

Raw lines 143 to 182
183### What preprocessing does not do
185FlowIO does not:
187- Parse and apply `$SPILL`/`$SPILLOVER` compensation
188- Perform logicle, biexponential, hyperlog, or arcsinh transformation
189- Normalize between files
190- Identify acquisition anomalies
191- Remove debris, doublets, or dead cells
192- Gate populations
194For compensation/transformation/gating, use a higher-level package such as
195FlowKit and preserve a record of the matrix and transform parameters.

What preprocessing does not do

FlowIO does not:

  • Parse and apply $SPILL/$SPILLOVER compensation
  • Perform logicle, biexponential, hyperlog, or arcsinh transformation
  • Normalize between files
  • Identify acquisition anomalies
  • Remove debris, doublets, or dead cells
  • Gate populations

For compensation/transformation/gating, use a higher-level package such as FlowKit and preserve a record of the matrix and transform parameters.

Raw lines 183 to 196
197## Spillover Metadata
199FlowIO can expose or write a spillover keyword as a string. It does not validate
200the matrix scientifically or apply it.
202A standard spillover string begins with:
2041. Number of compensated fluorescence parameters
2052. Matching PnN labels
2063. Flattened matrix values
208Items are comma-delimited with no newline characters.
210Before passing the matrix downstream:
212- Confirm the labels exactly map to the event columns.
213- Confirm the matrix dimensions match the declared parameter count.
214- Confirm whether values are a compensation matrix or spillover matrix as
215 expected by the downstream API.
216- Keep the uncompensated values and original metadata for provenance.

Spillover Metadata

FlowIO can expose or write a spillover keyword as a string. It does not validate the matrix scientifically or apply it.

A standard spillover string begins with:

  1. Number of compensated fluorescence parameters
  2. Matching PnN labels
  3. Flattened matrix values

Items are comma-delimited with no newline characters.

Before passing the matrix downstream:

  • Confirm the labels exactly map to the event columns.
  • Confirm the matrix dimensions match the declared parameter count.
  • Confirm whether values are a compensation matrix or spillover matrix as expected by the downstream API.
  • Keep the uncompensated values and original metadata for provenance.
Raw lines 197 to 217
218## Offset Semantics
220FCS 3.0/3.1 DATA offsets can appear in HEADER and TEXT. FlowIO normally uses
221TEXT offsets and checks that HEADER agrees.
223Some files have known defects:
225- The last DATA byte is reported as exclusive rather than inclusive, creating
226 an off-by-one error.
227- HEADER and TEXT report different offsets.
228- Large FCS 3.1 files put zero in HEADER DATA offsets when a segment extends
229 beyond the eight-digit HEADER limit and store the real offsets in TEXT.
231FlowIO accounts for the FCS 3.1 large-file rule. Do not use relaxed flags merely
232because a file is large.
234Recovery options:
236- `ignore_offset_error=True`: tolerate the documented off-by-one case.
237- `ignore_offset_discrepancy=True`: use TEXT despite a HEADER/TEXT mismatch.
238- `use_header_offsets=True`: use HEADER and suppress the mismatch error.
240Each option changes which bytes are interpreted as events. Use one only when
241the file's provenance or vendor behavior justifies it, and validate event count,
242channel distributions, and known controls afterward.

Offset Semantics

FCS 3.0/3.1 DATA offsets can appear in HEADER and TEXT. FlowIO normally uses TEXT offsets and checks that HEADER agrees.

Some files have known defects:

  • The last DATA byte is reported as exclusive rather than inclusive, creating an off-by-one error.
  • HEADER and TEXT report different offsets.
  • Large FCS 3.1 files put zero in HEADER DATA offsets when a segment extends beyond the eight-digit HEADER limit and store the real offsets in TEXT.

FlowIO accounts for the FCS 3.1 large-file rule. Do not use relaxed flags merely because a file is large.

Recovery options:

  • ignore_offset_error=True: tolerate the documented off-by-one case.
  • ignore_offset_discrepancy=True: use TEXT despite a HEADER/TEXT mismatch.
  • use_header_offsets=True: use HEADER and suppress the mismatch error.

Each option changes which bytes are interpreted as events. Use one only when the file's provenance or vendor behavior justifies it, and validate event count, channel distributions, and known controls afterward.

Raw lines 218 to 243
244## Memory Model
246Normal `FlowData` construction reads the full DATA segment into memory.
247`as_array()` then allocates a second `float64` representation.
249Approximate additional memory for the 2-D array is:
251```text
252event_count * channel_count * 8 bytes
253```
255The estimate assumes `$TOT` agrees with DATA; check the actual loaded length
256before allocating the array. This excludes the original event array, Python
257objects, temporary arrays, and downstream DataFrames.
259Use `only_text=True` for inventory. FlowIO 1.4.0 does not expose chunked,
260streaming, lazy, or memory-mapped event reads. If a file does not fit safely in
261memory, use a different parser/workflow or process it in a resource-limited
262environment; do not claim FlowIO can chunk it.
264`only_text=True` skips DATA loading but still parses ANALYSIS. Also prefer paths
265over open handles: `FlowData` closes caller-provided handles, and the
266multi-dataset helper cannot reuse a closed handle for its second dataset in
267FlowIO 1.4.0.

Memory Model

Normal FlowData construction reads the full DATA segment into memory. as_array() then allocates a second float64 representation.

Approximate additional memory for the 2-D array is:

event_count * channel_count * 8 bytes

The estimate assumes $TOT agrees with DATA; check the actual loaded length before allocating the array. This excludes the original event array, Python objects, temporary arrays, and downstream DataFrames.

Use only_text=True for inventory. FlowIO 1.4.0 does not expose chunked, streaming, lazy, or memory-mapped event reads. If a file does not fit safely in memory, use a different parser/workflow or process it in a resource-limited environment; do not claim FlowIO can chunk it.

only_text=True skips DATA loading but still parses ANALYSIS. Also prefer paths over open handles: FlowData closes caller-provided handles, and the multi-dataset helper cannot reuse a closed handle for its second dataset in FlowIO 1.4.0.

Raw lines 244 to 268
269## Writer Representation
271`create_fcs()` and `write_fcs()` produce:
273- FCS 3.1
274- List mode
275- Single-precision 32-bit float event values
276- Little-endian byte order
277- No ANALYSIS segment
278- A single dataset
280The float32 representation has roughly 6-7 decimal digits of precision.
281Round-trip comparisons should use tolerances rather than exact equality.
283Passing a NumPy array makes `create_fcs()` allocate an `array('f')` copy. An
284existing `array('f')` is written directly, so retain that representation when
285large writer memory is a concern.
287`create_fcs()` needs flattened event data. Flatten a two-dimensional array in
288C order:
290```python
291flat = events_2d.astype("float32", copy=False).ravel(order="C")
292```
294Validate:
296```python
297if events_2d.ndim != 2:
298 raise ValueError("expected events x channels")
299if events_2d.shape[1] != len(channel_names):
300 raise ValueError("channel label count mismatch")
301```
303Required keywords and channel interpretation fields are generated by FlowIO.
304Do not rely on `metadata_dict` to override `$PAR`, `$TOT`, `$MODE`,
305`$DATATYPE`, PnB, or PnN.
307For floating-point sources, `write_fcs()` can keep encoded values while
308dropping PnG and `timestep`. This changes the interpretation returned by
309`as_array(preprocess=True)`. Validate both encoded and metadata-scaled values,
310not just event counts.

Writer Representation

create_fcs() and write_fcs() produce:

  • FCS 3.1
  • List mode
  • Single-precision 32-bit float event values
  • Little-endian byte order
  • No ANALYSIS segment
  • A single dataset

The float32 representation has roughly 6-7 decimal digits of precision. Round-trip comparisons should use tolerances rather than exact equality.

Passing a NumPy array makes create_fcs() allocate an array('f') copy. An existing array('f') is written directly, so retain that representation when large writer memory is a concern.

create_fcs() needs flattened event data. Flatten a two-dimensional array in C order:

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

Validate:

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

Required keywords and channel interpretation fields are generated by FlowIO. Do not rely on metadata_dict to override $PAR, $TOT, $MODE, $DATATYPE, PnB, or PnN.

For floating-point sources, write_fcs() can keep encoded values while dropping PnG and timestep. This changes the interpretation returned by as_array(preprocess=True). Validate both encoded and metadata-scaled values, not just event counts.

Raw lines 269 to 311
312## Scientific Provenance
314For any converted or rewritten file, record:
316- Source file name and checksum
317- FlowIO version
318- FCS version and source `$DATATYPE`
319- Whether `preprocess` was true or false
320- Any relaxed offset option and its justification
321- Channel order and label mapping
322- Metadata removed, added, or renamed
323- Whether compensation or another transform occurred elsewhere
324- Output representation and float32 precision
325- Round-trip validation results

Scientific Provenance

For any converted or rewritten file, record:

  • Source file name and checksum
  • FlowIO version
  • FCS version and source $DATATYPE
  • Whether preprocess was true or false
  • Any relaxed offset option and its justification
  • Channel order and label mapping
  • Metadata removed, added, or renamed
  • Whether compensation or another transform occurred elsewhere
  • Output representation and float32 precision
  • Round-trip validation results
Raw lines 312 to 325