Source: https://github.com/aipoch/medical-research-skills
When to Use
- You need to read FCS v2.0/3.0/3.1 files and extract event matrices for downstream preprocessing.
- You want to inspect or validate FCS metadata (TEXT segment) without loading event data (memory-efficient parsing).
- You need channel definitions (PnN/PnS), ranges (PnR), and automatic identification of scatter/fluorescence/time channels.
- You need to handle problematic FCS files with offset inconsistencies or multi-dataset content.
- You want to export cytometry events to CSV/Pandas DataFrame or write new/modified FCS files.
Key Features
- FCS parsing (v2.0–3.1): Reads HEADER/TEXT/DATA and optional ANALYSIS segments.
- Event extraction to NumPy: Returns event data as
ndarray with shape (events, channels).
- Optional preprocessing: Applies standard FCS transformations (gain/log/time scaling) when enabled.
- Metadata access: Exposes TEXT keywords and common instrument/acquisition fields.
- Channel utilities: Provides PnN/PnS labels, ranges, and indices for scatter/fluorescence/time channels.
- Robust parsing options: Flags for offset discrepancy handling and null-channel exclusion.
- Multi-dataset support: Detects and reads files containing multiple datasets.
- FCS writing: Create new FCS files from arrays and optionally preserve/override metadata.
Dependencies
python >= 3.9
flowio (install via pip/uv; version depends on your environment)
- Example-only:
numpy >= 1.20
pandas >= 1.5
Example Usage
"""
End-to-end example:
1) Read an FCS file (metadata + events)
2) Convert to a Pandas DataFrame and export CSV
3) Filter events and write a new FCS file
4) Handle multi-dataset files
"""
from pathlib import Path
import numpy as np
import pandas as pd
from flowio import (
FlowData,
create_fcs,
read_multiple_data_sets,
MultipleDataSetsError,
FCSParsingError,
DataOffsetDiscrepancyError,
)
FCS_PATH = "sample.fcs"
def read_fcs_safely(path: str) -> FlowData:
try:
return FlowData(path)
except DataOffsetDiscrepancyError:
# Common workaround for files with inconsistent offsets
return FlowData(path, ignore_offset_discrepancy=True)
except FCSParsingError:
# Looser mode if the file is malformed
return FlowData(path, ignore_offset_error=True)
def main() -> None:
# --- 1) Read file (single dataset) ---
try:
flow = read_fcs_safely(FCS_PATH)
except MultipleDataSetsError:
# --- 4) Multi-dataset handling ---
datasets = read_multiple_data_sets(FCS_PATH)
flow = datasets[0] # pick the first dataset for this demo
print("File:", getattr(flow, "name", Path(FCS_PATH).name))
print("FCS version:", flow.version)
print("Events:", flow.event_count)
print("Channels:", flow.channel_count)
print("PnN labels:", flow.pnn_labels)
# Metadata (TEXT segment)
print("Instrument ($CYT):", flow.text.get("$CYT", "N/A"))
print("Acquisition date ($DATE):", flow.text.get("$DATE", "N/A"))
# --- 2) Events -> NumPy -> DataFrame -> CSV ---
events = flow.as_array(preprocess=True) # default preprocessing behavior
df = pd.DataFrame(events, columns=flow.pnn_labels)
df.to_csv("events.csv", index=False)
print("Wrote CSV:", "events.csv")
# --- 3) Filter and write a new FCS ---
# Example: threshold on first scatter channel if available, else channel 0
fsc_idx = flow.scatter_indices[0] if getattr(flow, "scatter_indices", []) else 0
threshold = np.percentile(events[:, fsc_idx], 50) # median threshold
mask = events[:, fsc_idx] > threshold
filtered = events[mask]
create_fcs(
"filtered.fcs",
filtered,
flow.pnn_labels,
opt_channel_names=flow.pns_labels,
metadata={**flow.text, "$SRC": "Filtered via FlowIO example"},
)
print("Wrote FCS:", "filtered.fcs")
# --- Metadata-only read (memory efficient) ---
meta_only = FlowData(FCS_PATH,
print("Metadata-only read: $DATE =", meta_only.text.get("$DATE", "N/A"))
if __name__ == "__main__":
main()
Implementation Details
Data Model and Segments
An FCS file is organized into segments:
- HEADER: FCS version and byte offsets for other segments.
- TEXT: Keyword/value metadata (e.g.,
$DATE, $CYT, $PnN, $PnS, $PnR, $PnG, $PnE).
- DATA: Event matrix encoded as integer/float/double/ASCII depending on file keywords.
- ANALYSIS (optional): Post-processing results if present.
In FlowIO, these are exposed via FlowData attributes such as:
flow.header (HEADER info)
flow.text (TEXT keyword dictionary)
flow.analysis (ANALYSIS keyword dictionary, if present)
flow.as_array(...) (decoded event matrix)
Preprocessing (as_array(preprocess=True))
When preprocessing is enabled, FlowIO applies common FCS transformations:
- Gain scaling (PnG): Values are multiplied by the per-parameter gain.
- Log/exponential transform (PnE): If present, applies:
value = a * 10^(b * raw_value) where PnE = "a,b".
- Time scaling: If a time channel is detected, values may be scaled into appropriate units.
To disable all transformations and obtain raw decoded values:
flow.as_array(preprocess=False)
Channel Identification
FlowIO provides convenience indices for common channel types:
flow.scatter_indices (e.g., FSC/SSC)
flow.fluoro_indices (fluorescence channels)
flow.time_index (time channel index or None)
These indices can be used to slice the event matrix:
events[:, flow.scatter_indices]
events[:, flow.fluoro_indices]
Handling Problematic Files (Offsets and Null Channels)
Some files contain inconsistent offsets between HEADER and TEXT:
ignore_offset_discrepancy=True to tolerate HEADER/TEXT offset mismatch.
use_header_offsets=True to prefer HEADER offsets.
ignore_offset_error=True to bypass offset-related failures more aggressively.
To exclude known null/empty channels during parsing:
FlowData(path, null_channel_list=[...])
Multi-Dataset Files
If a file contains multiple datasets, constructing FlowData(path) may raise MultipleDataSetsError. Use:
read_multiple_data_sets(path) to load all datasets, or
FlowData(path, nextdata_offset=...) to load a specific dataset using $NEXTDATA offsets.
Writing FCS
Two common patterns:
- Write metadata-only changes:
flow.write_fcs("out.fcs", metadata={...})
- Modify event data: extract array → modify →
create_fcs(...) to generate a new file (FlowIO does not modify event data in-place).
1---2name: flowio3description: Parse Flow Cytometry Standard (FCS) files v2.0–3.1 and extract events/metadata for preprocessing workflows (e.g., when you need NumPy arrays, channel info, or CSV/DataFrame export from cytometry files).4license: MIT5---6> **Source**: [https://github.com/aipoch/medical-research-skills](https://github.com/aipoch/medical-research-skills)78## When to Use910- You need to read FCS v2.0/3.0/3.1 files and extract event matrices for downstream preprocessing.11- You want to inspect or validate FCS metadata (TEXT segment) without loading event data (memory-efficient parsing).12- You need channel definitions (PnN/PnS), ranges (PnR), and automatic identification of scatter/fluorescence/time channels.13- You need to handle problematic FCS files with offset inconsistencies or multi-dataset content.14- You want to export cytometry events to CSV/Pandas DataFrame or write new/modified FCS files.1516## Key Features1718- **FCS parsing (v2.0–3.1):** Reads HEADER/TEXT/DATA and optional ANALYSIS segments.19- **Event extraction to NumPy:** Returns event data as `ndarray` with shape `(events, channels)`.20- **Optional preprocessing:** Applies standard FCS transformations (gain/log/time scaling) when enabled.21- **Metadata access:** Exposes TEXT keywords and common instrument/acquisition fields.22- **Channel utilities:** Provides PnN/PnS labels, ranges, and indices for scatter/fluorescence/time channels.23- **Robust parsing options:** Flags for offset discrepancy handling and null-channel exclusion.24- **Multi-dataset support:** Detects and reads files containing multiple datasets.25- **FCS writing:** Create new FCS files from arrays and optionally preserve/override metadata.2627## Dependencies2829- `python >= 3.9`30- `flowio` (install via pip/uv; version depends on your environment)31- Example-only:32 - `numpy >= 1.20`33 - `pandas >= 1.5`3435## Example Usage3637```python38"""39End-to-end example:401) Read an FCS file (metadata + events)412) Convert to a Pandas DataFrame and export CSV423) Filter events and write a new FCS file434) Handle multi-dataset files44"""4546from pathlib import Path4748import numpy as np49import pandas as pd5051from flowio import (52 FlowData,53 create_fcs,54 read_multiple_data_sets,55 MultipleDataSetsError,56 FCSParsingError,57 DataOffsetDiscrepancyError,58)5960FCS_PATH = "sample.fcs"6162def read_fcs_safely(path: str) -> FlowData:63 try:64 return FlowData(path)65 except DataOffsetDiscrepancyError:66 # Common workaround for files with inconsistent offsets67 return FlowData(path, ignore_offset_discrepancy=True)68 except FCSParsingError:69 # Looser mode if the file is malformed70 return FlowData(path, ignore_offset_error=True)7172def main() -> None:73 # --- 1) Read file (single dataset) ---74 try:75 flow = read_fcs_safely(FCS_PATH)76 except MultipleDataSetsError:77 # --- 4) Multi-dataset handling ---78 datasets = read_multiple_data_sets(FCS_PATH)79 flow = datasets[0] # pick the first dataset for this demo8081 print("File:", getattr(flow, "name", Path(FCS_PATH).name))82 print("FCS version:", flow.version)83 print("Events:", flow.event_count)84 print("Channels:", flow.channel_count)85 print("PnN labels:", flow.pnn_labels)8687 # Metadata (TEXT segment)88 print("Instrument ($CYT):", flow.text.get("$CYT", "N/A"))89 print("Acquisition date ($DATE):", flow.text.get("$DATE", "N/A"))9091 # --- 2) Events -> NumPy -> DataFrame -> CSV ---92 events = flow.as_array(preprocess=True) # default preprocessing behavior93 df = pd.DataFrame(events, columns=flow.pnn_labels)94 df.to_csv("events.csv", index=False)95 print("Wrote CSV:", "events.csv")9697 # --- 3) Filter and write a new FCS ---98 # Example: threshold on first scatter channel if available, else channel 099 fsc_idx = flow.scatter_indices[0] if getattr(flow, "scatter_indices", []) else 0100 threshold = np.percentile(events[:, fsc_idx], 50) # median threshold101 mask = events[:, fsc_idx] > threshold102 filtered = events[mask]103104 create_fcs(105 "filtered.fcs",106 filtered,107 flow.pnn_labels,108 opt_channel_names=flow.pns_labels,109 metadata={**flow.text, "$SRC": "Filtered via FlowIO example"},110 )111 print("Wrote FCS:", "filtered.fcs")112113 # --- Metadata-only read (memory efficient) ---114 meta_only = FlowData(FCS_PATH, only_text=True)115 print("Metadata-only read: $DATE =", meta_only.text.get("$DATE", "N/A"))116117if __name__ == "__main__":118 main()119```120121## Implementation Details122123### Data Model and Segments124125An FCS file is organized into segments:126127- **HEADER:** FCS version and byte offsets for other segments.128- **TEXT:** Keyword/value metadata (e.g., `$DATE`, `$CYT`, `$PnN`, `$PnS`, `$PnR`, `$PnG`, `$PnE`).129- **DATA:** Event matrix encoded as integer/float/double/ASCII depending on file keywords.130- **ANALYSIS (optional):** Post-processing results if present.131132In FlowIO, these are exposed via `FlowData` attributes such as:133- `flow.header` (HEADER info)134- `flow.text` (TEXT keyword dictionary)135- `flow.analysis` (ANALYSIS keyword dictionary, if present)136- `flow.as_array(...)` (decoded event matrix)137138### Preprocessing (`as_array(preprocess=True)`)139140When preprocessing is enabled, FlowIO applies common FCS transformations:1411421. **Gain scaling (PnG):** Values are multiplied by the per-parameter gain.1432. **Log/exponential transform (PnE):** If present, applies:144 - `value = a * 10^(b * raw_value)` where `PnE = "a,b"`.1453. **Time scaling:** If a time channel is detected, values may be scaled into appropriate units.146147To disable all transformations and obtain raw decoded values:148- `flow.as_array(preprocess=False)`149150### Channel Identification151152FlowIO provides convenience indices for common channel types:153154- `flow.scatter_indices` (e.g., FSC/SSC)155- `flow.fluoro_indices` (fluorescence channels)156- `flow.time_index` (time channel index or `None`)157158These indices can be used to slice the event matrix:159- `events[:, flow.scatter_indices]`160- `events[:, flow.fluoro_indices]`161162### Handling Problematic Files (Offsets and Null Channels)163164Some files contain inconsistent offsets between HEADER and TEXT:165166- `ignore_offset_discrepancy=True` to tolerate HEADER/TEXT offset mismatch.167- `use_header_offsets=True` to prefer HEADER offsets.168- `ignore_offset_error=True` to bypass offset-related failures more aggressively.169170To exclude known null/empty channels during parsing:171- `FlowData(path, null_channel_list=[...])`172173### Multi-Dataset Files174175If a file contains multiple datasets, constructing `FlowData(path)` may raise `MultipleDataSetsError`. Use:176177- `read_multiple_data_sets(path)` to load all datasets, or178- `FlowData(path, nextdata_offset=...)` to load a specific dataset using `$NEXTDATA` offsets.179180### Writing FCS181182Two common patterns:183184- **Write metadata-only changes:** `flow.write_fcs("out.fcs", metadata={...})`185- **Modify event data:** extract array → modify → `create_fcs(...)` to generate a new file (FlowIO does not modify event data in-place).