# zeztz-flow — Flow Cytometry FCS Analysis for AI Agents

## Identity
- Package: `zeztz-flow` | Import: `import zeztzflow as flow` | Python: `>=3.9`
- DO NOT write custom FCS parsers or gating logic. Use the functions below.

## Complete Function Reference

### Loading
| Signature | Notes |
|-----------|-------|
| `flow.zeztz_exp(working_dir: str, merge_dirs=[])` | Loads `.fcs` files, assigns `char`, `num`, `well`, `sample` metadata |
| `expr.filelist` → list of FCS file paths | |
| `expr.expr` → `Experiment` instance | Event data: `expr.expr.data` (pandas DataFrame) |
| `expr.expr.channels` → list of channel names | |
| `expr.name_locus_mapping` → `{1: ['A',0], 2: ['B',0], ...}` | Well position mapping |

### Gating (MUST run in this order)
| Signature | Notes |
|-----------|-------|
| `flow.gate_saturation(experiment)` → Experiment | Always run FIRST. Removes events at detector max. |
| `flow.auto_gate_FSC_SSC(experiment, keep=0.6, return_fig=False)` → Experiment or (Experiment, Figure) | Density gate on FSC-A vs SSC-A |
| `flow.auto_gate_FSC_A_H(experiment, keep=0.9, return_fig=False)` → Experiment or (Experiment, Figure) | Density gate on FSC-A vs FSC-H (singlets) |
| `flow.assist_gate(experiment, channel, low, high)` → Experiment | Manual range gate |
| `flow.polygon_gate(experiment, xchannel, ychannel, vertices=...)` → Experiment | `vertices`: ≥3 `[x, y]` pairs |
| `flow.interactive_gate_preview(experiment, xchannel, ychannel, xscale=None, yscale=None)` → Figure | Display with `mo.mpl.interactive(fig)` |
| `flow.apply_drawn_gate(experiment, name="marimo_gate")` → Experiment | Applies polygon from preview |

### Plotting
| Signature | Notes |
|-----------|-------|
| `flow.density_plot(experiment, xchannel, ychannel, xscale=None, yscale=None, cmap=None, colorbar=False)` → `(fig, ax)` | 2D density (scatter_density projection) |
| `flow.plot_histogram(experiment, channel, huefacet="sample", show_kde=True, kde_threshold=None)` → `fig` | Faceted histogram with KDE |
| `flow.plot_ridgeline(experiment, channel, huefacet="sample", show_kde=True, kde_threshold=None)` → `fig` | Ridgeline with KDE overlay |
| `flow.channel_info(experiment, channel)` → `(scale, lo, hi)` | linear for FSC/SSC, log for fluorescence |

### Subsetting & Utility
| Signature | Notes |
|-----------|-------|
| `flow.subset_by_char(experiment, row)` → Experiment | e.g. `"A"` |
| `flow.subset_by_num(experiment, num)` → Experiment | Pass as STRING: `"12"`, not `12` |
| `flow.subset_by_well(experiment, well)` → Experiment | `"A1"` or `["A1","A2"]` |
| `flow.cell_count(experiment)` → int | Event count |
| `flow._find_channel(experiment, *patterns)` → str or None | e.g. `_find_channel(exp, "FSC", "-A")` |
| `expr.median_96well(experiment, channel, interval="\t")` → 8×12 list | Writes `median_values.txt` |
| `flow.flow_style.apply()` | Apply default plot style |

## GOTCHAS — Read These
- **Gating order is MANDATORY**: `gate_saturation()` → `auto_gate_FSC_SSC()` → `auto_gate_FSC_A_H()`
- `subset_by_num` takes STRINGS: `"12"`, NOT integer `12`
- `return_fig=True` changes return type to `(experiment, figure)` — unpack if used
- Available scales: only `"linear"` and `"log"` (no logicle)
- Channel names vary by instrument. Use `_find_channel()` or `experiment.channels` to discover them. Common names from Xiaoyu's instrument: `FSC 488/10-A`, `SSC 488/10-A`, `FSC 488/10-H`, `Alexa 647-A`, `mCherry-A`, `TagBFP-A`, `549/15-488 nm citrine-A`
- `zeztz_exp` strips everything after the first space in filenames for well detection ("A1 EGFR.fcs" → well "A1")
- Non-data channels to skip: `TIME`, `TLSW`, `TMSW`, `Event Info`
- Headless scripts: put `matplotlib.use("Agg")` before `import matplotlib.pyplot`

## Standard Pipeline (copy-paste ready)

```python
import zeztzflow as flow
import marimo as mo

# 1. Load
expr = flow.zeztz_exp("path/to/fcs_data")

# 2. Cleanup gating (order matters!)
clean  = flow.gate_saturation(expr.expr)
cells  = flow.auto_gate_FSC_SSC(clean, keep=0.6)
single = flow.auto_gate_FSC_A_H(cells, keep=0.7)

# 3. Find channels dynamically
fluor_channels = [ch for ch in single.channels
                  if ch not in ("TIME", "TLSW", "TMSW", "Event Info")]
ch = fluor_channels[0]  # or let user pick

# 4. Plot
flow.plot_histogram(single, channel=ch, huefacet="char")

# 5. Export medians
expr.median_96well(single, channel=ch)
```

## Marimo-Specific Rules
- Display figures: `mo.mpl.interactive(fig)` (NOT `plt.show()`)
- Import style: `from zeztzflow.flow_style import apply as _apply_style; _apply_style()`
- Cell-local vars: prefix with `_` (e.g. `_data_dir`, `_fig`)
- For manual gating: use `interactive_gate_preview()` + `apply_drawn_gate()` (NOT `polygon_gate(vertices=[])`)
- Cell naming convention: prefix with `_` to keep them private
- Template notebook: `examples/marimo_template.py` in the repo (run `python -m zeztzflow init <data_dir>` to generate one)

## Anti-Patterns — NEVER Do These
- NEVER parse `.fcs` files manually (use `zeztz_exp`)
- NEVER write custom gating/density logic (use the gating functions)
- NEVER use `plt.show()` in marimo (use `mo.mpl.interactive(fig)`)
- NEVER skip saturation gating
- NEVER pass integers to `subset_by_num` — always strings
- NEVER hard-code channel names (use `_find_channel` or let users pick)
