pymetamaplite 0.1.0__tar.gz

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.
Files changed (143) hide show
  1. pymetamaplite-0.1.0/.gitignore +46 -0
  2. pymetamaplite-0.1.0/CHANGELOG.md +189 -0
  3. pymetamaplite-0.1.0/LICENSE +75 -0
  4. pymetamaplite-0.1.0/NOTICE-MetaMapLite.md +56 -0
  5. pymetamaplite-0.1.0/PKG-INFO +267 -0
  6. pymetamaplite-0.1.0/README.md +223 -0
  7. pymetamaplite-0.1.0/docs/GETTING_STARTED.md +210 -0
  8. pymetamaplite-0.1.0/docs/UMLS identifiers.md +40 -0
  9. pymetamaplite-0.1.0/examples/README.md +468 -0
  10. pymetamaplite-0.1.0/examples/_annotations.py +280 -0
  11. pymetamaplite-0.1.0/examples/_runs.py +210 -0
  12. pymetamaplite-0.1.0/examples/add_icd10cm_codes.py +222 -0
  13. pymetamaplite-0.1.0/examples/add_icd10cm_hierarchy.py +328 -0
  14. pymetamaplite-0.1.0/examples/add_loinc_codes.py +228 -0
  15. pymetamaplite-0.1.0/examples/add_rxnorm_codes.py +196 -0
  16. pymetamaplite-0.1.0/examples/add_rxnorm_ingredients.py +306 -0
  17. pymetamaplite-0.1.0/examples/add_snomed_codes.py +203 -0
  18. pymetamaplite-0.1.0/examples/add_snomedct_hierarchy.py +335 -0
  19. pymetamaplite-0.1.0/examples/analyze_file.py +174 -0
  20. pymetamaplite-0.1.0/examples/analyze_folder.py +360 -0
  21. pymetamaplite-0.1.0/examples/compare_exports.py +313 -0
  22. pymetamaplite-0.1.0/examples/create_combined_codes_view.py +134 -0
  23. pymetamaplite-0.1.0/examples/filter_mrconso.py +111 -0
  24. pymetamaplite-0.1.0/examples/filter_mrrel.py +138 -0
  25. pymetamaplite-0.1.0/examples/json_to_sqlite.py +256 -0
  26. pymetamaplite-0.1.0/examples/load_to_sqlite.py +345 -0
  27. pymetamaplite-0.1.0/examples/note01.txt +177 -0
  28. pymetamaplite-0.1.0/examples/note02.txt +160 -0
  29. pymetamaplite-0.1.0/examples/notes_of_interest.py +285 -0
  30. pymetamaplite-0.1.0/examples/parse_to_csv.py +218 -0
  31. pymetamaplite-0.1.0/examples/parse_to_jsonl.py +210 -0
  32. pymetamaplite-0.1.0/examples/parse_to_jsonl_batch.py +267 -0
  33. pymetamaplite-0.1.0/examples/parse_to_parquet.py +282 -0
  34. pymetamaplite-0.1.0/examples/parse_to_sqlite.py +221 -0
  35. pymetamaplite-0.1.0/pyproject.toml +130 -0
  36. pymetamaplite-0.1.0/requirements-scispacy-py312.txt +22 -0
  37. pymetamaplite-0.1.0/scripts/README.md +13 -0
  38. pymetamaplite-0.1.0/scripts/compare_nlp_with_java.py +161 -0
  39. pymetamaplite-0.1.0/scripts/compare_with_java.py +197 -0
  40. pymetamaplite-0.1.0/scripts/gen_context_triggers.py +67 -0
  41. pymetamaplite-0.1.0/scripts/gen_greek_table.py +37 -0
  42. pymetamaplite-0.1.0/scripts/gen_negex_triggers.py +34 -0
  43. pymetamaplite-0.1.0/src/pymetamaplite/__init__.py +18 -0
  44. pymetamaplite-0.1.0/src/pymetamaplite/api.py +232 -0
  45. pymetamaplite-0.1.0/src/pymetamaplite/cli.py +401 -0
  46. pymetamaplite-0.1.0/src/pymetamaplite/config.py +125 -0
  47. pymetamaplite-0.1.0/src/pymetamaplite/documents/__init__.py +115 -0
  48. pymetamaplite-0.1.0/src/pymetamaplite/documents/bioc.py +101 -0
  49. pymetamaplite-0.1.0/src/pymetamaplite/documents/corpora.py +204 -0
  50. pymetamaplite-0.1.0/src/pymetamaplite/documents/freetext.py +81 -0
  51. pymetamaplite-0.1.0/src/pymetamaplite/documents/model.py +100 -0
  52. pymetamaplite-0.1.0/src/pymetamaplite/documents/pubmed.py +187 -0
  53. pymetamaplite-0.1.0/src/pymetamaplite/documents/singleline.py +165 -0
  54. pymetamaplite-0.1.0/src/pymetamaplite/greek.py +147 -0
  55. pymetamaplite-0.1.0/src/pymetamaplite/index/__init__.py +4 -0
  56. pymetamaplite-0.1.0/src/pymetamaplite/index/build.py +227 -0
  57. pymetamaplite-0.1.0/src/pymetamaplite/index/lookup.py +278 -0
  58. pymetamaplite-0.1.0/src/pymetamaplite/index/schema.py +49 -0
  59. pymetamaplite-0.1.0/src/pymetamaplite/javautil.py +78 -0
  60. pymetamaplite-0.1.0/src/pymetamaplite/lru.py +48 -0
  61. pymetamaplite-0.1.0/src/pymetamaplite/normalize.py +93 -0
  62. pymetamaplite-0.1.0/src/pymetamaplite/output/__init__.py +78 -0
  63. pymetamaplite-0.1.0/src/pymetamaplite/output/formats.py +207 -0
  64. pymetamaplite-0.1.0/src/pymetamaplite/output/mmi.py +212 -0
  65. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/__init__.py +225 -0
  66. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/abbreviations.py +292 -0
  67. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/assertion.py +82 -0
  68. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/context.py +581 -0
  69. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/context_triggers.py +399 -0
  70. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/entity_lookup.py +446 -0
  71. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/negation.py +221 -0
  72. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/negex_triggers.py +268 -0
  73. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/postag.py +91 -0
  74. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/precision.py +111 -0
  75. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/segment.py +176 -0
  76. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/spacy_nlp.py +174 -0
  77. pymetamaplite-0.1.0/src/pymetamaplite/pipeline/tokenize.py +101 -0
  78. pymetamaplite-0.1.0/src/pymetamaplite/py.typed +0 -0
  79. pymetamaplite-0.1.0/src/pymetamaplite/semtypes.py +215 -0
  80. pymetamaplite-0.1.0/src/pymetamaplite/server.py +707 -0
  81. pymetamaplite-0.1.0/src/pymetamaplite/types.py +134 -0
  82. pymetamaplite-0.1.0/tests/__init__.py +0 -0
  83. pymetamaplite-0.1.0/tests/conftest.py +73 -0
  84. pymetamaplite-0.1.0/tests/examples/__init__.py +0 -0
  85. pymetamaplite-0.1.0/tests/examples/conftest.py +171 -0
  86. pymetamaplite-0.1.0/tests/examples/test_add_icd10cm_codes.py +162 -0
  87. pymetamaplite-0.1.0/tests/examples/test_add_icd10cm_hierarchy.py +172 -0
  88. pymetamaplite-0.1.0/tests/examples/test_add_loinc_codes.py +139 -0
  89. pymetamaplite-0.1.0/tests/examples/test_add_rxnorm_codes.py +127 -0
  90. pymetamaplite-0.1.0/tests/examples/test_add_rxnorm_ingredients.py +244 -0
  91. pymetamaplite-0.1.0/tests/examples/test_add_snomed_codes.py +141 -0
  92. pymetamaplite-0.1.0/tests/examples/test_add_snomedct_hierarchy.py +185 -0
  93. pymetamaplite-0.1.0/tests/examples/test_analyze_file.py +225 -0
  94. pymetamaplite-0.1.0/tests/examples/test_analyze_folder.py +206 -0
  95. pymetamaplite-0.1.0/tests/examples/test_annotations_helper.py +208 -0
  96. pymetamaplite-0.1.0/tests/examples/test_compare_exports.py +167 -0
  97. pymetamaplite-0.1.0/tests/examples/test_create_combined_codes_view.py +110 -0
  98. pymetamaplite-0.1.0/tests/examples/test_filter_mrconso.py +66 -0
  99. pymetamaplite-0.1.0/tests/examples/test_filter_mrrel.py +76 -0
  100. pymetamaplite-0.1.0/tests/examples/test_json_to_sqlite.py +180 -0
  101. pymetamaplite-0.1.0/tests/examples/test_load_to_sqlite.py +178 -0
  102. pymetamaplite-0.1.0/tests/examples/test_notes_of_interest.py +167 -0
  103. pymetamaplite-0.1.0/tests/examples/test_parse_to_csv.py +161 -0
  104. pymetamaplite-0.1.0/tests/examples/test_parse_to_jsonl.py +112 -0
  105. pymetamaplite-0.1.0/tests/examples/test_parse_to_jsonl_batch.py +167 -0
  106. pymetamaplite-0.1.0/tests/examples/test_parse_to_parquet.py +130 -0
  107. pymetamaplite-0.1.0/tests/examples/test_parse_to_sqlite.py +159 -0
  108. pymetamaplite-0.1.0/tests/examples/test_runs_helper.py +170 -0
  109. pymetamaplite-0.1.0/tests/parity/README.md +66 -0
  110. pymetamaplite-0.1.0/tests/parity/corpus/abbreviations.txt +1 -0
  111. pymetamaplite-0.1.0/tests/parity/corpus/clinical_note_1.txt +7 -0
  112. pymetamaplite-0.1.0/tests/parity/corpus/clinical_note_2.txt +1 -0
  113. pymetamaplite-0.1.0/tests/parity/corpus/crlf_note.txt +5 -0
  114. pymetamaplite-0.1.0/tests/parity/corpus/empty_ish.txt +3 -0
  115. pymetamaplite-0.1.0/tests/parity/corpus/long_sentence.txt +1 -0
  116. pymetamaplite-0.1.0/tests/parity/corpus/negation_cases.txt +1 -0
  117. pymetamaplite-0.1.0/tests/parity/corpus/pubmed_abstract_1.txt +1 -0
  118. pymetamaplite-0.1.0/tests/parity/corpus/pubmed_abstract_2.txt +1 -0
  119. pymetamaplite-0.1.0/tests/parity/corpus/short_lines.txt +9 -0
  120. pymetamaplite-0.1.0/tests/parity/corpus/tricky_tokens.txt +1 -0
  121. pymetamaplite-0.1.0/tests/parity/corpus/unicode_terms.txt +5 -0
  122. pymetamaplite-0.1.0/tests/parity/test_parity.py +334 -0
  123. pymetamaplite-0.1.0/tests/unit/__init__.py +0 -0
  124. pymetamaplite-0.1.0/tests/unit/test_abbreviations.py +173 -0
  125. pymetamaplite-0.1.0/tests/unit/test_assertion.py +146 -0
  126. pymetamaplite-0.1.0/tests/unit/test_cli.py +263 -0
  127. pymetamaplite-0.1.0/tests/unit/test_config.py +99 -0
  128. pymetamaplite-0.1.0/tests/unit/test_context.py +476 -0
  129. pymetamaplite-0.1.0/tests/unit/test_documents.py +747 -0
  130. pymetamaplite-0.1.0/tests/unit/test_entity_lookup.py +363 -0
  131. pymetamaplite-0.1.0/tests/unit/test_index.py +227 -0
  132. pymetamaplite-0.1.0/tests/unit/test_javautil.py +64 -0
  133. pymetamaplite-0.1.0/tests/unit/test_lru.py +67 -0
  134. pymetamaplite-0.1.0/tests/unit/test_negation.py +98 -0
  135. pymetamaplite-0.1.0/tests/unit/test_normalize.py +93 -0
  136. pymetamaplite-0.1.0/tests/unit/test_output.py +257 -0
  137. pymetamaplite-0.1.0/tests/unit/test_parity_comparator.py +120 -0
  138. pymetamaplite-0.1.0/tests/unit/test_pipeline.py +230 -0
  139. pymetamaplite-0.1.0/tests/unit/test_precision.py +103 -0
  140. pymetamaplite-0.1.0/tests/unit/test_segment.py +106 -0
  141. pymetamaplite-0.1.0/tests/unit/test_semtypes.py +19 -0
  142. pymetamaplite-0.1.0/tests/unit/test_server.py +428 -0
  143. pymetamaplite-0.1.0/tests/unit/test_tokenize.py +107 -0
