celldn 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
celldn/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """CellDN public API."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("celldn")
7
+ except PackageNotFoundError: # pragma: no cover - source-tree fallback.
8
+ __version__ = "1.0.0"
9
+
10
+ from .pipeline import (
11
+ DEFAULT_BIOC_CHUNK_SIZE,
12
+ DEFAULT_FILE_CHUNK_SIZE,
13
+ DEFAULT_NEN_MODEL,
14
+ DEFAULT_NER_MODEL,
15
+ DEFAULT_PMID_CHUNK_SIZE,
16
+ CellDNPipeline,
17
+ ExtractionResult,
18
+ RecognizedMention,
19
+ write_predictions_json,
20
+ )
21
+
22
+ __all__ = [
23
+ "DEFAULT_BIOC_CHUNK_SIZE",
24
+ "DEFAULT_FILE_CHUNK_SIZE",
25
+ "DEFAULT_NEN_MODEL",
26
+ "DEFAULT_NER_MODEL",
27
+ "DEFAULT_PMID_CHUNK_SIZE",
28
+ "CellDNPipeline",
29
+ "ExtractionResult",
30
+ "RecognizedMention",
31
+ "write_predictions_json",
32
+ "__version__",
33
+ ]
34
+
35
+ try:
36
+ from .mics.output_display import render_cell_type_annotations
37
+ except ImportError: # pragma: no cover - notebook helper is optional.
38
+ pass
39
+ else:
40
+ __all__.append("render_cell_type_annotations")
celldn/cli.py ADDED
@@ -0,0 +1,424 @@
1
+ """Command line interface for CellDN."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from importlib.metadata import version
9
+ from pathlib import Path
10
+ from typing import Iterable
11
+
12
+ from .io import (
13
+ clean_pmid_list,
14
+ collection_summary,
15
+ convert_bioc_file,
16
+ output_format_from_path,
17
+ )
18
+ from .pipeline import (
19
+ DEFAULT_BIOC_CHUNK_SIZE,
20
+ DEFAULT_FILE_CHUNK_SIZE,
21
+ DEFAULT_NEN_MODEL,
22
+ DEFAULT_NER_MODEL,
23
+ DEFAULT_PMID_CHUNK_SIZE,
24
+ CellDNPipeline,
25
+ write_predictions_json,
26
+ )
27
+ from .retrieval import combine_ids
28
+
29
+ TEXT_TASK_CHOICES = ["ner", "end-to-end", "e2e"]
30
+ BIOC_INPUT_FORMAT_CHOICES = ["auto", "bioc-xml", "bioc-json", "json"]
31
+ BIOC_OUTPUT_FORMAT_CHOICES = ["auto", "bioc-xml", "bioc-json"]
32
+ BIOC_RUN_TASK_CHOICES = ["ner", "end-to-end", "e2e"]
33
+ PMID_TASK_CHOICES = ["ner", "nen", "end-to-end", "e2e"]
34
+ FILE_TASK_CHOICES = ["ner", "nen", "end-to-end", "e2e"]
35
+
36
+
37
+ def build_parser() -> argparse.ArgumentParser:
38
+ """Build the top-level ``celldn`` command parser."""
39
+
40
+ parser = argparse.ArgumentParser(
41
+ prog="celldn",
42
+ description="Cell-type recognition and Cell Ontology normalization for text, BioC, JSON, and PMID inputs.",
43
+ )
44
+ parser.add_argument("--version", action="store_true", help="Print package version and exit.")
45
+ subparsers = parser.add_subparsers(dest="command")
46
+
47
+ # Existing public commands retained.
48
+ p_download = subparsers.add_parser("download-models", help="Download default Hugging Face model checkpoints.")
49
+ p_download.add_argument("--ner-model", default=DEFAULT_NER_MODEL)
50
+ p_download.add_argument("--nen-model", default=DEFAULT_NEN_MODEL)
51
+ p_download.add_argument("--output-dir", default="models")
52
+ p_download.add_argument("--trust-remote-code", action="store_true")
53
+ p_download.set_defaults(func=_cmd_download_models)
54
+
55
+ p_text = subparsers.add_parser("predict-text", help="Run CellDN on plain text and write JSON.")
56
+ _add_model_args(p_text)
57
+ text_group = p_text.add_mutually_exclusive_group(required=True)
58
+ text_group.add_argument("--text", help="Text string to process.")
59
+ text_group.add_argument("--input", "-i", help="Plain-text input file.")
60
+ p_text.add_argument("--output", "-o", required=True, help="Output JSON file.")
61
+ p_text.add_argument("--task", choices=TEXT_TASK_CHOICES, default="end-to-end")
62
+ p_text.add_argument("--document-id", default=None)
63
+ p_text.set_defaults(func=_cmd_predict_text)
64
+
65
+ p_run_bioc = subparsers.add_parser(
66
+ "run-bioc",
67
+ help="Run NER, NEN, or end-to-end prediction on BioC XML, BioC JSON, or generic JSON input.",
68
+ )
69
+ _add_bioc_run_args(p_run_bioc, default_task="end-to-end", allow_nen=True)
70
+ p_run_bioc.set_defaults(func=_cmd_run_bioc)
71
+
72
+ p_run_files = subparsers.add_parser(
73
+ "run-files",
74
+ help="Process many text, BioC XML, BioC JSON, or generic JSON files in reusable model chunks.",
75
+ )
76
+ _add_model_args(p_run_files, include_output_dir=False)
77
+ p_run_files.add_argument(
78
+ "inputs",
79
+ nargs="+",
80
+ help="Input files or directories. Directories may be searched recursively.",
81
+ )
82
+ p_run_files.add_argument(
83
+ "--results-dir",
84
+ required=True,
85
+ help="Directory for one result file per input file.",
86
+ )
87
+ p_run_files.add_argument("--task", choices=FILE_TASK_CHOICES, default="end-to-end")
88
+ p_run_files.add_argument(
89
+ "--chunk-size",
90
+ type=int,
91
+ default=DEFAULT_FILE_CHUNK_SIZE,
92
+ help="Input files combined per batch.",
93
+ )
94
+ p_run_files.add_argument(
95
+ "--bioc-chunk-size",
96
+ type=int,
97
+ default=DEFAULT_BIOC_CHUNK_SIZE,
98
+ help="BioC passages processed per inference chunk.",
99
+ )
100
+ p_run_files.add_argument("--input-format", choices=BIOC_INPUT_FORMAT_CHOICES, default="auto")
101
+ p_run_files.add_argument("--output-format", choices=BIOC_OUTPUT_FORMAT_CHOICES, default="auto")
102
+ p_run_files.add_argument("--preserve-existing-annotations", action="store_true")
103
+ p_run_files.add_argument(
104
+ "--recursive",
105
+ action=argparse.BooleanOptionalAction,
106
+ default=True,
107
+ help="Search input directories recursively (default: true).",
108
+ )
109
+ p_run_files.add_argument(
110
+ "--overwrite",
111
+ action=argparse.BooleanOptionalAction,
112
+ default=True,
113
+ help="Replace existing result files (default: true).",
114
+ )
115
+ p_run_files.set_defaults(func=_cmd_run_files)
116
+
117
+ p_predict_pmid = subparsers.add_parser(
118
+ "predict-pmid",
119
+ help="Fetch PMID/PMCID content and run NER, NEN, or end-to-end prediction.",
120
+ )
121
+ _add_model_args(p_predict_pmid)
122
+ _add_id_args(p_predict_pmid)
123
+ p_predict_pmid.add_argument("--output", "-o", required=True, help="Output BioC XML/JSON path.")
124
+ p_predict_pmid.add_argument("--task", choices=PMID_TASK_CHOICES, default="end-to-end")
125
+ p_predict_pmid.add_argument("--text-source", choices=["abstract", "fulltext"], default="abstract")
126
+ p_predict_pmid.add_argument("--source", choices=["ncbi", "europepmc"], default="ncbi")
127
+ p_predict_pmid.add_argument("--output-format", choices=BIOC_OUTPUT_FORMAT_CHOICES, default="auto")
128
+ p_predict_pmid.add_argument("--keep-intermediate-bioc", help="Save the retrieved BioC XML used for prediction.")
129
+ p_predict_pmid.add_argument("--preserve-existing-annotations", action="store_true")
130
+ p_predict_pmid.add_argument(
131
+ "--strict-fetch",
132
+ action="store_true",
133
+ help="Fail if any requested ID cannot be retrieved.",
134
+ )
135
+ p_predict_pmid.add_argument("--timeout", type=int, default=30)
136
+ p_predict_pmid.add_argument("--pause", type=float, default=0.12)
137
+ p_predict_pmid.add_argument(
138
+ "--chunk-size",
139
+ type=int,
140
+ default=DEFAULT_PMID_CHUNK_SIZE,
141
+ help="Publication identifiers retrieved per chunk.",
142
+ )
143
+ p_predict_pmid.add_argument(
144
+ "--bioc-chunk-size",
145
+ type=int,
146
+ default=DEFAULT_BIOC_CHUNK_SIZE,
147
+ help="BioC passages processed per inference chunk.",
148
+ )
149
+ p_predict_pmid.set_defaults(func=_cmd_predict_pmid)
150
+
151
+ p_convert = subparsers.add_parser(
152
+ "convert-bioc",
153
+ help="Convert BioC XML, BioC JSON, or generic JSON input to BioC XML/JSON.",
154
+ )
155
+ p_convert.add_argument("input")
156
+ p_convert.add_argument("output")
157
+ p_convert.add_argument("--input-format", choices=BIOC_INPUT_FORMAT_CHOICES, default="auto")
158
+ p_convert.add_argument("--output-format", choices=BIOC_OUTPUT_FORMAT_CHOICES, default="auto")
159
+ p_convert.add_argument("--summary", action="store_true", help="Print collection counts after conversion.")
160
+ p_convert.set_defaults(func=_cmd_convert_bioc)
161
+
162
+ return parser
163
+
164
+
165
+ def _add_model_args(
166
+ parser: argparse.ArgumentParser,
167
+ *,
168
+ include_output_dir: bool = True,
169
+ ) -> None:
170
+ """Add shared model/runtime options to a subcommand parser."""
171
+
172
+ parser.add_argument("--ner-model", default=DEFAULT_NER_MODEL)
173
+ parser.add_argument("--nen-model", default=DEFAULT_NEN_MODEL)
174
+ parser.add_argument("--ontology-path")
175
+ parser.add_argument("--abbreviations-path")
176
+ parser.add_argument("--disable-abbreviations", action="store_true")
177
+ if include_output_dir:
178
+ parser.add_argument(
179
+ "--output-dir",
180
+ default="celldn_outputs",
181
+ help="Working directory for intermediate prediction artifacts.",
182
+ )
183
+ parser.add_argument(
184
+ "--warmup-runs",
185
+ type=int,
186
+ default=0,
187
+ help="Optional one-batch NER warmup runs. The full corpus is never used for warmup.",
188
+ )
189
+ parser.add_argument("--batch-size", type=int, default=16, help="Inference batch size per device.")
190
+ parser.add_argument("--fp16", action="store_true")
191
+ parser.add_argument("--trust-remote-code", action="store_true")
192
+ parser.add_argument("--verbose", action="store_true")
193
+
194
+
195
+ def _add_bioc_run_args(parser: argparse.ArgumentParser, *, default_task: str, allow_nen: bool) -> None:
196
+ """Add shared BioC prediction arguments to a subcommand parser."""
197
+
198
+ _add_model_args(parser)
199
+ parser.add_argument("input", help="Input BioC XML/BioC JSON/JSON file.")
200
+ parser.add_argument("output", help="Output BioC XML/JSON file.")
201
+ choices = BIOC_RUN_TASK_CHOICES + (["nen"] if allow_nen else [])
202
+ parser.add_argument("--task", choices=choices, default=default_task)
203
+ parser.add_argument("--input-format", choices=BIOC_INPUT_FORMAT_CHOICES, default="auto")
204
+ parser.add_argument("--output-format", choices=BIOC_OUTPUT_FORMAT_CHOICES, default="auto")
205
+ parser.add_argument("--preserve-existing-annotations", action="store_true")
206
+ parser.add_argument("--ner-output-xml")
207
+ parser.add_argument(
208
+ "--chunk-size",
209
+ type=int,
210
+ default=DEFAULT_BIOC_CHUNK_SIZE,
211
+ help="BioC passages processed per inference chunk.",
212
+ )
213
+ parser.add_argument("--overwrite", action=argparse.BooleanOptionalAction, default=True)
214
+
215
+
216
+ def _add_id_args(parser: argparse.ArgumentParser) -> None:
217
+ """Add PMID/PMCID input arguments to a subcommand parser."""
218
+
219
+ parser.add_argument("ids", nargs="*", help="PMIDs/PMCIDs. Commas, spaces, and repeated values are accepted.")
220
+ parser.add_argument(
221
+ "--pmid",
222
+ "--id",
223
+ dest="inline_ids",
224
+ action="append",
225
+ default=[],
226
+ help="PMID/PMCID value or comma-separated list. Can be repeated.",
227
+ )
228
+ parser.add_argument("--ids-file", help="Text file containing PMIDs/PMCIDs.")
229
+
230
+
231
+ def _pipeline_from_args(args: argparse.Namespace) -> CellDNPipeline:
232
+ """Create a pipeline instance from parsed CLI arguments."""
233
+
234
+ return CellDNPipeline.from_pretrained(
235
+ ner_model=args.ner_model,
236
+ nen_model=args.nen_model,
237
+ ontology_path=args.ontology_path,
238
+ abbreviations_path=args.abbreviations_path,
239
+ disable_abbreviations=args.disable_abbreviations,
240
+ output_dir=getattr(args, "output_dir", "celldn_outputs"),
241
+ warmup_runs=args.warmup_runs,
242
+ batch_size=args.batch_size,
243
+ fp16=args.fp16,
244
+ trust_remote_code=args.trust_remote_code,
245
+ verbose=args.verbose,
246
+ )
247
+
248
+
249
+ def _ids_from_args(args: argparse.Namespace) -> list[str]:
250
+ """Resolve PMIDs/PMCIDs from positional, repeated, and file-based inputs."""
251
+
252
+ ids: list[str] = []
253
+ ids.extend(clean_pmid_list(args.ids or []))
254
+ ids.extend(clean_pmid_list(args.inline_ids or []))
255
+ ids = combine_ids(ids, args.ids_file)
256
+ if not ids:
257
+ raise SystemExit("No PMIDs/PMCIDs provided. Use positional IDs, --pmid, or --ids-file.")
258
+ return ids
259
+
260
+
261
+ def _read_text_arg(args: argparse.Namespace) -> str:
262
+ """Return inline text or load it from the requested input file."""
263
+
264
+ return args.text if args.text is not None else Path(args.input).read_text(encoding="utf-8")
265
+
266
+
267
+ def _resolve_bioc_output_format(output_path: str, requested_format: str) -> str:
268
+ """Honor an explicit format or infer one from the output path."""
269
+
270
+ if requested_format == "auto":
271
+ return output_format_from_path(output_path)
272
+ return requested_format
273
+
274
+
275
+ def _model_dir_name(model_reference: str) -> str:
276
+ """Derive a readable local directory name from a model reference."""
277
+
278
+ text = str(model_reference).rstrip("/").rstrip("\\")
279
+ if not text:
280
+ return "model"
281
+ return text.replace("\\", "/").split("/")[-1] or "model"
282
+
283
+
284
+ def _cmd_download_models(args: argparse.Namespace) -> int:
285
+ """Download the default NER and NEN checkpoints for local reuse."""
286
+
287
+ from huggingface_hub import snapshot_download
288
+
289
+ output_dir = Path(args.output_dir)
290
+ output_dir.mkdir(parents=True, exist_ok=True)
291
+ ner_dir = output_dir / _model_dir_name(args.ner_model)
292
+ nen_dir = output_dir / _model_dir_name(args.nen_model)
293
+ # snapshot_download stores model files only; trust_remote_code is used later by model loaders.
294
+ ner_path = snapshot_download(args.ner_model, local_dir=ner_dir)
295
+ nen_path = snapshot_download(args.nen_model, local_dir=nen_dir)
296
+ manifest = {
297
+ "ner_model": ner_path,
298
+ "nen_model": nen_path,
299
+ }
300
+ (output_dir / "models.json").write_text(
301
+ json.dumps(manifest, indent=2) + "\n",
302
+ encoding="utf-8",
303
+ )
304
+ print(json.dumps(manifest, indent=2))
305
+ return 0
306
+
307
+
308
+ def _cmd_predict_text(args: argparse.Namespace) -> int:
309
+ """Run the plain-text prediction subcommand and write JSON output."""
310
+
311
+ pipe = _pipeline_from_args(args)
312
+ text = _read_text_arg(args)
313
+ predictions = pipe.run_text(
314
+ text,
315
+ task=args.task,
316
+ document_id=args.document_id,
317
+ )
318
+ output = write_predictions_json(
319
+ predictions,
320
+ args.output,
321
+ include_document_metadata=False,
322
+ )
323
+ print(str(output))
324
+ return 0
325
+
326
+
327
+ def _cmd_run_bioc(args: argparse.Namespace) -> int:
328
+ """Run the BioC/JSON prediction subcommand."""
329
+
330
+ pipe = _pipeline_from_args(args)
331
+ output = pipe.run_bioc(
332
+ args.input,
333
+ args.output,
334
+ task=args.task,
335
+ input_format=args.input_format,
336
+ output_format=args.output_format,
337
+ preserve_existing_annotations=args.preserve_existing_annotations,
338
+ ner_output_xml=args.ner_output_xml,
339
+ overwrite=args.overwrite,
340
+ passage_chunk_size=args.chunk_size,
341
+ )
342
+ print(str(output))
343
+ return 0
344
+
345
+
346
+ def _cmd_run_files(args: argparse.Namespace) -> int:
347
+ """Process many files while reusing one pipeline instance."""
348
+
349
+ pipe = _pipeline_from_args(args)
350
+ outputs = pipe.run_files(
351
+ args.inputs,
352
+ args.results_dir,
353
+ task=args.task,
354
+ batch_size=args.chunk_size,
355
+ passage_chunk_size=args.bioc_chunk_size,
356
+ input_format=args.input_format,
357
+ output_format=args.output_format,
358
+ preserve_existing_annotations=args.preserve_existing_annotations,
359
+ recursive=args.recursive,
360
+ overwrite=args.overwrite,
361
+ )
362
+ for output in outputs:
363
+ print(str(output))
364
+ return 0
365
+
366
+
367
+ def _cmd_predict_pmid(args: argparse.Namespace) -> int:
368
+ """Fetch PMID/PMCID content and run prediction in one step."""
369
+
370
+ pipe = _pipeline_from_args(args)
371
+ ids = _ids_from_args(args)
372
+ output = pipe.run_pmids(
373
+ ids,
374
+ args.output,
375
+ task=args.task,
376
+ text_source=args.text_source,
377
+ source=args.source,
378
+ output_format=args.output_format,
379
+ keep_intermediate_bioc=args.keep_intermediate_bioc,
380
+ preserve_existing_annotations=args.preserve_existing_annotations,
381
+ strict_fetch=args.strict_fetch,
382
+ timeout=args.timeout,
383
+ pause=args.pause,
384
+ batch_size=args.chunk_size,
385
+ passage_chunk_size=args.bioc_chunk_size,
386
+ )
387
+ print(str(output))
388
+ return 0
389
+
390
+
391
+ def _cmd_convert_bioc(args: argparse.Namespace) -> int:
392
+ """Convert between supported BioC input and output formats."""
393
+
394
+ output = convert_bioc_file(
395
+ args.input,
396
+ args.output,
397
+ input_format=args.input_format,
398
+ output_format=args.output_format,
399
+ )
400
+ print(str(output))
401
+ if args.summary:
402
+ print(json.dumps(collection_summary(output), indent=2))
403
+ return 0
404
+
405
+
406
+ def main(argv: Iterable[str] | None = None) -> int:
407
+ """Run the ``celldn`` CLI entry point."""
408
+
409
+ parser = build_parser()
410
+ args = parser.parse_args(list(argv) if argv is not None else None)
411
+ if args.version:
412
+ try:
413
+ print(version("celldn"))
414
+ except Exception: # noqa: BLE001
415
+ print("celldn")
416
+ return 0
417
+ if not hasattr(args, "func"):
418
+ parser.print_help()
419
+ return 2
420
+ return int(args.func(args))
421
+
422
+
423
+ if __name__ == "__main__": # pragma: no cover
424
+ raise SystemExit(main(sys.argv[1:]))