Skip to content

Commit f921fac

Browse files
authored
update docs (#50)
1 parent 777cbc6 commit f921fac

6 files changed

Lines changed: 122 additions & 86 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
2. Generates a structured description for every label (or part) with an LLM.
77
3. Embeds those descriptions with a configured embedding model.
88
4. Matches each embedding to the closest CL term.
9-
5. Scores each author/algorithm pair using an ontology-aware similarity metric (default: a Gaussian kernel on the cosine similarity of the CL term embeddings). Compound pairs use Hungarian bipartite matching with an optional coverage penalty when part counts differ.
9+
5. Scores each author/algorithm pair using an ontology-aware similarity metric (default: a Gaussian kernel on the cosine similarity of the CL term embeddings). Compound pairs default to the maximum entry in the part-by-part score matrix (`compound_scoring="max"`); set `compound_scoring="hungarian_mean"` for Hungarian assignment mean with a coverage penalty when part counts differ.
1010
6. Returns a tidy DataFrame with one row per `(algorithm, pair_index)`.
1111

1212
Updated ReadMe: [cyteonto/README.md](cyteonto/README.md). Process flow and file layout: [docs/WORKFLOW.md](docs/WORKFLOW.md), [docs/FILE_MANAGEMENT.md](docs/FILE_MANAGEMENT.md).

cyteonto/README.md

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Given two parallel lists of cell type labels (one from the study author, one fro
88
2. Generates a structured description for each label or part using an LLM.
99
3. Embeds those descriptions with a configured embedding model.
1010
4. Matches each embedding to the closest CL term via cosine similarity.
11-
5. Scores each author/algorithm pair using an ontology-aware similarity metric (default: kernelised cosine on the CL term embeddings). Compound pairs use Hungarian match mean with a coverage penalty when part counts differ.
11+
5. Scores each author/algorithm pair using an ontology-aware similarity metric (default: kernelised cosine on the CL term embeddings). Compound pairs default to the maximum entry in the part-by-part score matrix (`compound_scoring="max"`); set `compound_scoring="hungarian_mean"` for Hungarian assignment mean with a coverage penalty when part counts differ.
1212
6. Returns a tidy `pandas.DataFrame` with per-pair scores.
1313

1414
All LLM descriptions and embeddings are persisted on disk and reused across runs.
@@ -98,6 +98,7 @@ df = await cyto.compare(
9898
},
9999
run_id="sample_run_001", # optional; auto-generated UUID if omitted
100100
metric="cosine_kernel",
101+
compound_scoring="max", # default; use "hungarian_mean" for assignment mean
101102
)
102103
print(df)
103104
print("run_id used:", df["run_id"].iloc[0])
@@ -122,15 +123,15 @@ One row per `(algorithm, pair_index)`:
122123
| `run_id` | str | The `run_id` passed to `compare`. |
123124
| `algorithm` | str | Algorithm key from the `algorithms` mapping. |
124125
| `pair_index` | int | Position inside the label list, starting at 0. |
125-
| `author_label` | str | The author label for this pair. |
126-
| `algorithm_label` | str | The algorithm label for this pair. |
127-
| `author_ontology_id` | str | Best CL match for the author label. For compound pairs, semicolon-separated ids from the Hungarian assignment only. Empty string if unmatched. |
126+
| `author_label` | str | The author label for this pair (lowercased at compare entry). |
127+
| `algorithm_label` | str | The algorithm label for this pair (lowercased at compare entry). |
128+
| `author_ontology_id` | str | Best CL match per author part, semicolon-separated in part order. Empty string if unmatched. |
128129
| `author_ontology_name` | str | Primary CSV label (or OWL fallback) for each id in `author_ontology_id`. |
129-
| `author_embedding_similarity` | float | Mean cosine similarity between author part embeddings and their CL matches. |
130-
| `algorithm_ontology_id` | str | Best CL match for the algorithm label. Same compound rules as author. |
130+
| `author_embedding_similarity` | float \| str | Cosine similarity of each author part to its CL match. Single float when one part; semicolon-separated floats when multiple parts. |
131+
| `algorithm_ontology_id` | str | Best CL match per algorithm part, same rules as author. |
131132
| `algorithm_ontology_name` | str | Names for ids in `algorithm_ontology_id`. |
132-
| `algorithm_embedding_similarity` | float | Mean cosine similarity between algorithm part embeddings and their CL matches. |
133-
| `cytescore_similarity` | float | Score under the chosen `metric`; `0.0` if either side is unmatched. |
133+
| `algorithm_embedding_similarity` | float \| str | Same shape as author embedding similarity, for algorithm parts. |
134+
| `cytescore_similarity` | float | Score under the chosen `metric` / compound reducer; `0.0` if either side is unmatched. |
134135
| `similarity_method` | str | `cytescore`, `cytescore_compound`, `string_similarity`, `partial_match`, `no_matches`, or `empty`. |
135136

136137
---
@@ -145,7 +146,7 @@ Read once on import, via `python-dotenv`:
145146
|-----------------------------|-----------------------------------------------------------|---------|
146147
| `EMBEDDING_MODEL_API_KEY` | Fallback `apiKey` when `EmbdConfig.apiKey` is `None`. | `""` |
147148
| `NCBI_API_KEY` | Optional. Raises PubMed rate limits for the agent tool. | `""` |
148-
| `CYTEONTO_LOG_LEVEL` | Loguru log level (`DEBUG`, `INFO`, `WARNING`, ...). | `INFO` |
149+
| `CYTEONTO_LOG_LEVEL` | Loguru log level (`DEBUG`, `INFO`, `WARNING`, ...). | `DEBUG` |
149150
| `CYTEONTO_LOG_FILE` | Optional log file path with 10 MB rotation. | unset |
150151

151152
The LLM agent keys follow whatever the caller passes to `pydantic_ai`; this package does not manage them.
@@ -238,7 +239,7 @@ cyto = await CyteOnto.from_config(
238239

239240
Additional behavior on top of `__init__`:
240241

241-
1. Checks that `cell_ontology/cell_to_cell_ontology.csv` and `cell_ontology/cl.owl` exist.
242+
1. Checks that `cell_ontology/cell_to_cell_ontology.csv` (and preferably the enriched CSV) and `cell_ontology/cl.owl` exist. Lookups prefer `cell_to_cell_ontology_enriched.csv` when present.
242243
2. Computes ontology paths from `llm.to_artifact_key()` and `embedding.to_artifact_key()`.
243244
3. If `force_regenerate=True`, unlinks both the ontology embeddings NPZ and the descriptions JSON before proceeding.
244245
4. Loads the descriptions JSON (if present) and drops any blank entries so they can be retried.
@@ -259,6 +260,7 @@ df = await cyto.compare(
259260
metric_params: dict[str, Any] | None = None,
260261
min_match_similarity: float = 0.1,
261262
use_cache: bool = True,
263+
compound_scoring: Literal["max", "hungarian_mean"] = "max",
262264
)
263265
```
264266

@@ -268,33 +270,35 @@ Constraints:
268270

269271
- Every value in `algorithms` must be the same length as `author_labels`. Mismatched lengths raise `ValueError`.
270272
- Algorithm names must be unique. `"author"` is reserved and rejected.
273+
- `compound_scoring` must be `"max"` or `"hungarian_mean"`; any other value raises `ValueError`.
274+
- Non-empty author and algorithm labels (and decomposed parts) are lowercased before decompose/describe/embed so casing does not split caches.
271275
- `run_id` is used as a cache namespace on disk. Reusing the same id reuses cached author and algorithm embeddings when the labels match. If you pass `None`, a UUID of the form `run-<uuid4>` is generated and logged; the same value is written into every result row so you can recover it later.
272276

273277
Call flow:
274278

275-
1. `_resolve_label_parts` decomposes unique labels via LLM (cached per `run_id` under `decompositions/`).
276-
2. `_embed_user_labels` on the union of sanitized parts per side (author, then each algorithm).
277-
3. `_match` returns the best CL id and similarity for each part.
278-
4. `_ensure_similarity()` lazy-loads the OWL file and the ontology embeddings into `OntologySimilarity`.
279-
5. For each aligned pair index:
279+
1. Lowercase non-empty labels on both sides.
280+
2. `_resolve_label_parts` decomposes unique labels via LLM (cached per `run_id` under `decompositions/`). Compound parts are lowercased.
281+
3. `_embed_user_labels` on the union of sanitized parts per side (author, then each algorithm).
282+
4. `_match` returns the best CL id and similarity for each part.
283+
5. `_ensure_similarity()` lazy-loads the OWL file and the ontology embeddings into `OntologySimilarity`.
284+
6. For each aligned pair index:
280285
- **Simple pair** (one part on each side): single `OntologySimilarity.similarity` call; `similarity_method` from `_method_for`.
281-
- **Compound pair** (more than one part on either side): build an m×n score matrix, run Hungarian max-weight matching, average assigned scores; multiply by `min(m,n)/max(m,n)` when `m ≠ n`; `similarity_method = cytescore_compound`.
282-
6. Rows are concatenated into a DataFrame with columns from `RESULT_COLUMNS`.
286+
- **Compound pair** (more than one part on either side): build an m×n score matrix `S`, then reduce with `compound_scoring` (see below); `similarity_method = cytescore_compound`.
287+
7. Rows are concatenated into a DataFrame with columns from `RESULT_COLUMNS`. Ontology ids/names and embedding similarities list **all** parts in order (not only Hungarian-matched pairs).
283288

284289
### Compound label scoring
285290

286291
Mixture labels such as doublets or mixed populations are poor matches when embedded as a single string. `compare` therefore:
287292

288-
1. Calls `decompose_labels` to split a label into one or more cell-type parts (semicolon-separated synonyms stay as one part).
293+
1. Calls `decompose_labels` to split a label into one or more cell-type parts (semicolon-separated synonyms stay as one part; compound part names are lowercased).
289294
2. Embeds and matches each unique part.
290-
3. Scores compound pairs with **Hungarian match mean**:
291-
- Build matrix `S` where `S[i,j]` is the cytescore between author part `i` and algorithm part `j`.
292-
- Pick `k = min(m,n)` one-to-one assignments that maximize total score.
293-
- `match_mean` = mean of assigned cell scores.
294-
- If `m ≠ n`, multiply by coverage `min(m,n) / max(m,n)`.
295-
4. Writes `similarity_method = cytescore_compound`. Ontology ids and names list only the matched assignment pairs (semicolon-separated when `k > 1`).
295+
3. Builds matrix `S` where `S[i,j]` is the cytescore between author part `i` and algorithm part `j`.
296+
4. Reduces `S` with `compound_scoring`:
297+
- **`"max"` (default):** `cytescore_similarity = max(S)`.
298+
- **`"hungarian_mean"`:** pick `k = min(m,n)` one-to-one assignments that maximize total score; take the mean of those `k` scores; if `m ≠ n`, multiply by coverage `min(m,n) / max(m,n)`.
299+
5. Writes `similarity_method = cytescore_compound`. Result ontology ids/names and per-part embedding similarities include every part on that side.
296300

297-
Illustrative scores (see `notebooks/quick_tutorial.ipynb`):
301+
Illustrative scores with `compound_scoring="hungarian_mean"` (see `notebooks/quick_tutorial.ipynb`):
298302

299303
| Scenario | m×n | Typical score |
300304
|----------|-----|---------------|
@@ -304,6 +308,8 @@ Illustrative scores (see `notebooks/quick_tutorial.ipynb`):
304308
| Partial overlap, extra author type | 3×2 | ~0.35 |
305309
| Author doublet vs single type | 2×1 | best match × 0.5 |
306310

311+
With the default `"max"`, the same matrices report the highest single cell of `S` instead (for example 2×1 with scores `[0.6, 0.2]` yields `0.6`).
312+
307313
### `compare_anndata` (async)
308314

309315
Pulls label lists out of `adata.obs[target_columns]` for each AnnData object and delegates to `compare`. Skips any object missing `author_column` and warns for missing target columns.
@@ -353,7 +359,7 @@ CyteOnto.compare
353359
├─ _match(algo_part_emb)
354360
└─ for each pair_index:
355361
├─ simple: OntologySimilarity.similarity(a_id, g_id, ...)
356-
└─ compound: build S, _hungarian_match_mean(S) → cytescore_compound
362+
└─ compound: build S; max(S) or _hungarian_match_mean(S) → cytescore_compound
357363
```
358364

359365
---
@@ -418,7 +424,7 @@ Usage limits default to `request_limit=50, input_tokens_limit=60_000`. Override
418424

419425
### Compound label decomposition
420426

421-
`describe.decompose_label` and `describe.decompose_labels` call a dedicated LLM agent with `output_type=LabelDecomposition`. The model decides whether a label names multiple cell types (doublets, mixed populations) and returns sanitized parts. Semicolon-separated synonyms stay as one part. Results are cached per `run_id` under `user_files/decompositions/<run_id>/decompositions_<llmKey>.json`.
427+
`describe.decompose_label` and `describe.decompose_labels` call a dedicated LLM agent with `output_type=LabelDecomposition`. The model decides whether a label names multiple cell types (doublets, mixed populations) and returns sanitized parts in lowercase. Semicolon-separated synonyms stay as one part. Results are cached per `run_id` under `user_files/decompositions/<run_id>/decompositions_<llmKey>.json`.
422428

423429
Tune description batching by editing the constants at the top of `describe.py` or by passing `second_pass_wait_seconds` explicitly; the other values are module-level for now.
424430

@@ -448,7 +454,7 @@ Tune description batching by editing the constants at the top of `describe.py` o
448454
|----------------|-------------|-------|
449455
| `initialLabel` | `str` | Input label, copied verbatim. |
450456
| `isCompound` | `bool` | `true` when the label names more than one cell type. |
451-
| `parts` | `list[str]` | Sanitized cell-type parts. For non-compound labels, a single element equal to the label. |
457+
| `parts` | `list[str]` | Sanitized cell-type parts (lowercased when compound). For non-compound labels, a single element equal to the label. |
452458

453459
Used only for decomposition; descriptions are generated per part afterward.
454460

@@ -541,8 +547,9 @@ Set via `PathConfig(data_dir=..., user_dir=...)`:
541547
```
542548
<data_dir>/
543549
├── cell_ontology/
544-
│ ├── cell_to_cell_ontology.csv shipped
545-
│ └── cl.owl shipped
550+
│ ├── cell_to_cell_ontology.csv shipped (original synonyms)
551+
│ ├── cell_to_cell_ontology_enriched.csv shipped or built by setup.py
552+
│ └── cl.owl shipped
546553
└── embedding/
547554
├── cell_ontology/
548555
│ └── embeddings_<llmKey>_<embdKey>.npz
@@ -563,6 +570,10 @@ Set via `PathConfig(data_dir=..., user_dir=...)`:
563570
└── decompositions_<llmKey>.json
564571
```
565572

573+
`OntologyMapping` prefers the enriched CSV when it exists. That file keeps the original `label` column for display and adds `label_normalized` (lowercase) for lookup and ontology description inputs. Rows that share the same `(ontology_id, label_normalized)` after lowercasing are deduplicated (first kept).
574+
575+
`uv run python cyteonto/setup.py` downloads the original CSV, OWL, primary ontology descriptions/embeddings (required), backup pair artifacts (optional; failures warn and continue), and the enriched CSV (optional; builds locally from the original if the CDN file is missing).
576+
566577
Filename rules (`ModelArtifactKey.filename_segment`, `paths._clean_identifier`):
567578

568579
- Artifact key segment: `{provider}_{company}-{modelName}` after sanitizing `/`, `:`, and spaces.
@@ -601,6 +612,7 @@ When `use_cache=False` is passed to `compare`, all cache lookups are skipped and
601612
- Ontology embedding generation failure: `from_config` raises `RuntimeError`.
602613
- User embedding generation failure: `compare` raises `RuntimeError` with the offending identifier.
603614
- Label length mismatch between author and algorithm lists: `compare` raises `ValueError`.
615+
- Invalid `compound_scoring`: `compare` raises `ValueError`.
604616
- Duplicate algorithm name or an algorithm named `"author"`: `compare` raises `ValueError`.
605617
- Ontology match below `min_match_similarity`: CL id stored as empty string, `similarity_method` becomes `partial_match` or `no_matches`.
606618
- Both labels empty on a pair: `similarity_method = empty`, ontology fields blank.
@@ -616,7 +628,7 @@ When `use_cache=False` is passed to `compare`, all cache lookups are skipped and
616628
- **New provider**: add a URL to `_PROVIDER_URL` in `embed.py`, extend the `EmbdProvider` `Literal` in `models.py`, and adjust `_headers` / `_build_payload` / `_extract_embedding` if the request or response shape differs.
617629
- **New similarity metric**: implement a helper in `OntologySimilarity`, add a branch in `OntologySimilarity.similarity`, and update the table above.
618630
- **Alternative prompt**: edit `describe._build_prompt`. The existing prompt lists every `CellDescription` field with a soft length cap and a neutral tone; keep the same structure to avoid anchoring bias.
619-
- **Different ontology file**: pass a custom `data_dir`. The package expects the two files under `<data_dir>/cell_ontology/` to be named exactly `cl.owl` and `cell_to_cell_ontology.csv`.
631+
- **Different ontology file**: pass a custom `data_dir`. The package expects `cl.owl` and `cell_to_cell_ontology.csv` under `<data_dir>/cell_ontology/`. Prefer also shipping or generating `cell_to_cell_ontology_enriched.csv` via `setup.py`.
620632

621633
---
622634

@@ -626,7 +638,7 @@ Already declared in the project `pyproject.toml`:
626638

627639
- `pydantic`, `pydantic-ai`
628640
- `aiohttp`, `tenacity`
629-
- `numpy`, `pandas`, `scikit-learn`
641+
- `numpy`, `pandas`, `scikit-learn` (Hungarian assignment uses `scipy.optimize`, pulled in transitively)
630642
- `owlready2`
631643
- `loguru`, `python-dotenv`, `tqdm`
632644
- `requests` (PubMed tool)

0 commit comments

Comments
 (0)