@@ -0,0 +1,46 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ ivf/
8
+ # Java MetaMapLite output: contains UMLS-derived strings, do not commit
9
+ tests/parity/fixtures/
10
+ *.RRF
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ # examples/: the scripts, their README and their tests are source and ship. Everything they
14
+ # *produce* is UMLS-derived and does not -- annotation exports, the loaded databases and the
15
+ # filtered MRCONSO/MRREL extracts. Listed by extension rather than by name so a new export
16
+ # format is excluded by default: only .py, .md and the synthetic .txt notes are tracked.
17
+ examples/*.csv
18
+ examples/*.jsonl
19
+ examples/*.parquet
20
+ examples/*.db
21
+ examples/*.sqlite
22
+ examples/*.json
23
+
24
+ # annotation run output (examples/parse_to_*.py): verbatim note text and UMLS-derived
25
+ # strings, one directory per run. Never commit it.
26
+ out/
27
+ # corpora of notes to annotate: real ones are PHI, and even synthetic ones are not source
28
+ free_texts/
29
+
30
+ # local agent/tool state (a shared .claude/settings.json can still be committed)
31
+ # and per-user editor/notes vaults
32
+ .obsidian/
33
+ .claude/*.lock
34
+
35
+ # local credentials
36
+ .env
37
+ .env.*
38
+
39
+ # internal working document, not distributed
40
+ docs/DEVELOPMENT_PLAN.md
41
+
42
+ # internal working documents, not distributed
43
+ docs/OPERATIONS_MANUAL.md
44
+ docs/USER_GUIDE.md
45
+ docs/Negation detection.md
46
+ docs/Deploying to another machine.md
@@ -0,0 +1,189 @@
1
+ # Changelog
2
+
3
+ All notable changes to pymetamaplite are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ Rationale for the parity-driven decisions behind these changes is recorded in the project's
8
+ development plan, an internal working document that is not distributed.
9
+
10
+ ## [0.1.0] — 2026-09-19
11
+
12
+ First release. A Python port of MetaMapLite 3.6.2rc8 — the whole `EntityLookup4` path — as a
13
+ library, a CLI and a REST API, with no JVM.
14
+
15
+ Measured against Java MetaMapLite 3.6.2rc8 on UMLS 2026AA: the index tables are identical row
16
+ for row, tokenization is exact, and concepts found `(CUI, positions)` agree at P 0.975 / R 0.963
17
+ / F1 0.969. Most of the residual gap is the two NLP models MetaMapLite uses (OpenNLP's sentence
18
+ detector and POS tagger), which have no Python equivalent. See
19
+ [tests/parity/README.md](tests/parity/README.md) for the method.
20
+
21
+ ### Annotation
22
+
23
+ - **Index building** from UMLS RRF (`MRCONSO`, `MRSTY`, optionally `MRSAT` for MeSH tree codes),
24
+ producing the `cuisourceinfo`, `cuiconcept`, `cuist` and `meshtcrelaxed` tables in SQLite.
25
+ `--sources` restricts the build; `--include-suppressed` keeps `SUPPRESS=O/E/Y` rows.
26
+ - **The lookup pipeline**: normalization, sentence segmentation, tokenization, POS gating,
27
+ longest-match candidate generation, semantic-type and source restriction (abbreviations or
28
+ TUIs, any case), subsumption removal, Schwartz–Hearst abbreviation propagation, user-defined
29
+ acronyms, custom concept lists and excluded terms.
30
+ - **Negation**, two detectors: NegEx (the default, as in Java) and ConText (`--usecontext`),
31
+ which also reports temporality and experiencer.
32
+ - **Greek-letter spans.** A span may start or end with a Greek letter, as Java's `CharUtils`
33
+ allows, so `β-blocker` matches.
34
+ - **Non-ASCII dictionary keys** resolve reliably. Java's binary search compares UTF-8 as signed
35
+ bytes against a file sorted in `String` order and finds only about 44% of them, missing terms
36
+ such as `Sjögren` and `Ménière disease`; this is a deliberate divergence, pinned by name in the
37
+ test suite rather than averaged into the parity figures.
38
+ - **Precision filter** (`Settings.precision_filter`, default off, not in Java): drops a concept
39
+ whose only dictionary strings for the matched text are acronyms when the text is not in
40
+ capitals ("Plan" loses OMIM's `PLAN`), and one-word function-word mentions ("for", "with")
41
+ unless capitalised. Over the parity corpus it removed 267 of 2,907 matches, 261 of them (97.8%)
42
+ judged wrong or useless by hand.
43
+
44
+ ### Input and output
45
+
46
+ - **Every input format MetaMapLite registers**: `freetext`, `sli`, `sldi`, `sldiwi`, `chemdner`,
47
+ `chemdnersldi`, `ncbicorpus`, `pubtator`, `pubmed`, `medline`, `bioc`. `pubtator` is a
48
+ correction rather than a port — Java's loader closes its reader inside the read loop and throws
49
+ on every input. A document model and loader registry (`pymetamaplite.documents`) takes a new
50
+ format without changes elsewhere.
51
+ - **Output formats**: `mmi` (the default), `json`, `brat`, `cuilist`, `full`, and `bc` with
52
+ Java's aliases `bc-evaluate`, `cdi` and `bioc`.
53
+ - **Segmentation**: `SENTENCES`, `BLANKLINES`, `LINES`.
54
+
55
+ ### Interfaces
56
+
57
+ - **CLI** (`pymetamaplite`): `annotate`, `lookup`, `normalize`, `tokenize`, `build-index`,
58
+ `index-stats`, `serve`. `annotate` mirrors `metamaplite.sh`.
59
+ - **Python API**: `MetaMapLite(index_directory=...)`, `process_text`, `process_file`, `format`.
60
+ Configuration comes from a `Settings` object, MetaMapLite's own `metamaplite.properties` keys,
61
+ or `PYMETAMAPLITE_<NAME>` environment variables.
62
+ - **REST API** (`serve`): `POST /annotate`, `/annotate/batch`, `/annotate/text`,
63
+ `/annotate/formatted`, `GET /lookup`, `/formats`, `/semantic-types`, `/health`, with OpenAPI
64
+ docs at `/docs`. Requests are serialised behind a lock — annotation is CPU-bound and the
65
+ underlying objects are not thread-safe — so scale out with processes.
66
+ - **Opt-in REST authentication and CORS.** `PYMETAMAPLITE_API_TOKEN` makes every endpoint except
67
+ `/health` require `Authorization: Bearer <token>` (constant-time comparison);
68
+ `PYMETAMAPLITE_CORS_ORIGINS` adds `CORSMiddleware`. Both off unless set.
69
+ - **Request limits**: `PYMETAMAPLITE_MAX_TEXT_CHARS` (50,000, per document) and
70
+ `PYMETAMAPLITE_MAX_FORMATTED_CHARS` (1,000,000, the raw `/annotate/formatted` body). Both are
71
+ checked right after parsing, before anything is annotated.
72
+
73
+ ### Configuration and extension
74
+
75
+ - **`strict_parity`** (default `true`). `true` reproduces MetaMapLite 3.6.2rc8 exactly, bugs
76
+ included. `false` fixes the upstream bugs that give wrong answers, all in ConText:
77
+ - Only the mentions inside the sentence being read are analysed, each within its own bullet
78
+ item. Java applies every sentence to every mention in the document, so a later "Family
79
+ history of hypertension in mother" turned the patient's own earlier "hypertension" into
80
+ `Historical` / `Other`. This also makes ConText close to linear in document length rather
81
+ than quadratic.
82
+ - The longest trigger at a position wins, instead of whichever bucket is substituted first.
83
+ "Pneumonia was ruled out." is negated by the *post* trigger "was ruled out"; Java tagged it
84
+ with the shorter *pre* trigger "ruled out", which lands at the end of the sentence with
85
+ nothing after it to negate. "may be ruled out for X" is `Possible`, not a negation.
86
+ - Triggers stored with a trailing space (`denies `, `no `) fire anywhere, not only right before
87
+ the concept, so a whole "Denies A, B, C" list is negated rather than its first item — and
88
+ "no evidence of X" / "without evidence of X" negate at all.
89
+ - Time patterns match ("3 months of", "since last march"); they were character classes where
90
+ groups were meant. "previous" marks the next word historical. The first pseudo-trigger keeps
91
+ its first letter. "Rule out X" / "r/o X" gives `Possible`.
92
+ - Experiencer and temporality triggers inside a concept's own name count; negation triggers
93
+ there deliberately do not ("No known allergies" is itself the finding).
94
+ - `mmi`, `json` and `brat` list semantic types and sources alphabetically and documents in
95
+ input order, instead of Java `HashSet` order.
96
+
97
+ NegEx output is identical in both modes. `GET /health` reports `strict_parity`, and a server
98
+ running strict-parity ConText logs a warning once.
99
+ - **Extra ConText triggers** (`context_triggers.EXTRA_TRIGGERS`, fixed mode only). `cannot` is a
100
+ negation trigger on its own, so "cannot exclude pneumonia" came out *negated* — the opposite of
101
+ what it says. Adds the exclusion family (`cannot exclude`, `cannot be excluded`,
102
+ `cannot rule out`, `unable to exclude`, …), the hedges `concerning for`, `suspicious for`,
103
+ `suspected`, `differential includes`, `low suspicion for`, `possible`, `probable`, and
104
+ `absent` / `is absent` / `are absent`. `ConText(extra_triggers=...)` takes your own list.
105
+ - **`Entity.assertion`** carries ConText's verdict — `Affirmed`, `Negated` or `Possible`. Java
106
+ keeps only a boolean and discards the verdict, so a hedged mention read exactly like an
107
+ affirmed one. Shown as `[possible]` in `full` output and returned by the REST API; `negated` is
108
+ unchanged, so `mmi`, `json` and `brat` are untouched. `None` under NegEx.
109
+ - **Pluggable assertion.** `AssertionClassifier` adapts any per-sentence assertion model
110
+ (medspaCy's ConText, a transformer classifier, site rules) to the pipeline.
111
+ `register_negation_detector(name, factory, aliases)` makes it selectable wherever `negex` /
112
+ `context` are, and packages can declare one under the `pymetamaplite.negation_detectors`
113
+ entry-point group. `--negation-detector NAME` on `annotate` and `serve`.
114
+ - **Selectable POS tagger.** `Settings.postag_model` names the spaCy pipeline whose tagger gates
115
+ candidate spans; the default is `en_core_web_sm`. scispaCy's `en_core_sci_sm` agrees with Java
116
+ better (F1 0.977 vs 0.969) and is opt-in because its models need spaCy 3.7 / Python ≤ 3.12 —
117
+ `requirements-scispacy-py312.txt` pins a tested environment for it.
118
+ - **POS verb rescue** (`Settings.postag_verb_rescue`, default `0.03`) re-tags a token the model
119
+ calls a finite verb with its most likely noun or adjective reading. `en_core_web_sm` calls rare
120
+ clinical words verbs far more readily than OpenNLP, and the POS gate then never looks them up.
121
+ Raises parity F1 from 0.969 to 0.974 while *reducing* extras.
122
+
123
+ ### Performance
124
+
125
+ - Annotation runs at ~500 sentences/s on one core; resident memory ~120 MB. An index build over
126
+ UMLS 2026AA (all English sources) takes ~2.5 minutes and produces a 2.2 GB SQLite database:
127
+ 9.1 M terms, 7.3 M distinct keys, 3.5 M CUIs.
128
+ - Abbreviation propagation tokenizes each passage once rather than once per abbreviation-bearing
129
+ entity (48 KB: 7.0 s → 2.1 s, byte-identical output), and NegEx receives only the entities
130
+ starting inside each sentence.
131
+ - The four per-concept caches are LRU-capped at 50,000 entries each (~125 MB together) instead of
132
+ growing for the life of the process.
133
+ - `--preload` / `PYMETAMAPLITE_PRELOAD_INDEX` is off by default and documented as rarely worth
134
+ turning on: measured A/B with the index on local disk it is no faster, and costs ~4 s of
135
+ start-up and ~1.65 GB RSS per process.
136
+
137
+ ### Not implemented, deliberately
138
+
139
+ - **MetaMap-style scoring** (`EntityLookup5`, Java's `--enable_scoring`), off by default in Java.
140
+ Measured rather than assumed: with NLM's own index the score takes one of two values, carrying
141
+ ~0.04 bits per evidence. See the development plan, §10.1.
142
+ - **Word sense disambiguation, derivational variants, disjoint entities** — absent from
143
+ MetaMapLite itself.
144
+
145
+ ### Examples
146
+
147
+ Runnable scripts in [examples/](examples/) — a starting point to adapt, not a library. See
148
+ [examples/README.md](examples/README.md).
149
+
150
+ - **Annotation** — `analyze_file.py`, `analyze_folder.py` (one loaded index reused across a
151
+ folder, `--workers` for a process pool) and `json_to_sqlite.py`, which loads `--outputformat
152
+ json` files into a normalized `entities`/`concepts`/`evidence` database.
153
+ - **Corpus exports** — `parse_to_csv.py`, `parse_to_jsonl.py`, `parse_to_jsonl_batch.py`,
154
+ `parse_to_parquet.py` and `parse_to_sqlite.py` write one row per (span, concept) in five
155
+ formats from a single row shape (`_annotations.py`), so exports of one corpus agree and join
156
+ on the same key. `compare_exports.py` checks that rather than asserting it, and exits non-zero
157
+ on disagreement. `load_to_sqlite.py` completes the two-step route, sharing its schema with
158
+ `parse_to_sqlite.py` so both end at the same database.
159
+ - An attribute the run never assessed gets **no column**, and where one is kept it is
160
+ empty/NULL rather than 0: NegEx assesses `negated` only, `--usecontext` adds `assertion`,
161
+ `temporality` and `experiencer`, `--no-negation` assesses none. NULL means "not assessed"
162
+ and 0 means "assessed, and absent".
163
+ - Each run saves itself into a fresh `out/runs/<run-id>/` directory beside a `manifest.json`
164
+ recording the settings that produced it (`_runs.py`), so no run overwrites another.
165
+ - **Phenotyping** — `notes_of_interest.py` finds the notes that *affirm* a concept, counting a
166
+ document only when the mention is not negated, and matching by CUI so synonym and
167
+ morphological variants collapse.
168
+ - **Terminology crosswalks** — `filter_mrconso.py` / `filter_mrrel.py` and the `add_*_codes.py`
169
+ and `add_*_hierarchy.py` scripts map matched concepts to SNOMED CT, ICD-10-CM, RxNorm and
170
+ LOINC, with `create_combined_codes_view.py` over the result.
171
+
172
+ ### Documentation
173
+
174
+ - [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) — install to first results.
175
+ - [docs/UMLS identifiers.md](<docs/UMLS identifiers.md>) — CUI, LUI, SUI, AUI and TUI explained.
176
+
177
+ ### Licensing
178
+
179
+ Released under the same licence as MetaMapLite — NLM's open-source BSD licence — with the
180
+ upstream Informational Notice retained in [NOTICE-MetaMapLite.md](NOTICE-MetaMapLite.md).
181
+ pymetamaplite is an independent port and is **not** developed, funded or endorsed by NLM, NIH or
182
+ the U.S. Government. The UMLS data it indexes is licensed separately.
183
+
184
+ ### Quality
185
+
186
+ 706 tests, `ruff` clean, `mypy --strict` clean over `src/pymetamaplite`, CI on Python 3.11–3.13.
187
+ Every `examples/` script has its own test file under `tests/examples/`, run against small
188
+ synthetic MRCONSO/MRSTY fixtures and an index built from them, so the suite needs no UMLS
189
+ download. The parity tests skip wherever licensed fixtures are absent.
@@ -0,0 +1,75 @@
1
+ # Terms and Conditions for Use of pymetamaplite
2
+
3
+ pymetamaplite is a Python port of MetaMapLite. It is released under the same terms as
4
+ MetaMapLite itself: the open-source BSD license published by the U.S. National Library of
5
+ Medicine, reproduced in full below.
6
+
7
+ Scope, so the provenance is unambiguous:
8
+
9
+ * MetaMapLite was developed and funded by the National Library of Medicine. The Informational
10
+ Notice and LICENSE below are NLM's, and are reproduced verbatim from the MetaMapLite
11
+ distribution (see NOTICE-MetaMapLite.md). They apply to MetaMapLite and to the portions of
12
+ this software derived from it — algorithms, data tables, field layouts and output formats.
13
+ * pymetamaplite itself was not developed, funded, endorsed or reviewed by the National Library
14
+ of Medicine, the National Institutes of Health, or the U.S. Government. Copyright in the
15
+ Python code written for this port is held by its contributors, who release it under these
16
+ same terms.
17
+ * The UMLS Metathesaurus data this software indexes is licensed separately by NLM and is not
18
+ covered by this license. See the note in the Informational Notice below.
19
+
20
+ ---
21
+
22
+ # Informational Notice:
23
+
24
+ This software, “MetaMapLite” was developed and funded by the National
25
+ Library of Medicine, part of the National Institutes of Health, and
26
+ agency of the United States Department of Health and Human Services,
27
+ which is making the software available to the public for any
28
+ commercial or non-commercial purpose under the following open-source
29
+ BSD license.
30
+
31
+ NOTE: Users of the data distributed with MetaMapLite are
32
+ responsible for compliance with the UMLS Metathesaurus License
33
+ Agreement which requires you to respect the copyrights of the
34
+ constituent vocabularies and to file a brief annual report on your use
35
+ of the UMLS. You also must have activated a UMLS Terminology Services
36
+ (UTS) account.
37
+
38
+ # LICENSE:
39
+
40
+ Government Usage Rights Notice: The U.S. Government retains unlimited,
41
+ royalty-free usage rights to this software, but not ownership, as
42
+ provided by Federal law.
43
+
44
+ Redistribution and use in source and binary forms, with or without
45
+ modification, are permitted provided that the following conditions are
46
+ met:
47
+
48
+ * Redistributions of source code must retain this Informational Notice.
49
+ * Redistributions in binary form must reproduce this Informational
50
+ Notice, this list of conditions and the following disclaimer in the
51
+ documentation and/or other materials provided with the distribution.
52
+ * Neither the names of the National Library of Medicine, the National
53
+ Institutes of Health, nor the names of any of the software
54
+ developers may be used to endorse or promote products derived from
55
+ this software without specific prior written permission. The
56
+ U.S. Government retains an unlimited, royalty-free right to use,
57
+ distribute or modify the software.
58
+ * Please acknowledge NLM as the source of the MetaMapLite software by
59
+ including the phrase “Courtesy of the U.S. National Library of
60
+ Medicine” or “Source: U.S. National Library of Medicine.”
61
+
62
+
63
+ THIS SOFTWARE IS PROVIDED BY THE U.S. GOVERNMENT AND CONTRIBUTORS "AS
64
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
65
+ LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
66
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
67
+ U.S. GOVERNMENT
68
+
69
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
70
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
71
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
72
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
73
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
74
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
75
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,56 @@
1
+ # Terms and Conditions for Use of MetaMapLite
2
+
3
+ # Informational Notice:
4
+
5
+ This software, “MetaMapLite” was developed and funded by the National
6
+ Library of Medicine, part of the National Institutes of Health, and
7
+ agency of the United States Department of Health and Human Services,
8
+ which is making the software available to the public for any
9
+ commercial or non-commercial purpose under the following open-source
10
+ BSD license.
11
+
12
+ NOTE: Users of the data distributed with MetaMapLite are
13
+ responsible for compliance with the UMLS Metathesaurus License
14
+ Agreement which requires you to respect the copyrights of the
15
+ constituent vocabularies and to file a brief annual report on your use
16
+ of the UMLS. You also must have activated a UMLS Terminology Services
17
+ (UTS) account.
18
+
19
+ # LICENSE:
20
+
21
+ Government Usage Rights Notice: The U.S. Government retains unlimited,
22
+ royalty-free usage rights to this software, but not ownership, as
23
+ provided by Federal law.
24
+
25
+ Redistribution and use in source and binary forms, with or without
26
+ modification, are permitted provided that the following conditions are
27
+ met:
28
+
29
+ * Redistributions of source code must retain this Informational Notice.
30
+ * Redistributions in binary form must reproduce this Informational
31
+ Notice, this list of conditions and the following disclaimer in the
32
+ documentation and/or other materials provided with the distribution.
33
+ * Neither the names of the National Library of Medicine, the National
34
+ Institutes of Health, nor the names of any of the software
35
+ developers may be used to endorse or promote products derived from
36
+ this software without specific prior written permission. The
37
+ U.S. Government retains an unlimited, royalty-free right to use,
38
+ distribute or modify the software.
39
+ * Please acknowledge NLM as the source of the MetaMapLite software by
40
+ including the phrase “Courtesy of the U.S. National Library of
41
+ Medicine” or “Source: U.S. National Library of Medicine.”
42
+
43
+
44
+ THIS SOFTWARE IS PROVIDED BY THE U.S. GOVERNMENT AND CONTRIBUTORS "AS
45
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
46
+ LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
47
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
48
+ U.S. GOVERNMENT
49
+
50
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
51
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
52
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
53
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
54
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
55
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
56
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.5
2
+ Name: pymetamaplite
3
+ Version: 0.1.0
4
+ Summary: Python port of NLM MetaMapLite: near-real-time UMLS concept recognition
5
+ Project-URL: MetaMapLite (upstream), https://lhncbc.nlm.nih.gov/LHC-research/LHC-projects/NLP/MetaMapLite.html
6
+ Project-URL: MetaMapLite source (upstream), https://github.com/lhncbc/metamaplite
7
+ Project-URL: UMLS licensing, https://uts.nlm.nih.gov/uts/
8
+ License-Expression: LicenseRef-MetaMapLite-BSD
9
+ License-File: LICENSE
10
+ License-File: NOTICE-MetaMapLite.md
11
+ Keywords: biomedical,concept-recognition,metamap,metamaplite,ner,nlp,umls
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Healthcare Industry
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
23
+ Classifier: Topic :: Text Processing :: Indexing
24
+ Classifier: Topic :: Text Processing :: Linguistic
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: typer>=0.12
28
+ Provides-Extra: all
29
+ Requires-Dist: fastapi>=0.110; extra == 'all'
30
+ Requires-Dist: spacy>=3.7; extra == 'all'
31
+ Requires-Dist: uvicorn>=0.29; extra == 'all'
32
+ Provides-Extra: dev
33
+ Requires-Dist: build>=1.2; extra == 'dev'
34
+ Requires-Dist: httpx>=0.27; extra == 'dev'
35
+ Requires-Dist: mypy>=1.10; extra == 'dev'
36
+ Requires-Dist: pytest>=8; extra == 'dev'
37
+ Requires-Dist: ruff>=0.5; extra == 'dev'
38
+ Provides-Extra: nlp
39
+ Requires-Dist: spacy>=3.7; extra == 'nlp'
40
+ Provides-Extra: server
41
+ Requires-Dist: fastapi>=0.110; extra == 'server'
42
+ Requires-Dist: uvicorn>=0.29; extra == 'server'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # pymetamaplite
46
+
47
+ A Python port of [MetaMapLite](https://lhncbc.nlm.nih.gov/LHC-research/LHC-projects/NLP/MetaMapLite.html),
48
+ the U.S. National Library of Medicine's near-real-time UMLS concept recognizer — as a library,
49
+ a CLI, and a REST API, with no JVM.
50
+
51
+ *Courtesy of the U.S. National Library of Medicine.*
52
+
53
+ This README is a quick overview. **New here? Start with
54
+ [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md)** — install, build the index and get your
55
+ first results in under an hour. New to UMLS identifiers (CUI, LUI, SUI, AUI, TUI)? See
56
+ [docs/UMLS identifiers.md](<docs/UMLS identifiers.md>). Release notes are in
57
+ [CHANGELOG.md](CHANGELOG.md). Every CLI flag has `--help`, and the REST API documents itself at
58
+ `/docs`.
59
+
60
+ ## Verified against the original
61
+
62
+ Measured against **Java MetaMapLite 3.6.2rc8** on UMLS 2026AA (see
63
+ [tests/parity/README.md](tests/parity/README.md) for the method and how to reproduce):
64
+
65
+ | | |
66
+ |---|---|
67
+ | Index tables (`cuisourceinfo`, `cuiconcept`, `meshtcrelaxed`) | **identical, row for row** |
68
+ | Tokenization | **exact**, token for token |
69
+ | Concepts found, `(CUI, positions)` | **P 0.975 / R 0.963 / F1 0.969** |
70
+ | MMI records byte-identical | 430/565 |
71
+ | `json` output, spans found by both | 285/296 agree on every field Java emits |
72
+
73
+ Index contents, normalization, candidate generation, restriction, subsumption, negation,
74
+ abbreviations, MMI scoring and field layout are reproduced deterministically. Most of the
75
+ residual gap is the two NLP models MetaMapLite uses — OpenNLP's sentence detector (F1 0.923 vs
76
+ ours) and its POS tagger (0.850 tag agreement, 0.960 on the decision that affects results) —
77
+ which have no Python equivalent. One known difference is deliberate: Java's index lookup is
78
+ **unreliable for dictionary keys containing non-ASCII characters** — its binary search compares
79
+ UTF-8 as signed bytes against a file sorted in `String` order, so it finds some and misses others
80
+ depending on where the key falls. Measured over a 600-key sample it resolves about 44% of them,
81
+ missing terms such as `Sjögren` and `Ménière disease` that this port finds. That accounts for five
82
+ of the 35 concept-level disagreements, each pinned by name in the test suite rather than averaged
83
+ into the figures above. See the development plan, §16 and §21.
84
+
85
+ ## Install
86
+
87
+ ```powershell
88
+ uv venv
89
+ uv pip install --python .venv -e ".[dev,nlp,server]"
90
+ uv pip install --python .venv en_core_web_sm@https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl
91
+ ```
92
+
93
+ Extras: `nlp` (spaCy — POS tagging and spaCy sentence detection), `server` (FastAPI + uvicorn),
94
+ `all` for both. The core package needs only `typer`; without `nlp`, pass `--no-postag`.
95
+
96
+ The POS tagger is a spaCy model chosen by name (`--postag-model`, `PYMETAMAPLITE_POSTAG_MODEL`).
97
+ On Python ≤ 3.12, scispaCy's `en_core_sci_sm` measures **closer to Java than the default**
98
+ (F1 0.977 vs 0.969) and finds 27/30 drugs in a medication list where `en_core_web_sm` finds 20 —
99
+ see `--postag-model` in `pymetamaplite annotate --help`.
100
+
101
+ ## Build an index
102
+
103
+ You supply the UMLS — a licensed download, never redistributed with this package. `MRCONSO.RRF`
104
+ and `MRSTY.RRF` are required, `MRSAT.RRF` adds MeSH tree codes (used by MMI scoring):
105
+
106
+ ```powershell
107
+ pymetamaplite -v build-index --mrconso ...\MRCONSO.RRF --mrsty ...\MRSTY.RRF --mrsat ...\MRSAT.RRF --out ivf
108
+ ```
109
+
110
+ UMLS 2026AA (all English sources) takes ~2.5 min and produces a 2.2 GB SQLite database:
111
+ 9.1 M terms / 7.3 M distinct keys / 3.5 M CUIs. `--sources MSH,SNOMEDCT_US` restricts it;
112
+ `--include-suppressed` keeps `SUPPRESS=O/E/Y` rows.
113
+
114
+ ## Command line
115
+
116
+ ```powershell
117
+ pymetamaplite annotate "No history of myocardial infarction." --restrict-to-sts dsyn,sosy
118
+ pymetamaplite annotate --input note.txt --outputformat json # mmi (default) | json | brat | cuilist | full | bc
119
+ pymetamaplite annotate --pipe --no-negation < note.txt
120
+ pymetamaplite lookup "type 2 diabetes mellitus" # dictionary spot-check
121
+ pymetamaplite normalize "Alzheimer's Disease" # show the lookup keys for a span
122
+ pymetamaplite tokenize "Dr. Smith has type 2 diabetes." # sentences + tokens + POS
123
+ pymetamaplite index-stats
124
+ ```
125
+
126
+ `annotate` mirrors `metamaplite.sh`: `--restrict-to-sts`, `--restrict-to-sources`,
127
+ `--segmentation SENTENCES|BLANKLINES|LINES`, `--inputformat` (`freetext`, `sli`, `sldi`,
128
+ `sldiwi`, `chemdner`, `chemdnersldi`, `ncbicorpus`, `pubtator`, `pubmed`, `medline`, `bioc`),
129
+ `--excluded-terms`, `--uda`,
130
+ `--cui-term-list`, `--usecontext`, `--keep-subsumed`, `--preload`. Restrictions take semantic type abbreviations or TUIs and UMLS
131
+ SABs, in any case (`dsyn`, `DSYN`, `T047` and `t047` all select the same concepts).
132
+
133
+ ## Python API
134
+
135
+ ```python
136
+ from pymetamaplite import MetaMapLite
137
+
138
+ mml = MetaMapLite(index_directory="ivf")
139
+ entities = mml.process_text(
140
+ "No history of type 2 diabetes mellitus (T2DM). T2DM is treated with metformin.",
141
+ restrict_to_sts={"dsyn", "phsu"},
142
+ )
143
+ for e in entities:
144
+ print(e.start, e.length, e.text, e.negated, [(ev.cui, ev.concept.preferred_name) for ev in e.evs])
145
+ print(mml.format(entities, "mmi"))
146
+ ```
147
+
148
+ ```
149
+ 14 24 type 2 diabetes mellitus True [('C0011860', 'Diabetes Mellitus, Non-Insulin-Dependent')]
150
+ 40 4 T2DM False [('C0011860', 'Diabetes Mellitus, Non-Insulin-Dependent')]
151
+ 47 4 T2DM False [('C0011860', 'Diabetes Mellitus, Non-Insulin-Dependent')]
152
+ 68 9 metformin False [('C0025598', 'metformin')]
153
+ ```
154
+
155
+ `mml.process_file("note.txt")` annotates a file directly — offsets index the file verbatim
156
+ (line endings are never translated), so `brat` output lines up with a CRLF `.txt`.
157
+
158
+ `MetaMapLite(...)` takes a `Settings` object or `properties_file=` accepting MetaMapLite's own
159
+ `metamaplite.properties` keys (`metamaplite.index.directory`, `metamaplite.semanticgroup`,
160
+ `metamaplite.enable.postagging`, `metamaplite.detect.negations`, `metamaplite.postaglist`,
161
+ `metamaplite.uda.filename`, …); every key also reads from `PYMETAMAPLITE_<NAME>` env vars.
162
+
163
+ ## REST API
164
+
165
+ ```powershell
166
+ pymetamaplite serve --indexdir ivf --port 8000 # OpenAPI docs at /docs
167
+ ```
168
+
169
+ | Endpoint | Purpose |
170
+ |---|---|
171
+ | `POST /annotate` | one document → structured JSON entities |
172
+ | `POST /annotate/batch` | several documents in one request |
173
+ | `POST /annotate/text?format=mmi` | MetaMapLite's own output formats as `text/plain` |
174
+ | `POST /annotate/formatted` | a blob in any `--inputformat` (ChemDNER, PubMed XML, BioC, …) → one result per document |
175
+ | `GET /lookup?term=…` | dictionary lookup for a single term |
176
+ | `GET /formats` | every accepted input/output format, segmentation method and negation detector |
177
+ | `GET /semantic-types` | the UMLS semantic types, for building `restrict_to_sts` |
178
+ | `GET /health` | status, index metadata, uptime |
179
+
180
+ Any annotate request may pass `"detector": "context"` to use ConText for that request instead of
181
+ NegEx, which adds `temporality` and `experiencer` to each entity.
182
+
183
+ ```bash
184
+ curl -X POST http://127.0.0.1:8000/annotate -H "Content-Type: application/json" \
185
+ -d '{"text":"No chest pain.","restrict_to_sts":["sosy"]}'
186
+ ```
187
+
188
+ The index and spaCy model load once at start-up (~10 s; `--preload` adds a 4 s term-map load and
189
+ ~1.6 GB of RAM, and measured no faster, so leave it off). Requests take ~10–20 ms for a short clinical note. Annotation is
190
+ CPU-bound and the underlying objects are not thread-safe, so requests are serialised behind a
191
+ lock — **scale out with processes** (`uvicorn pymetamaplite.server:app --workers N`, each holding
192
+ its own copy of the index), not threads.
193
+
194
+ ## Performance
195
+
196
+ UMLS 2026AA, all English sources, on a laptop: index build 2.5 min; annotation ~500 sentences/s
197
+ (the absolute figure varies a lot with machine state);
198
+ resident memory ~120 MB, or ~1.8 GB with `preload_index=True` (a 419 MB pickled term map, 4 s to
199
+ load). Preloading measured no faster than the default SQLite lookups with the index on local disk,
200
+ so it is off by default and rarely worth turning on.
201
+
202
+ ## What is and isn't implemented
203
+
204
+ Implemented: the whole `EntityLookup4` path — index building, normalization, segmentation,
205
+ tokenization, POS gating, longest-match candidate generation, semantic-type/source restriction,
206
+ subsumption removal, NegEx negation, Schwartz–Hearst abbreviations, user-defined acronyms,
207
+ custom concept lists, excluded terms, both negation detectors (NegEx by default, ConText via
208
+ `--usecontext`, which adds temporality and experiencer), every `--inputformat` MetaMapLite registers (`freetext`,
209
+ `sli`/`sldi`, `sldiwi`, `chemdner`, `chemdnersldi`, `ncbicorpus`, `pubtator`, `pubmed`, `medline`,
210
+ `bioc` — `pubtator` as a correction, since Java's own loader throws on every input), and the
211
+ MMI / JSON / BRAT / CuiList output formats.
212
+
213
+ Not implemented, deliberately:
214
+
215
+ - **MetaMap-style scoring** (`EntityLookup5`, Java's `--enable_scoring`) — off by default in Java,
216
+ and measured rather than assumed: with NLM's own index the `vars` table is a single placeholder
217
+ row, so the variation term is identically zero and the coverage and cohesiveness terms reduce to
218
+ 1.0 by construction. The score takes one of two values — 666.67, or 833.33 in the rare case that
219
+ a defective head test fires (749 / 4 over the parity corpus). `EntityLookup5`'s substantive
220
+ difference from `EntityLookup4` is its candidate generation, not its scores. See
221
+ the development plan, §10.1, for the closed form and how to reproduce it.
222
+ - **Word sense disambiguation, derivational variants, disjoint entities** — absent from
223
+ MetaMapLite itself.
224
+
225
+ ## Development
226
+
227
+ ```powershell
228
+ .venv\Scripts\python -m pytest -q # 706 tests; the parity tests skip without Java fixtures
229
+ .venv\Scripts\ruff check src tests scripts examples
230
+ .venv\Scripts\ruff format --check src tests scripts examples
231
+ .venv\Scripts\mypy # strict, over src/pymetamaplite (see pyproject.toml)
232
+ python scripts/compare_with_java.py --java-dir <public_mm_lite> # parity vs the real thing
233
+ ```
234
+
235
+ `ruff format` is the formatter; lint rules and the 100-character line length are configured in
236
+ `pyproject.toml`. [CI](.github/workflows/ci.yml) runs the same lint, `mypy --strict` and the tests
237
+ on Python 3.11–3.13 on every push; the parity tests skip there, since their fixtures are
238
+ licensed. Anything that changes annotation output must keep the parity harness green — see
239
+ [tests/parity/README.md](tests/parity/README.md).
240
+
241
+ ## Releasing
242
+
243
+ [`release.yml`](.github/workflows/release.yml) publishes via PyPI Trusted Publishing, so no API
244
+ token is stored anywhere — PyPI verifies that this workflow, in this repository, produced the
245
+ upload. Running the workflow by hand publishes to **TestPyPI**; pushing a `v*` tag publishes to
246
+ **PyPI**. Both build, run `twine check`, and install the wheel into a clean environment before
247
+ uploading, and a tagged run additionally refuses to publish if the tag disagrees with
248
+ `__version__`. A version number, once on PyPI, can never be reused — so bump
249
+ `src/pymetamaplite/__init__.py` and tag it, rather than re-cutting one.
250
+
251
+ The project's development plan, an internal working document, records what each Java class does
252
+ and why each behaviour was reproduced — including deliberate quirks (a span must start and end with a
253
+ character Java's `CharUtils` calls alphanumeric, which is ASCII *and Greek*, so `β-blocker`
254
+ matches but `-blocker` cannot; sources and semantic types print in Java `HashSet` order).
255
+
256
+ ## Licensing
257
+
258
+ This package is released under the **same licence as MetaMapLite** — NLM's open-source BSD
259
+ licence — reproduced in [LICENSE](LICENSE), with the upstream notice retained verbatim in
260
+ [NOTICE-MetaMapLite.md](NOTICE-MetaMapLite.md). That licence requires you to keep the
261
+ Informational Notice in redistributions and to acknowledge NLM as the source; hence the
262
+ "Courtesy of the U.S. National Library of Medicine" line at the top of this README.
263
+
264
+ pymetamaplite is an independent port and is **not** developed, funded or endorsed by NLM, NIH or
265
+ the U.S. Government. The UMLS data it indexes is licensed separately: you need a UMLS
266
+ Metathesaurus License and a UTS account, and the index you build from it is yours alone to keep —
267
+ don't redistribute it.