aimedicalcoding 0.3.6__tar.gz → 0.3.8__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 (73) hide show
  1. {aimedicalcoding-0.3.6/aimedicalcoding.egg-info → aimedicalcoding-0.3.8}/PKG-INFO +1 -1
  2. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/fhir/codes.py +19 -1
  3. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/cvx/patterns.py +6 -3
  4. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/icd10cm/patterns.py +2 -1
  5. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/loinc/patterns.py +4 -1
  6. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/rxnorm/patterns.py +7 -3
  7. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/snomed/patterns.py +4 -2
  8. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8/aimedicalcoding.egg-info}/PKG-INFO +1 -1
  9. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/GAP_SCHEMA.md +2 -2
  10. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/setup.py +1 -1
  11. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/LICENSE +0 -0
  12. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/MANIFEST.in +0 -0
  13. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/README.md +0 -0
  14. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/README_PYPI.md +0 -0
  15. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/__init__.py +0 -0
  16. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/__main__.py +0 -0
  17. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/__init__.py +0 -0
  18. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/base.py +0 -0
  19. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/ccda/__init__.py +0 -0
  20. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/ccda/adapter.py +0 -0
  21. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/ccda/codes.py +0 -0
  22. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/ccda/narrative.py +0 -0
  23. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/ccda/sections.py +0 -0
  24. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/custom_json/__init__.py +0 -0
  25. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/custom_json/adapter.py +0 -0
  26. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/fhir/__init__.py +0 -0
  27. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/fhir/adapter.py +0 -0
  28. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/adapters/fhir/mapping.py +0 -0
  29. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/api.py +0 -0
  30. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/core/__init__.py +0 -0
  31. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/core/checkdigits.py +0 -0
  32. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/core/models.py +0 -0
  33. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/core/system_registry.py +0 -0
  34. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/pipeline/__init__.py +0 -0
  35. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/pipeline/orchestrator.py +0 -0
  36. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/recovery/__init__.py +0 -0
  37. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/recovery/engine.py +0 -0
  38. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/serialize.py +0 -0
  39. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/__init__.py +0 -0
  40. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/base.py +0 -0
  41. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/cvx/__init__.py +0 -0
  42. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/cvx/handler.py +0 -0
  43. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/cvx/normalize.py +0 -0
  44. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/icd10cm/__init__.py +0 -0
  45. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/icd10cm/handler.py +0 -0
  46. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/icd10cm/icdformat.py +0 -0
  47. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/loinc/__init__.py +0 -0
  48. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/loinc/axes.py +0 -0
  49. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/loinc/handler.py +0 -0
  50. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/loinc/normalize.py +0 -0
  51. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/rxnorm/__init__.py +0 -0
  52. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/rxnorm/handler.py +0 -0
  53. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/rxnorm/normalize.py +0 -0
  54. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/snomed/__init__.py +0 -0
  55. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/snomed/handler.py +0 -0
  56. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/snomed/hierarchy.py +0 -0
  57. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/systems/snomed/qualifiers.py +0 -0
  58. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/terminology/__init__.py +0 -0
  59. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding/terminology/service.py +0 -0
  60. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding.egg-info/SOURCES.txt +0 -0
  61. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding.egg-info/dependency_links.txt +0 -0
  62. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding.egg-info/entry_points.txt +0 -0
  63. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding.egg-info/requires.txt +0 -0
  64. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/aimedicalcoding.egg-info/top_level.txt +0 -0
  65. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/ARCHITECTURE.md +0 -0
  66. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/OUTPUT_GUIDE.md +0 -0
  67. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/patterns/CVX_pattern_analysis.md +0 -0
  68. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/patterns/ICD10CM_pattern_analysis.md +0 -0
  69. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/patterns/LOINC_pattern_analysis.md +0 -0
  70. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/patterns/RxNorm_pattern_analysis.md +0 -0
  71. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/docs/patterns/SNOMED_pattern_analysis.md +0 -0
  72. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/pyproject.toml +0 -0
  73. {aimedicalcoding-0.3.6 → aimedicalcoding-0.3.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aimedicalcoding
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
5
5
  Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
6
6
  License: MIT
@@ -169,7 +169,9 @@ def extract_value(resource: dict) -> Optional[ValueInfo]:
169
169
  # --------------------------------------------------------------------------
170
170
 
171
171
  def extract_companion(resource: dict) -> dict:
172
- """interpretation + reference range (Observation-shaped resources)."""
172
+ """interpretation + reference range + method + body site (Observation-shaped
173
+ resources). FHIR analog of the CCDA extractor: Observation.method and
174
+ Observation.bodySite are CodeableConcepts (the LOINC Method axis / site)."""
173
175
  out: dict[str, Any] = {}
174
176
  for interp in resource.get("interpretation") or []:
175
177
  code = next((clean(c.get("code")) for c in interp.get("coding") or []
@@ -189,6 +191,22 @@ def extract_companion(resource: dict) -> dict:
189
191
  if clean(rr.get("text")):
190
192
  out["reference_range"] = clean(rr.get("text"))
191
193
  break
194
+
195
+ # method: LOINC Method axis (Observation.method CodeableConcept)
196
+ m_codings, m_text = concept_codings(resource.get("method"))
197
+ method = best_label(m_codings, m_text)
198
+ if method:
199
+ out["method"] = method
200
+
201
+ # body site: Observation.bodySite CodeableConcept
202
+ b_codings, b_text = concept_codings(resource.get("bodySite"))
203
+ body_site = best_label(b_codings, b_text)
204
+ if body_site:
205
+ out["body_site"] = body_site
206
+ primary = next((c for c in b_codings if c.code), None)
207
+ if primary:
208
+ out["body_site_code"] = primary.code
209
+ return out
192
210
  return out
193
211
 
194
212
 
@@ -109,17 +109,20 @@ def parse_gap(stmt: ClinicalStatement, pattern: CvxGapPattern) -> Gap:
109
109
 
110
110
  if pattern == CvxGapPattern.RXNORM_ONLY and cc:
111
111
  gap.evidence["rxnorm_code"] = cc.code
112
- gap.evidence["rxnorm_system"] = cc.raw_system
112
+ gap.evidence["rxnorm_system"] = cc.system.value if cc.system else None
113
+ gap.evidence["rxnorm_system_raw"] = cc.raw_system
113
114
  gap.note = ("Use the CDC/NLM CVX↔RxNorm crosswalk — deterministic; "
114
115
  "LLM only if a generic RxCUI maps to multiple CVX valences.")
115
116
  elif pattern == CvxGapPattern.NDC_ONLY and cc:
116
117
  gap.evidence["ndc_code"] = cc.code
117
- gap.evidence["ndc_system"] = cc.raw_system
118
+ gap.evidence["ndc_system"] = cc.system.value if cc.system else None
119
+ gap.evidence["ndc_system_raw"] = cc.raw_system
118
120
  gap.note = ("Use the CDC vaccine NDC↔CVX table (published, free) — "
119
121
  "deterministic mapping from the dispensed NDC to CVX.")
120
122
  elif pattern == CvxGapPattern.LOCAL_CODE_ONLY and cc:
121
123
  gap.evidence["local_code"] = cc.code
122
- gap.evidence["local_system"] = cc.raw_system
124
+ gap.evidence["local_system"] = cc.system.value if cc.system else None
125
+ gap.evidence["local_system_raw"] = cc.raw_system
123
126
  gap.note = ("Crosswalk local immunization code → CVX first; else search "
124
127
  "by normalized vaccine concept (+ valence) → CVX.")
125
128
  elif pattern == CvxGapPattern.BAD_CODE and cc:
@@ -117,7 +117,8 @@ def parse_gap(stmt: ClinicalStatement, pattern: Icd10cmGapPattern) -> Gap:
117
117
  "deterministic; LLM to pick among targets / add specificity.")
118
118
  elif pattern == Icd10cmGapPattern.LOCAL_CODE_ONLY and cc:
119
119
  gap.evidence["local_code"] = cc.code
120
- gap.evidence["local_system"] = (cc.system.value if cc.system else cc.raw_system)
120
+ gap.evidence["local_system"] = (cc.system.value if cc.system else None)
121
+ gap.evidence["local_system_raw"] = cc.raw_system
121
122
  gap.note = ("Crosswalk the source code → ICD-10-CM (ICD-9 → use CMS GEMs); "
122
123
  "else text + qualifier search.")
123
124
  elif pattern == Icd10cmGapPattern.BAD_CODE and cc:
@@ -94,7 +94,10 @@ def parse_local_code_only(stmt: ClinicalStatement, expand_abbrev: bool = False)
94
94
  local = next((c for c in stmt.codings if c.code and c.system != System.LOINC), None)
95
95
  ev = axes.evidence_packet(stmt, expand_abbrev)
96
96
  ev["local_code"] = local.code if local else None
97
- ev["local_system"] = (local.raw_system if local else None)
97
+ # canonical system (format-independent: "CPT" whether from a CCDA OID or a
98
+ # FHIR URI) drives the crosswalk; raw is kept for provenance.
99
+ ev["local_system"] = (local.system.value if local and local.system else None)
100
+ ev["local_system_raw"] = (local.raw_system if local else None)
98
101
  return Gap(
99
102
  type=GapPattern.LOCAL_CODE_ONLY.value,
100
103
  match_key=axes.build_match_key(stmt, expand_abbrev),
@@ -115,7 +115,8 @@ def parse_gap(stmt: ClinicalStatement, pattern: RxnormGapPattern) -> Gap:
115
115
 
116
116
  if pattern == RxnormGapPattern.NDC_ONLY and cc:
117
117
  gap.evidence["ndc_code"] = cc.code
118
- gap.evidence["ndc_system"] = cc.raw_system
118
+ gap.evidence["ndc_system"] = cc.system.value if cc.system else None
119
+ gap.evidence["ndc_system_raw"] = cc.raw_system
119
120
  gap.note = ("Use the NDC↔RxNorm crosswalk (RxNorm release / REST API) — "
120
121
  "deterministic; LLM only if multiple RxCUIs map.")
121
122
  elif pattern in _SOURCE_VOCAB_PATTERN.values() and cc:
@@ -123,15 +124,18 @@ def parse_gap(stmt: ClinicalStatement, pattern: RxnormGapPattern) -> Gap:
123
124
  gap.evidence["source_code"] = cc.code
124
125
  gap.evidence["source_vocab"] = vocab
125
126
  gap.evidence["source_system"] = cc.raw_system
126
- # serialize.py builds a crosswalk hint from local_code/local_system
127
+ # serialize.py builds a crosswalk hint from local_code/local_system.
128
+ # local_system is the canonical vocab (format-independent); raw kept too.
127
129
  gap.evidence["local_code"] = cc.code
128
130
  gap.evidence["local_system"] = vocab
131
+ gap.evidence["local_system_raw"] = cc.raw_system
129
132
  gap.note = (f"Crosswalk {vocab} → RxNorm via the UMLS Metathesaurus "
130
133
  f"(shared CUI) or the vendor's RxNorm map — deterministic; "
131
134
  f"LLM only if the mapping is ambiguous or 1→many.")
132
135
  elif pattern == RxnormGapPattern.LOCAL_CODE_ONLY and cc:
133
136
  gap.evidence["local_code"] = cc.code
134
- gap.evidence["local_system"] = cc.raw_system
137
+ gap.evidence["local_system"] = cc.system.value if cc.system else None
138
+ gap.evidence["local_system_raw"] = cc.raw_system
135
139
  gap.note = ("Crosswalk local/formulary code → RxNorm first; else search by "
136
140
  "ingredient + strength + dose form → SCD/SBD.")
137
141
  elif pattern == RxnormGapPattern.NULLFLAVOR:
@@ -149,7 +149,8 @@ def parse_gap(stmt: ClinicalStatement, pattern: SnomedGapPattern) -> Gap:
149
149
 
150
150
  if pattern == SnomedGapPattern.LOCAL_CODE_ONLY and cc:
151
151
  gap.evidence["local_code"] = cc.code
152
- gap.evidence["local_system"] = cc.raw_system
152
+ gap.evidence["local_system"] = cc.system.value if cc.system else None
153
+ gap.evidence["local_system_raw"] = cc.raw_system
153
154
  gap.note = ("Crosswalk (system, code) → SNOMED first (e.g. CPT/local map); "
154
155
  "else hierarchy-constrained text search, then LLM.")
155
156
  elif pattern == SnomedGapPattern.ICD_ONLY and cc:
@@ -159,7 +160,8 @@ def parse_gap(stmt: ClinicalStatement, pattern: SnomedGapPattern) -> Gap:
159
160
  "LLM only to choose among multiple map targets.")
160
161
  elif pattern == SnomedGapPattern.RXNORM_ONLY and cc:
161
162
  gap.evidence["rxnorm_code"] = cc.code
162
- gap.evidence["rxnorm_system"] = cc.raw_system
163
+ gap.evidence["rxnorm_system"] = cc.system.value if cc.system else None
164
+ gap.evidence["rxnorm_system_raw"] = cc.raw_system
163
165
  gap.note = ("RxNorm-coded allergen (drug allergy) — crosswalk RxNorm → "
164
166
  "SNOMED substance/product via UMLS CUI; LLM only if the "
165
167
  "ingredient maps ambiguously.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aimedicalcoding
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
5
5
  Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
6
6
  License: MIT
@@ -80,7 +80,7 @@ Each record separates four concerns:
80
80
  "search_terms": ["BUN", "Blood Urea Nitrogen"], // text to query (raw + normalized)
81
81
  "filters": { "specimen": "...", "property": "...", "scale": "..." }, // LOINC axis filters
82
82
  "ecl": "< 404684003", // SNOMED: the candidate constraint
83
- "crosswalk": { "from_system": "LOCAL", "from_code": "BUN" }, // try first (local/ICD → target)
83
+ "crosswalk": { "from_system": "CPT", "from_code": "80061" }, // try first (local/ICD/NDC → target). from_system is the CANONICAL system (e.g. "CPT","NDC","ICD-10-CM"), identical for CCDA and FHIR; the raw OID/URI is in evidence as *_system_raw
84
84
  "checkdigit_correction": "2160-0", // BAD_CODE only
85
85
  "match_key": "…", // human summary (do NOT query verbatim)
86
86
  "suggested_action": "…"
@@ -132,7 +132,7 @@ CODE tag's — the other slot's labels are not mixed in.
132
132
  "recovery": {
133
133
  "search_terms": ["BUN", "Blood Urea Nitrogen"],
134
134
  "filters": { "specimen": "Ser/Plas", "property": "MCnc", "scale": "Qn" },
135
- "crosswalk": { "from_system": "2.16.840.1.113883.19.5.99999.1", "from_code": "BUN" },
135
+ "crosswalk": { "from_system": "LOCAL", "from_code": "BUN" },
136
136
  "match_key": "component:BUN | specimen:Ser/Plas | property:MCnc | scale:Qn | method:-",
137
137
  "suggested_action": "crosswalk local code → target; else text + filter search"
138
138
  },
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="aimedicalcoding",
5
- version="0.3.6",
5
+ version="0.3.8",
6
6
  author="Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan",
7
7
  description=(
8
8
  "Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / "
File without changes