hiasgnpsadgd commited on
Commit
9a3640a
·
verified ·
1 Parent(s): 263b46c

Deploy ENCODE with private artifact bucket

Browse files
.dockerignore CHANGED
@@ -4,17 +4,3 @@
4
  !src/
5
  !src/**
6
  src/frontend/demo/
7
- !artifacts/
8
- artifacts/**
9
- !artifacts/part_a_embeddings/
10
- !artifacts/part_a_embeddings/**
11
- !artifacts/part_a_canonical/
12
- !artifacts/part_a_canonical/canonical_phenotypes.jsonl
13
- !artifacts/part_a_canonical/code_descriptions.jsonl
14
- !artifacts/part_a_canonical/algorithm_components.jsonl
15
- !artifacts/part_a_canonical/cipher_links.jsonl
16
- !artifacts/part_a_canonical/med_ingredient_map.jsonl
17
- !artifacts/part_a_canonical/rxcui_ingredient_map.jsonl
18
- !artifacts/part_a_canonical/loinc_terms.jsonl
19
- !artifacts/vidul/
20
- !artifacts/vidul/**
 
4
  !src/
5
  !src/**
6
  src/frontend/demo/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
COMMIT_CONTENTS.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Initial Commit Contents
2
+
3
+ This file defines the deployed contents of the lightweight ENCODE Space and its
4
+ separate private runtime-artifact bucket.
5
+
6
+ ## Application and deployment
7
+
8
+ - `README.md` — project overview and Hugging Face Space metadata.
9
+ - `COMMIT_CONTENTS.md` — this auditable release inventory.
10
+ - `Dockerfile` — CPU-only backend container listening on port 7860.
11
+ - `netlify.toml` and `deploy/` — Netlify build/Drop package that proxies the
12
+ static frontend's API requests to the Hugging Face Space backend.
13
+ - `.dockerignore` — excludes development-only files from container builds.
14
+ - `.gitignore` — excludes raw data, archives, experiments, profiles, and caches.
15
+ - `src/requirements.txt` — pinned Python runtime dependencies.
16
+
17
+ ## Backend
18
+
19
+ - `src/backend/app.py` — FastAPI application and HTTP endpoints.
20
+ - `src/backend/codesearch.py` — medical-code retrieval.
21
+ - `src/backend/retriever.py` — phenotype retrieval using Vidul's fine-tuned
22
+ embeddings.
23
+ - `src/backend/encode.py` — annotation and response handling.
24
+ - `src/backend/graph.py` — phenotype detail graph construction.
25
+ - `src/backend/rxnav.py` — conservative, cached RxNAV/RxNorm completion for
26
+ medication mappings that are absent from packaged artifacts.
27
+ - `src/backend/__init__.py` — backend package marker.
28
+
29
+ ## Frontend
30
+
31
+ - `src/frontend/index.html` — browser application shell.
32
+ - `src/frontend/app.js` — search, review, and annotation behavior.
33
+ - `src/frontend/styles.css` — application styling.
34
+
35
+ ## Private bucket: runtime model and indexes
36
+
37
+ - `artifacts/vidul/BGE_FT_VA/` — Vidul's fine-tuned model weights, tokenizer,
38
+ and configuration.
39
+ - `artifacts/vidul/icd_index/` — diagnosis FAISS index and metadata.
40
+ - `artifacts/vidul/med_index/` — medication FAISS index and metadata.
41
+ - `artifacts/vidul/ndc_index/` — NDC FAISS index and metadata.
42
+ - `artifacts/vidul/labchem_index/` — LOINC/lab FAISS index and metadata.
43
+ - `artifacts/vidul/cpt_index/` — procedure FAISS index and metadata.
44
+ - `artifacts/part_a_embeddings/code.npy`
45
+ - `artifacts/part_a_embeddings/code_meta.json`
46
+ - `artifacts/part_a_embeddings/config.json`
47
+ - `artifacts/part_a_embeddings/metadata.npy`
48
+ - `artifacts/part_a_embeddings/metadata_ids.json`
49
+
50
+ ## Private bucket: runtime phenotype records
51
+
52
+ - `artifacts/part_a_canonical/algorithm_components.jsonl`
53
+ - `artifacts/part_a_canonical/canonical_phenotypes.jsonl`
54
+ - `artifacts/part_a_canonical/cipher_links.jsonl`
55
+ - `artifacts/part_a_canonical/code_descriptions.jsonl`
56
+ - `artifacts/part_a_canonical/loinc_terms.jsonl`
57
+ - `artifacts/part_a_canonical/med_ingredient_map.jsonl`
58
+ - `artifacts/part_a_canonical/rxcui_ingredient_map.jsonl`
59
+
60
+ ## Verification
61
+
62
+ - `tests/test_retrieval.py` — focused tests for the retained dense-retrieval
63
+ path.
64
+ - `tests/test_rxnav_and_deployment.py` — RxNAV provenance and Netlify Drop
65
+ deployment checks.
66
+
67
+ ## Explicitly excluded
68
+
69
+ - Raw VA source data and local data exports.
70
+ - Source ZIPs, including the original application bundle, plus LOINC and RxNorm
71
+ distribution archives.
72
+ - Training, evaluation, reranker, extracted-facet, and profiling outputs.
73
+ - Historical documentation, retired scripts, generated demos, and caches.
74
+ - Local environments, credentials, editor settings, and annotations.
75
+
76
+ The release contains one retrieval path: Vidul's fine-tuned model, prebuilt
77
+ embeddings, and exact dense search. Large runtime assets are stored in
78
+ `hf://buckets/hiasgnpsadgd/encode-artifacts` and mounted read-only at
79
+ `/home/user/app/artifacts`; they are not committed to the Space repository or
80
+ copied into the Docker image.
Dockerfile CHANGED
@@ -19,6 +19,7 @@ ENV HOME=/home/user \
19
  HF_HUB_DISABLE_TELEMETRY=1
20
 
21
  WORKDIR $HOME/app
 
22
 
23
  COPY --chown=user src/requirements.txt ./requirements.txt
24
  RUN python -m pip install --no-cache-dir --upgrade pip && \
@@ -26,11 +27,7 @@ RUN python -m pip install --no-cache-dir --upgrade pip && \
26
  --index-url https://download.pytorch.org/whl/cpu "torch==2.13.0" && \
27
  python -m pip install --no-cache-dir -r requirements.txt
28
 
29
- COPY --chown=user artifacts/vidul ./artifacts/vidul
30
-
31
  COPY --chown=user src ./src
32
- COPY --chown=user artifacts/part_a_embeddings ./artifacts/part_a_embeddings
33
- COPY --chown=user artifacts/part_a_canonical ./artifacts/part_a_canonical
34
 
35
  EXPOSE 7860
36
  STOPSIGNAL SIGTERM
 
19
  HF_HUB_DISABLE_TELEMETRY=1
20
 
21
  WORKDIR $HOME/app
22
+ RUN mkdir -p ./artifacts
23
 
24
  COPY --chown=user src/requirements.txt ./requirements.txt
25
  RUN python -m pip install --no-cache-dir --upgrade pip && \
 
27
  --index-url https://download.pytorch.org/whl/cpu "torch==2.13.0" && \
28
  python -m pip install --no-cache-dir -r requirements.txt
29
 
 
 
30
  COPY --chown=user src ./src
 
 
31
 
32
  EXPOSE 7860
33
  STOPSIGNAL SIGTERM
README.md CHANGED
@@ -13,7 +13,9 @@ short_description: CPU clinical-code and phenotype retrieval with BGE_FT_VA
13
  # ENCODE
14
 
15
  ENCODE is a CPU-only FastAPI application packaged as a Hugging Face Docker
16
- Space. The runtime uses Vidul's `BGE_FT_VA` weights and prebuilt dense indexes.
 
 
17
 
18
  The exact proposed fresh-history file set is listed in
19
  [COMMIT_CONTENTS.md](COMMIT_CONTENTS.md).
@@ -50,22 +52,26 @@ Dockerfile CPU-only Space image
50
  src/backend/ FastAPI and retrieval engines
51
  src/frontend/ Static browser UI
52
  tests/ Focused retrieval tests
53
- artifacts/part_a_embeddings/ Runtime phenotype vectors
54
- artifacts/part_a_canonical/ Runtime phenotype/detail records
55
- artifacts/vidul/ Vidul model and original FAISS indexes
 
56
  ```
57
 
58
  Raw VA inputs, source ZIP archives, LOINC/RxNorm archives, prior experiment
59
  outputs, model-training
60
  artifacts, profiling output, generated demos, retired scripts, and historical
61
- docs are intentionally excluded by `.gitignore`. Large runtime files are marked
62
- for Git LFS in `.gitattributes`.
 
63
 
64
  ## Run locally
65
 
66
  ```bash
67
  docker build -t encode-space .
68
- docker run --rm --memory=16g --cpus=2 -p 7860:7860 encode-space
 
 
69
  ```
70
 
71
  Open <http://localhost:7860>. The readiness endpoint is
@@ -73,19 +79,19 @@ Open <http://localhost:7860>. The readiness endpoint is
73
 
74
  ## Push to a Hugging Face Space
75
 
76
- Install Git LFS before staging the repository:
77
 
78
  ```bash
79
- git lfs install
80
- git add -A
81
- git commit -m "Prepare ENCODE CPU Space"
82
- git remote add space https://huggingface.co/spaces/OWNER/SPACE_NAME
83
- git push space HEAD:main
84
  ```
85
 
86
- Replace `OWNER/SPACE_NAME` with the target Space. Hugging Face reads the YAML
87
- metadata at the top of this file and exposes the container on port 7860. The
88
- image runs one Uvicorn worker so model and index memory are not duplicated.
89
 
90
  ## Deploy the Netlify Drop frontend
91
 
 
13
  # ENCODE
14
 
15
  ENCODE is a CPU-only FastAPI application packaged as a Hugging Face Docker
16
+ Space. The runtime uses Vidul's `BGE_FT_VA` weights and prebuilt dense indexes
17
+ from a private Hugging Face Storage Bucket mounted read-only at
18
+ `/home/user/app/artifacts`.
19
 
20
  The exact proposed fresh-history file set is listed in
21
  [COMMIT_CONTENTS.md](COMMIT_CONTENTS.md).
 
52
  src/backend/ FastAPI and retrieval engines
53
  src/frontend/ Static browser UI
54
  tests/ Focused retrieval tests
55
+ hf://buckets/hiasgnpsadgd/encode-artifacts/
56
+ part_a_embeddings/ Runtime phenotype vectors
57
+ part_a_canonical/ Runtime phenotype/detail records
58
+ vidul/ Vidul model and original FAISS indexes
59
  ```
60
 
61
  Raw VA inputs, source ZIP archives, LOINC/RxNorm archives, prior experiment
62
  outputs, model-training
63
  artifacts, profiling output, generated demos, retired scripts, and historical
64
+ docs are intentionally excluded by `.gitignore`. Runtime artifacts are excluded
65
+ from the Docker build and Space repository so storage is billed and managed
66
+ independently from runtime compute.
67
 
68
  ## Run locally
69
 
70
  ```bash
71
  docker build -t encode-space .
72
+ docker run --rm --memory=16g --cpus=2 \
73
+ -v "$PWD/artifacts:/home/user/app/artifacts:ro" \
74
+ -p 7860:7860 encode-space
75
  ```
76
 
77
  Open <http://localhost:7860>. The readiness endpoint is
 
79
 
80
  ## Push to a Hugging Face Space
81
 
82
+ Mount the private artifact bucket and upload the lightweight Space files:
83
 
84
  ```bash
85
+ hf spaces volumes set hiasgnpsadgd/ENCODE \
86
+ --volume hf://buckets/hiasgnpsadgd/encode-artifacts:/home/user/app/artifacts:ro
87
+ hf upload hiasgnpsadgd/ENCODE . . --type space \
88
+ --include README.md --include COMMIT_CONTENTS.md \
89
+ --include Dockerfile --include .dockerignore --include 'src/**'
90
  ```
91
 
92
+ Hugging Face reads the YAML metadata at the top of this file and exposes the
93
+ container on port 7860. The image runs one Uvicorn worker so model and index
94
+ memory are not duplicated.
95
 
96
  ## Deploy the Netlify Drop frontend
97
 
artifacts/part_a_canonical/algorithm_components.jsonl DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/part_a_canonical/build_report.md DELETED
@@ -1,56 +0,0 @@
1
- # ENCODE Part A Canonical Data Build Report
2
-
3
- Generated by `scripts/build_part_a_canonical.py`.
4
-
5
- ## Inputs
6
-
7
- - `Data source/all_phenotypes.zip`
8
- - `Data source/VA_CDW_Code_dimensions_tables (1).zip`
9
-
10
- ## Output Contract
11
-
12
- - Schema version: `encode_part_a_v1`
13
- - Canonical phenotype records: `canonical_phenotypes.jsonl`
14
- - Embedding documents: `embedding_documents.jsonl`
15
- - Embedding document chunk cap: 4000 characters
16
-
17
- ## Profile
18
-
19
- - Phenotypes: 8013
20
- - Embedding documents: 36681
21
- - Missing phenotype descriptions: 1665
22
- - Missing algorithm descriptions: 0
23
- - CIPHER enum sidecar parsed cleanly: False
24
-
25
- ## Embedding Documents By View
26
-
27
- - code_evidence: 28668
28
- - phenotype_metadata: 8013
29
-
30
- ## Code Entries By Type
31
-
32
- - Healthcare Common Procedure Coding System (HCPCS): 3
33
- - ICD-10 Diagnostic Codes: 6468
34
- - ICD-10 Procedure Codes: 21
35
- - ICD-9 Diagnostic Codes: 5995
36
- - ICD-9 Procedure Codes: 25
37
- - Laboratory Tests: 235
38
- - Medications: 156
39
- - Observational Medical Outcomes Partnership (OMOP) Concept: 28
40
- - Other: 1690
41
- - SNOMED CT, US Edition: 276
42
- - Text snippets: 1802
43
- - VA Clinic Stop Codes: 44
44
-
45
- ## Code Label Status
46
-
47
- - exact: 395795
48
- - label_missing: 10186
49
- - prefix_expanded: 86799
50
- - unsupported_code_system: 140337
51
-
52
- ## Notes
53
-
54
- - `prefix_expanded` labels are retrieval evidence only; the original CIPHER code is preserved as provenance.
55
- - Unsupported medication/lab/SNOMED/OMOP/CUI labels remain explicit instead of being guessed.
56
- - The malformed enum sidecar should still be replaced for production use.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/part_a_canonical/canonical_phenotypes.jsonl DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:5062bcec9b228a7962a9d0652a4ab9047d8b8b4f7965e35be0cc7c85222e9c73
3
- size 198475995
 
 
 
 
artifacts/part_a_canonical/cipher_links.jsonl DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/part_a_canonical/code_descriptions.jsonl DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/part_a_canonical/code_descriptions_report.md DELETED
@@ -1,32 +0,0 @@
1
- # Code Description Resolution Report (professor feedback 1.1)
2
-
3
- Generated by `scripts/resolve_code_descriptions.py`. Sidecar: `code_descriptions.jsonl`.
4
-
5
- - Unsupported code occurrences in corpus: **140,337**
6
- - Distinct unsupported codes: **77,233**
7
- - Distinct codes resolved to a description: **4,756** (6%)
8
-
9
- ## Resolved by source
10
-
11
- - `va_dim_exact`: 2,166
12
- - `cipher_name`: 2,108
13
- - `va_labname`: 482
14
-
15
- ## Left unresolved (honest gaps)
16
-
17
- - `study_specific`: 66,856
18
- - `needs_vocab:OMOP`: 4,322
19
- - `needs_vocab:LOINC`: 806
20
- - `needs_vocab:VA_STOP`: 255
21
- - `needs_vocab:NDC`: 238
22
-
23
- ## Notes
24
-
25
- - `va_dim_exact`: code matched a VA ICD/CPT dictionary (often a code CIPHER
26
- filed under "Other").
27
- - `cipher_name`: Medication codes are the drug name itself (generic/brand).
28
- - `va_labname`: Lab `%PATTERN%` SQL-LIKE test-name, wildcards stripped.
29
- - `needs_vocab:*`: resolvable once that vocabulary is added (LOINC, OMOP/Athena,
30
- RxNorm/NDC, VA stop-code table) — none require guessing.
31
- - `study_specific`: author-defined variables (e.g. `PDI`, `hPDI`) with no
32
- standard-vocabulary description; surfaced as-is, never fabricated.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/part_a_canonical/embedding_documents.jsonl DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:97a6a429278e07ce8a9be4eb5b66245d76f5e763a17f89972c9cd31487c01f37
3
- size 84233128
 
 
 
 
artifacts/part_a_canonical/loinc_terms.jsonl DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:dd0f1d9e4571f1f3fc0fda925870a3e0589cf96a9fcbb7475c9558a4abe17124
3
- size 17205441
 
 
 
 
artifacts/part_a_canonical/med_ingredient_map.jsonl DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/part_a_canonical/rxcui_ingredient_map.jsonl DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/part_a_embeddings/code.npy DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:6bdc926e8827f2e05e22c73d1b06349f707fad8f85f83d6c9f57939d80d456a6
3
- size 58712192
 
 
 
 
artifacts/part_a_embeddings/code_meta.json DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/part_a_embeddings/config.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "model": "artifacts/models/bge_ft_va",
3
- "pooling": "cls",
4
- "query_prefix": "",
5
- "doc_prefix": "",
6
- "max_seq_length": 512,
7
- "dim": 1024,
8
- "n_metadata": 8013,
9
- "n_code": 28668
10
- }
 
 
 
 
 
 
 
 
 
 
 
artifacts/part_a_embeddings/metadata.npy DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:add2b7bcd22e7566f8348a6d6dce7e9a1de47fb2ccbe898febfccf9a6a44afec
3
- size 16410752
 
 
 
 
artifacts/part_a_embeddings/metadata_ids.json DELETED
@@ -1 +0,0 @@
1
- [2678, 2017, 2679, 2632, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2070, 2073, 2074, 2075, 2076, 2077, 2078, 2079, 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107, 2108, 2109, 2110, 2111, 2112, 2113, 2114, 2115, 2116, 2117, 2118, 2119, 2120, 2121, 2122, 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2133, 2134, 2135, 2136, 2137, 2138, 2139, 2140, 2141, 2142, 2143, 2144, 2146, 2148, 2149, 2150, 2151, 2153, 2155, 2156, 2157, 2158, 2160, 2161, 2162, 2163, 2165, 2166, 2167, 2168, 2169, 2170, 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, 2179, 2180, 2181, 2182, 2184, 2185, 2186, 2187, 2188, 2189, 2190, 2191, 2192, 2193, 2194, 2195, 2196, 2197, 2198, 2199, 2200, 2201, 2202, 2203, 2204, 2205, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2220, 2221, 2222, 2223, 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, 2232, 2233, 2234, 2236, 2237, 2238, 2239, 2240, 2241, 2242, 2243, 2244, 2245, 2246, 2247, 2248, 2249, 2250, 2251, 2252, 2253, 2254, 2255, 2256, 2257, 2258, 2259, 2260, 2262, 2263, 2264, 2265, 2266, 2267, 2269, 2270, 2271, 2272, 2273, 2274, 2275, 2276, 2277, 2278, 2279, 2280, 2281, 2283, 2284, 2285, 2286, 2288, 2289, 2290, 2291, 2292, 2293, 2294, 2295, 2296, 2297, 2298, 2299, 2301, 2302, 2303, 2304, 2305, 2306, 2307, 2308, 2309, 2310, 2311, 2312, 2313, 2314, 2315, 2316, 2317, 2321, 2322, 2323, 2324, 2325, 2326, 2327, 2328, 2329, 2330, 2331, 2332, 2333, 2334, 2335, 2336, 2337, 2338, 2339, 2340, 2341, 2342, 2343, 2344, 2345, 2346, 2347, 2348, 2349, 2350, 2351, 2352, 2353, 2354, 2355, 2356, 2357, 2358, 2359, 2360, 2361, 2362, 2363, 2364, 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2381, 2382, 2383, 2384, 2385, 2386, 2387, 2388, 2389, 2390, 2391, 2392, 2393, 2394, 2395, 2396, 2397, 2399, 2401, 2402, 2404, 2405, 2407, 2408, 2409, 2411, 2412, 2413, 2414, 2415, 2416, 2417, 2418, 2419, 2420, 2422, 2424, 2425, 2427, 2428, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2437, 2438, 2439, 2440, 2441, 2442, 2443, 2444, 2445, 2446, 2447, 2448, 2449, 2450, 2451, 2452, 2453, 2454, 2455, 2456, 2457, 2458, 2459, 2460, 2461, 2462, 2464, 2465, 2466, 2467, 2468, 2469, 2471, 2472, 2474, 2475, 2476, 2481, 2482, 2483, 2485, 2486, 2487, 2488, 2489, 2490, 2491, 2492, 2493, 2494, 2495, 2496, 2497, 2498, 2501, 2502, 2503, 2504, 2505, 2506, 2507, 2508, 2509, 2510, 2511, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2522, 2523, 2524, 2525, 2526, 2527, 2528, 2529, 2530, 2531, 2532, 2534, 2535, 2536, 2538, 2539, 2540, 2541, 2542, 2543, 2544, 2545, 2546, 2547, 2548, 2549, 2550, 2551, 2552, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, 2565, 2566, 2567, 2568, 2569, 2570, 2571, 2572, 2573, 2574, 2576, 2577, 2578, 2579, 2580, 2581, 2583, 2585, 2586, 2587, 2588, 2589, 2591, 2592, 2593, 2594, 2595, 2596, 2597, 2599, 2600, 2601, 2603, 2604, 2605, 2606, 2607, 2608, 2609, 2610, 2611, 2612, 2613, 2614, 2615, 2616, 2617, 2618, 2619, 2620, 2621, 2622, 2623, 2624, 2625, 2626, 2627, 2628, 2629, 2630, 2631, 2633, 2634, 2635, 2636, 2637, 2638, 2639, 2640, 2641, 2642, 2644, 2645, 2646, 2647, 2650, 2651, 2652, 2653, 2654, 2655, 2656, 2657, 2658, 2659, 2660, 2661, 2662, 2663, 2664, 2665, 2666, 2667, 2668, 2669, 2670, 2671, 2672, 2673, 2674, 2675, 2676, 2677, 2680, 2681, 2682, 2683, 2684, 2686, 2687, 2688, 2689, 2690, 2691, 2692, 2693, 2694, 2695, 2696, 2697, 2698, 2699, 2700, 2701, 2702, 2703, 2704, 2706, 2707, 2708, 2709, 2710, 2711, 2712, 2713, 2725, 2727, 2731, 2732, 2733, 2734, 2735, 2736, 2737, 2738, 2739, 2740, 2741, 2742, 2743, 2744, 2745, 2746, 2747, 2748, 2749, 2750, 2752, 2753, 2754, 2755, 2756, 2758, 2759, 2760, 2761, 2762, 2763, 2765, 2767, 2768, 2769, 2770, 2771, 2772, 2773, 2774, 2775, 2776, 2778, 2779, 2780, 2781, 2782, 2783, 2784, 2786, 2787, 2788, 2789, 2790, 2791, 2792, 2793, 2794, 2795, 2797, 2798, 2799, 2800, 2801, 2802, 2803, 2804, 2805, 2806, 2807, 2808, 2809, 2810, 2811, 2812, 2813, 2814, 2815, 2816, 2817, 2818, 2819, 2821, 2822, 2823, 2824, 2825, 2826, 2827, 2828, 2829, 2830, 2831, 2832, 2833, 2834, 2835, 2836, 2837, 2838, 2839, 2840, 2841, 2842, 2843, 2844, 2846, 2847, 2848, 2849, 2850, 2851, 2852, 2853, 2854, 2855, 2856, 2857, 2858, 2859, 2860, 2861, 2862, 2863, 2864, 2865, 2866, 2867, 2868, 2869, 2870, 2871, 2872, 2873, 2874, 2875, 2876, 2877, 2878, 2879, 2880, 2881, 2882, 2884, 2885, 2886, 2888, 2889, 2890, 2891, 2892, 2893, 2894, 2896, 2897, 2898, 2899, 2900, 2901, 2902, 2903, 2904, 2905, 2906, 2907, 2911, 2913, 2914, 2915, 2917, 2918, 2919, 2920, 2921, 2922, 2924, 2925, 2926, 2927, 2928, 2930, 2931, 2932, 2933, 2934, 2935, 2936, 2937, 2938, 2939, 2940, 2941, 2942, 2943, 2945, 2946, 2948, 2949, 2950, 2952, 2953, 2954, 2955, 2956, 2958, 2959, 2960, 2961, 2962, 2963, 2964, 2965, 2966, 2967, 2969, 2970, 2971, 2972, 2973, 2974, 2478, 2533, 2069, 2071, 2072, 2145, 2147, 2152, 2154, 2159, 2164, 2183, 2218, 2219, 2235, 2268, 2261, 2282, 2287, 2300, 2318, 2319, 2320, 2365, 2379, 2380, 2398, 2400, 2403, 2406, 2410, 2421, 2423, 2426, 2463, 2470, 2477, 2479, 2480, 2484, 2499, 2500, 2521, 2537, 2564, 2575, 2582, 2584, 2590, 2598, 2602, 2643, 2648, 2649, 2685, 2705, 2714, 2715, 2717, 2716, 2718, 2719, 2720, 2721, 2722, 2723, 2724, 2726, 2728, 2729, 2730, 2751, 2757, 2764, 2766, 2785, 2777, 2796, 2820, 2845, 2883, 2887, 2895, 2908, 2910, 2909, 2912, 2957, 2968, 2916, 2923, 2929, 2944, 2947, 2951, 2473, 2089, 3935, 4117, 4065, 4011, 4035, 3509, 3334, 3962, 3508, 3355, 3209, 4045, 3972, 3987, 3948, 3960, 3999, 4001, 4002, 4003, 4004, 4005, 4006, 4008, 4009, 4010, 4012, 4013, 4014, 4015, 4016, 4017, 4019, 4020, 4021, 4022, 4023, 4025, 4026, 4027, 4028, 4030, 4031, 4032, 4034, 4037, 4038, 4039, 4040, 4041, 4044, 4046, 4047, 4049, 4050, 4051, 4052, 4054, 4055, 4056, 4057, 4058, 4059, 4060, 4062, 4063, 4064, 4066, 4067, 4069, 4070, 4042, 4072, 4073, 4074, 4075, 4077, 4078, 4079, 4080, 4081, 4082, 4083, 4084, 4085, 4086, 4088, 4089, 4090, 4093, 4094, 4095, 4096, 4098, 4099, 4100, 4101, 4102, 4103, 4104, 4105, 4106, 4107, 4108, 4109, 4110, 4111, 4112, 4114, 4115, 4116, 4118, 4119, 4121, 4122, 4123, 4124, 3440, 4126, 4127, 3443, 3442, 3449, 3495, 3526, 3560, 3639, 4024, 4120, 3444, 3446, 3447, 3450, 3031, 3036, 3037, 3052, 3056, 4043, 3069, 3074, 3132, 3589, 3441, 3448, 3615, 3158, 3169, 3174, 3176, 3659, 3661, 3171, 3179, 3167, 3189, 3190, 3198, 3199, 3200, 3210, 3224, 3260, 3178, 3177, 3235, 3239, 3246, 3251, 3255, 3276, 3303, 3308, 3310, 3314, 3340, 3343, 3261, 3401, 3437, 3688, 3700, 3732, 3734, 3737, 3346, 3378, 3390, 3439, 3674, 3675, 3676, 3678, 3736, 3738, 3739, 3740, 3750, 2975, 2976, 2977, 2978, 3755, 2980, 2981, 2982, 2983, 2984, 2985, 2986, 2987, 2988, 2989, 2990, 2991, 2992, 2993, 2994, 2995, 2996, 2997, 2998, 2999, 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3757, 3032, 3033, 3034, 3035, 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3053, 3054, 3055, 3057, 3058, 3059, 3060, 3061, 3062, 3063, 3064, 3065, 3066, 3067, 3068, 3070, 3071, 3072, 3073, 3075, 3076, 3077, 3078, 3079, 3080, 3081, 3082, 3083, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3765, 3797, 3093, 3094, 3095, 3096, 3097, 3098, 3099, 3100, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3123, 3124, 3125, 3126, 3127, 3128, 3129, 3130, 3131, 3133, 3134, 3135, 3136, 3137, 3138, 3139, 3140, 3141, 3142, 3143, 3144, 3145, 3146, 3147, 3148, 3149, 3150, 3151, 3152, 3153, 3154, 3155, 3156, 3157, 3159, 3160, 3161, 3162, 3163, 3164, 3165, 3166, 3168, 3170, 3172, 3173, 3175, 3180, 3181, 3182, 3183, 3184, 3185, 3186, 3187, 3188, 3191, 3192, 3193, 3194, 3195, 3196, 3197, 3201, 3202, 3203, 3204, 3205, 3206, 3207, 3208, 3211, 3212, 3213, 3214, 3215, 3216, 3217, 3218, 3219, 3220, 3221, 3222, 3223, 3225, 3226, 3227, 3228, 3229, 3230, 3231, 3232, 3233, 3234, 3236, 3237, 3238, 3240, 3241, 3242, 3243, 3244, 3245, 3247, 3248, 3249, 3250, 3252, 3253, 3254, 3256, 3257, 3258, 3259, 3262, 3263, 3264, 3265, 3266, 3267, 3268, 3269, 3270, 3271, 3272, 3273, 3274, 3275, 3277, 3278, 3279, 3280, 3281, 3282, 3283, 3284, 3285, 3286, 3287, 3288, 3289, 3290, 3291, 3292, 3293, 3294, 3295, 3296, 3297, 3298, 3299, 3300, 3301, 3302, 3304, 3305, 3306, 3307, 3309, 3311, 3312, 3313, 3315, 3316, 3317, 3318, 3319, 3320, 3321, 3322, 3323, 3324, 3325, 3326, 3327, 3328, 3329, 3330, 3331, 3332, 3333, 3335, 3336, 3337, 3338, 3339, 3341, 3342, 3344, 3345, 3347, 3348, 3349, 3350, 3351, 3352, 3353, 3354, 3356, 3357, 3358, 3359, 3360, 3361, 3362, 3363, 3364, 3365, 3366, 3367, 3368, 3369, 3370, 3371, 3372, 3373, 3374, 3375, 3376, 3377, 3379, 3380, 3381, 3382, 3383, 3384, 3385, 3386, 3387, 3388, 3389, 3391, 3392, 3393, 3394, 3395, 3396, 3397, 3398, 3399, 3400, 3402, 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, 3411, 3412, 3413, 3414, 3415, 3416, 3417, 3418, 3419, 3420, 3421, 3422, 3423, 3424, 3425, 3426, 3427, 3428, 3429, 3430, 3431, 3432, 3433, 3434, 3435, 3436, 3438, 3445, 3451, 3452, 3453, 3454, 3455, 3456, 3457, 3458, 3459, 3460, 3461, 3462, 3463, 3464, 3465, 3466, 3467, 3468, 3469, 3470, 3471, 3472, 3473, 3474, 3475, 3476, 3477, 3478, 3479, 3480, 3481, 3482, 3483, 3484, 3485, 3486, 3487, 3488, 3489, 3490, 3491, 3492, 3493, 3494, 3496, 3497, 3498, 3499, 3500, 3501, 3502, 3503, 3504, 3505, 3506, 3507, 3813, 3824, 3510, 3511, 3512, 3513, 3514, 3515, 3516, 3517, 3518, 3519, 3520, 3521, 3522, 3523, 3524, 3525, 3527, 3528, 3529, 3530, 3531, 3532, 3533, 3534, 3535, 3536, 3537, 3538, 3539, 3540, 3541, 3542, 3543, 3544, 3545, 3546, 3547, 3548, 3549, 3550, 3551, 3552, 3553, 3554, 3555, 3556, 3557, 3558, 3559, 3561, 3562, 3563, 3828, 3651, 3122, 3613, 3626, 3565, 3566, 3567, 3568, 3569, 3570, 3571, 3572, 3573, 3574, 3575, 3576, 3577, 3578, 3579, 3580, 3581, 3582, 3583, 3584, 3585, 3586, 3587, 3588, 3590, 3591, 3592, 3593, 3594, 3595, 3596, 3597, 3598, 3599, 3600, 3601, 3602, 3603, 3604, 3605, 3606, 3607, 3608, 3609, 3610, 3611, 3612, 3614, 3616, 3617, 3618, 3619, 3620, 3621, 3622, 3623, 3624, 3625, 3627, 3628, 3629, 3630, 3631, 3632, 3633, 3634, 3635, 3636, 3637, 3638, 3640, 3641, 3642, 3643, 3644, 3645, 3646, 3647, 3648, 3649, 3650, 3652, 3653, 3654, 3655, 3656, 3657, 3658, 3660, 3662, 3663, 3664, 3665, 3666, 3667, 3668, 3669, 3670, 3672, 3673, 3677, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3689, 3690, 3691, 3692, 3693, 3694, 3695, 3696, 3697, 3698, 3699, 3701, 3702, 3703, 3704, 3705, 3706, 3707, 3708, 3709, 3710, 3711, 3712, 3713, 3714, 3715, 3716, 3717, 3718, 3719, 3720, 3721, 3722, 3723, 3724, 3725, 3726, 3727, 3728, 3729, 3730, 3731, 3733, 3735, 3741, 3742, 3743, 3744, 3745, 3746, 3747, 3748, 3749, 3751, 3752, 3753, 3754, 3756, 3758, 3759, 3760, 3761, 3763, 3764, 3766, 3767, 3768, 3769, 3770, 3771, 3772, 3773, 3774, 3775, 3776, 3777, 3778, 3779, 3780, 3781, 3782, 3783, 3784, 3785, 3786, 3787, 3788, 3789, 3790, 3791, 3792, 3793, 3794, 3795, 3796, 3798, 3799, 3800, 3801, 3802, 3803, 3804, 3805, 3806, 3807, 3808, 3809, 3810, 3811, 3812, 3814, 3815, 3816, 3817, 3818, 3819, 3820, 3821, 3822, 3823, 3825, 3826, 3827, 3829, 3830, 3831, 3832, 3833, 3834, 3835, 3836, 3837, 3838, 3839, 4091, 4113, 4068, 4029, 4061, 3998, 4071, 3671, 4097, 4048, 2979, 3030, 3993, 3994, 3995, 3762, 3564, 4125, 4053, 4007, 3934, 3051, 4033, 4000, 4235, 4752, 4351, 4377, 4374, 12468, 4193, 4224, 4294, 12469, 8628, 8629, 8630, 8631, 8632, 8633, 8634, 8635, 8636, 12471, 8637, 4150, 4276, 12472, 4269, 4129, 4130, 4131, 4132, 12474, 4135, 4136, 4137, 4139, 4140, 4141, 4143, 4144, 4145, 4147, 4148, 4149, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4175, 12477, 4178, 4179, 4180, 4181, 4182, 4183, 4185, 4186, 4187, 4188, 4190, 4191, 4192, 4195, 4196, 4198, 4199, 4200, 4201, 4203, 4204, 4205, 4206, 4208, 4209, 4210, 4211, 4212, 4213, 4214, 4216, 4217, 4218, 4219, 4220, 4221, 4222, 4223, 4225, 4226, 4229, 4230, 4231, 4232, 4233, 4234, 4236, 4237, 4238, 4239, 4240, 4241, 4242, 4243, 4244, 4245, 4246, 4247, 4248, 4249, 4250, 4251, 4252, 4253, 4254, 4255, 4256, 4257, 4258, 4259, 4260, 4261, 4262, 4263, 4264, 4265, 4266, 4268, 4270, 4271, 4272, 4273, 4274, 4275, 4277, 4278, 4279, 4280, 4281, 4282, 4283, 4286, 4287, 4288, 4289, 4290, 4291, 4292, 4295, 4296, 4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4305, 4306, 4307, 4308, 4309, 4310, 4311, 4312, 4313, 4314, 4315, 4316, 4317, 4318, 4319, 4320, 4321, 4322, 4323, 4325, 4326, 4327, 4330, 4331, 4333, 4176, 4334, 4335, 4336, 4337, 4338, 4339, 4340, 4341, 4343, 4344, 4345, 4346, 4347, 4348, 4350, 4352, 4354, 12478, 4356, 4357, 4358, 4359, 4360, 4362, 4363, 4364, 4365, 4366, 4367, 4368, 4369, 4370, 4371, 4372, 4373, 4375, 4376, 4378, 4380, 4381, 4382, 4383, 4384, 4385, 4386, 4387, 4388, 4389, 4390, 4391, 4392, 4393, 4394, 4395, 4396, 4397, 4399, 4400, 4401, 4402, 4403, 4404, 4405, 4406, 4407, 4409, 4410, 4411, 4412, 4413, 4414, 4415, 4416, 4417, 4194, 12479, 4408, 4215, 4328, 4722, 12480, 12481, 4128, 4349, 12482, 4267, 12483, 12484, 12485, 4228, 4167, 12487, 4749, 12488, 12489, 12490, 12491, 12492, 10513, 12493, 10526, 10527, 10533, 10534, 10535, 10536, 10537, 4353, 4750, 10515, 10523, 4207, 4739, 4751, 4284, 10586, 10588, 10589, 10590, 12495, 12496, 12497, 12498, 12501, 12502, 12506, 12507, 12514, 12515, 12516, 12518, 12519, 12522, 12523, 12524, 12525, 12526, 12527, 12529, 12530, 12531, 12532, 12533, 12534, 12535, 12536, 12537, 12538, 12539, 12540, 12541, 12542, 12543, 12544, 12545, 12546, 12547, 12549, 12556, 12557, 12562, 12563, 12565, 12566, 12569, 12570, 12573, 12576, 12577, 12578, 12579, 12580, 12581, 12582, 12583, 12584, 12585, 12590, 12591, 12592, 12594, 12595, 12596, 12597, 12598, 12599, 12600, 12602, 12603, 12604, 12605, 12609, 12611, 12617, 12618, 12621, 12622, 12623, 12624, 12625, 12626, 12627, 12630, 12631, 12632, 12633, 12634, 12635, 12636, 12637, 12638, 12639, 12640, 12641, 12642, 12643, 12644, 12645, 12646, 12648, 12650, 12651, 12652, 12653, 12654, 12659, 12660, 12662, 12663, 12667, 12668, 12669, 12670, 12671, 12672, 12673, 12674, 12675, 12676, 12677, 12678, 12679, 12680, 12681, 12682, 12686, 12687, 12688, 12689, 12690, 12691, 12693, 12694, 12695, 12697, 12698, 12699, 12703, 12704, 12705, 12706, 12707, 12708, 12709, 12710, 12712, 12713, 12714, 12716, 12718, 12722, 12724, 12725, 12726, 12727, 12728, 12729, 12730, 12731, 12733, 12734, 12735, 12736, 12737, 12738, 12739, 12740, 12741, 12742, 12744, 12745, 12747, 12749, 12750, 12752, 12753, 12755, 12757, 12761, 12762, 12764, 12766, 12767, 12770, 12772, 12773, 12774, 12777, 12780, 12783, 12784, 12785, 12788, 12791, 12793, 12795, 12796, 12797, 12798, 12804, 12805, 12809, 12811, 12813, 12817, 12818, 12819, 12821, 12822, 12823, 12824, 12825, 12827, 12828, 12829, 12830, 12831, 12832, 12833, 12834, 12837, 12838, 12839, 12840, 12841, 12842, 12843, 12844, 12845, 12846, 12847, 12848, 12849, 12850, 12851, 12852, 12854, 12857, 12858, 12859, 12861, 12863, 12864, 12865, 12866, 12867, 12868, 12872, 12874, 12875, 12876, 12877, 12878, 12879, 12880, 12881, 12882, 12883, 12885, 12887, 12891, 12892, 4738, 12894, 12528, 4177, 4737, 12895, 4332, 12896, 12897, 12898, 4146, 4293, 4285, 4723, 4725, 12901, 10522, 10519, 10524, 10521, 10516, 10520, 10518, 10517, 12936, 12906, 12911, 12912, 4202, 4324, 4138, 12913, 12914, 12915, 12918, 12919, 12920, 12922, 12923, 12924, 12925, 12926, 12927, 12930, 12931, 4189, 4197, 12932, 12933, 13010, 13014, 13017, 4355, 4329, 13018, 13019, 13020, 13021, 13022, 13023, 13024, 13025, 13026, 13027, 13028, 13029, 13030, 13032, 13033, 13034, 13035, 13037, 13039, 13043, 13044, 13045, 13046, 13047, 13048, 13049, 13050, 13053, 13055, 13056, 13057, 13058, 13059, 13060, 13062, 13065, 13066, 13067, 13070, 13071, 4721, 4133, 13072, 13073, 13074, 13075, 13076, 13078, 13079, 13080, 13081, 13082, 13083, 13084, 13085, 13086, 13087, 13088, 13089, 13092, 13094, 13095, 13096, 13097, 13098, 13099, 13100, 13101, 13103, 13104, 13105, 13106, 13107, 13108, 13109, 13110, 13111, 13112, 13114, 13115, 13116, 13117, 13118, 13119, 13121, 13122, 13123, 13124, 13125, 13126, 13127, 13128, 13051, 13129, 13130, 13131, 13132, 13134, 13135, 13136, 13137, 13138, 13139, 12470, 12473, 12475, 12476, 12486, 12494, 12499, 12500, 12503, 12504, 12505, 12508, 12509, 12510, 12511, 12512, 12513, 12517, 12520, 12521, 12548, 12550, 12551, 12552, 12553, 12554, 12555, 12558, 12559, 12560, 12561, 12564, 12567, 12568, 12571, 12572, 12574, 12575, 12586, 12587, 12588, 12589, 12593, 12601, 12606, 12607, 12608, 12610, 12612, 12613, 12614, 12615, 12616, 12619, 12620, 12628, 12629, 12647, 12649, 12655, 12656, 12657, 12658, 12661, 12664, 12665, 12666, 12683, 12684, 12685, 12692, 12696, 12700, 12701, 12702, 12711, 12715, 12717, 12719, 12720, 12721, 12723, 12732, 12743, 12746, 12748, 12751, 12754, 12756, 12758, 12759, 12760, 12763, 12765, 12768, 12769, 12771, 12775, 12776, 12778, 12779, 12781, 12782, 12786, 12787, 12789, 12790, 12792, 12794, 12799, 12800, 12801, 12802, 12803, 12806, 12807, 12808, 12810, 12812, 12814, 12815, 12816, 12820, 12826, 12835, 12836, 12853, 12855, 12856, 12860, 12862, 12869, 12870, 12871, 12873, 12884, 12886, 12888, 12889, 12890, 12893, 12899, 12900, 12902, 12903, 12904, 12905, 12907, 12908, 12909, 12910, 12916, 12917, 12921, 12928, 12929, 12934, 12935, 12937, 12938, 12939, 12940, 12941, 12942, 12943, 12944, 12945, 12946, 12947, 12948, 12949, 12950, 12951, 12952, 12953, 12954, 12955, 12956, 12957, 12958, 12959, 12960, 12961, 12962, 12963, 12964, 12965, 12966, 12967, 12968, 12969, 12970, 12971, 12972, 12973, 12974, 12975, 12976, 12977, 12978, 12979, 12980, 12981, 12982, 12983, 12984, 12985, 12986, 12987, 12988, 12989, 12990, 12991, 12992, 12993, 12994, 12995, 12996, 12997, 12998, 12999, 13000, 13001, 13002, 13003, 13004, 13005, 13006, 13007, 13008, 13009, 13011, 13012, 13013, 13015, 13016, 13031, 13036, 13038, 13040, 13041, 13042, 13052, 13054, 13061, 13063, 13064, 13068, 13069, 13077, 13090, 13091, 13093, 13102, 13113, 13120, 13133, 4142, 4184, 4361, 13610, 13822, 13611, 13612, 13615, 13617, 13618, 13823, 13825, 13619, 13826, 13620, 13827, 13621, 13622, 13828, 13829, 13623, 13624, 13625, 13626, 13627, 13831, 13629, 13832, 13833, 13631, 13632, 13834, 13633, 13634, 13635, 13835, 13636, 13637, 13638, 13639, 13836, 13640, 13641, 13642, 13837, 13838, 13643, 13644, 13645, 13839, 13646, 13647, 13840, 13841, 13649, 13843, 13651, 13652, 13653, 13654, 13655, 13844, 13657, 13658, 13659, 13845, 13660, 13661, 13662, 13663, 13846, 13664, 13847, 13665, 13666, 13670, 13672, 13673, 13674, 13848, 13675, 13676, 13849, 13850, 13852, 13854, 13855, 13677, 13856, 13678, 13857, 13679, 13680, 13858, 13681, 13859, 13682, 13860, 13685, 13688, 13861, 13689, 13690, 13691, 13692, 13693, 13694, 13696, 13862, 13697, 13698, 13699, 13863, 13700, 13701, 13704, 13864, 13865, 13866, 13867, 13706, 13868, 13869, 13707, 13708, 13709, 13870, 13710, 13711, 13712, 13715, 13871, 13872, 13873, 13874, 13875, 13716, 13885, 13888, 13892, 13898, 13899, 13903, 13905, 13717, 13906, 13907, 13908, 13915, 13916, 13918, 13919, 13929, 13931, 13932, 13718, 13719, 13936, 13720, 13721, 13942, 13722, 13723, 13724, 13725, 13952, 13726, 13727, 13728, 13960, 13729, 13730, 13731, 13733, 13734, 13735, 13961, 13736, 13737, 13739, 13740, 13741, 13742, 13962, 13963, 13970, 13971, 13974, 13975, 13981, 13985, 13989, 13744, 13745, 13746, 13747, 13748, 13749, 13750, 13752, 13990, 13754, 13756, 13757, 13758, 13759, 13760, 13992, 13767, 13768, 13769, 14001, 13772, 13774, 13775, 14007, 14010, 13779, 13781, 13783, 14024, 14025, 14027, 14028, 14033, 14034, 13788, 13789, 13792, 13795, 13796, 13797, 13798, 13799, 13800, 14038, 13801, 13802, 13803, 13805, 13808, 13809, 13810, 13811, 13813, 13814, 13815, 13816, 13817, 13818, 13824, 13830, 13842, 13851, 14082, 13853, 14084, 14089, 14090, 14092, 13876, 14039, 13877, 13878, 13879, 13880, 13881, 13882, 13883, 13884, 13886, 13887, 13889, 13890, 13891, 13893, 13894, 13895, 13896, 14041, 13897, 13900, 13901, 13902, 13904, 13909, 13910, 14050, 13911, 14054, 13912, 13913, 14056, 13914, 14058, 14061, 14062, 13917, 13920, 13921, 13922, 13923, 14079, 13924, 13925, 13926, 14080, 13927, 13928, 13930, 13933, 14093, 13934, 13935, 13937, 13938, 13939, 13940, 13941, 13943, 13944, 13945, 13946, 13947, 13948, 14100, 14101, 14103, 14105, 14106, 14108, 14109, 14110, 14111, 14112, 14114, 14115, 14116, 14119, 14120, 14123, 14124, 14125, 13949, 14126, 14127, 14128, 14130, 14131, 14133, 14137, 14138, 14139, 13950, 13951, 13953, 13147, 13148, 13151, 13152, 13153, 13154, 13155, 13156, 13157, 13158, 13159, 13160, 13161, 13162, 13163, 13164, 13165, 13168, 13169, 13170, 13171, 13172, 13173, 13174, 13175, 13176, 13178, 13179, 13180, 13954, 13955, 13956, 13957, 13958, 13959, 13964, 13965, 13966, 13967, 13968, 13969, 13972, 13973, 13976, 13977, 13978, 13979, 13980, 13982, 13983, 13984, 13986, 13987, 13988, 13991, 13993, 13994, 13995, 13996, 13997, 13998, 13999, 14000, 13181, 13182, 13184, 13185, 13186, 13187, 14002, 14003, 14004, 14005, 14006, 14008, 14009, 14011, 14012, 14013, 14014, 14015, 14016, 14017, 14018, 14019, 14020, 14021, 14022, 14023, 14026, 14029, 14030, 14031, 14032, 14035, 14036, 14037, 14040, 14042, 14043, 14044, 14045, 14046, 14047, 14048, 14049, 14051, 14052, 14053, 14055, 14057, 14059, 14060, 14063, 14064, 14065, 14066, 14067, 14068, 14069, 14070, 14071, 14072, 14073, 14074, 14075, 14076, 14077, 14078, 14081, 14083, 14085, 14086, 14087, 14088, 14091, 14094, 14095, 14096, 14097, 14098, 14099, 14102, 14104, 14107, 14113, 14117, 14118, 14121, 14122, 14129, 14132, 14134, 14135, 14136, 13140, 13141, 13142, 13143, 13144, 13145, 13146, 13188, 13189, 13190, 13192, 13193, 13194, 13195, 13196, 13197, 13198, 13199, 13202, 13204, 13206, 13207, 13210, 13211, 13212, 13213, 13215, 13216, 13217, 13218, 13219, 13221, 13223, 13224, 13228, 13229, 13230, 13231, 13232, 13234, 13235, 13236, 13237, 13238, 13239, 13240, 13241, 13242, 13243, 13244, 13245, 13246, 13247, 13249, 13250, 13251, 13252, 13253, 13254, 13255, 13256, 13257, 13258, 13259, 13260, 13261, 13263, 13264, 13266, 13267, 13268, 13269, 13270, 13271, 13272, 13273, 13274, 13275, 13277, 13278, 13279, 13280, 13281, 13282, 13284, 13285, 13286, 13287, 13288, 13289, 13290, 13291, 13292, 13293, 13294, 13295, 13296, 13297, 13298, 13299, 13300, 13301, 13302, 13303, 13304, 13305, 13306, 13307, 13308, 13309, 13310, 13311, 13312, 13313, 13314, 13316, 13317, 13318, 13319, 13320, 13321, 13322, 13323, 13324, 13325, 13326, 13327, 13329, 13330, 13331, 13332, 13334, 13335, 13336, 13337, 13338, 13341, 13342, 13343, 13344, 13347, 13348, 13349, 13351, 13352, 13353, 13354, 13357, 13367, 13368, 13369, 13371, 13372, 13373, 13374, 13376, 13378, 13379, 13380, 13381, 13382, 13383, 13384, 13385, 13386, 13387, 13388, 13389, 13390, 13391, 13393, 13394, 13395, 13396, 13397, 13398, 13399, 13400, 13401, 13402, 13403, 13404, 13405, 13406, 13407, 13408, 13409, 13410, 13411, 13412, 13413, 13414, 13415, 13416, 13417, 13420, 13421, 13422, 13423, 13424, 13425, 13426, 13427, 13428, 13429, 13430, 13431, 13432, 13433, 13434, 13435, 13437, 13439, 13440, 13441, 13442, 13443, 13444, 13445, 13446, 13447, 13448, 13449, 13450, 13451, 13452, 13454, 13455, 13457, 13458, 13459, 13461, 13462, 13463, 13464, 13465, 13466, 13469, 13470, 13471, 13472, 13473, 13474, 13475, 13478, 13479, 13480, 13481, 13482, 13483, 13484, 13485, 13487, 13488, 13489, 13490, 13491, 13492, 13493, 13494, 13495, 13496, 13497, 13498, 13499, 13500, 13503, 13504, 13505, 13506, 13507, 13509, 13511, 13512, 13514, 13515, 13518, 13522, 13523, 13524, 13527, 13528, 13529, 13530, 13531, 13532, 13534, 13535, 13536, 13539, 13540, 13541, 13542, 13543, 13544, 13546, 13547, 13548, 13549, 13550, 13551, 13552, 13553, 13554, 13556, 13557, 13558, 13559, 13560, 13561, 13562, 13563, 13564, 13565, 13567, 13568, 13569, 13570, 13571, 13572, 13573, 13574, 13575, 13576, 13579, 13580, 13581, 13582, 13583, 13584, 13585, 13586, 13587, 13588, 13589, 13590, 13591, 13592, 13593, 13594, 13595, 13596, 13597, 13598, 13599, 13600, 13601, 13602, 13604, 13605, 13606, 13607, 13609, 13149, 13150, 13166, 13167, 13177, 13183, 13191, 13200, 13201, 13203, 13205, 13208, 13209, 13214, 13220, 13222, 13225, 13226, 13227, 13233, 13248, 13262, 13265, 13276, 13283, 13315, 13328, 13333, 13339, 13340, 13345, 13346, 13350, 13355, 13356, 13358, 13359, 13360, 13361, 13362, 13363, 13364, 13365, 13366, 13370, 13375, 13377, 13392, 13418, 13419, 13436, 13438, 13453, 13456, 13460, 13467, 13468, 13476, 13477, 13486, 13501, 13502, 13508, 13510, 13513, 13516, 13517, 13519, 13520, 13521, 13525, 13526, 13533, 13537, 13538, 13545, 13555, 13566, 13577, 13578, 13603, 13608, 13613, 13614, 13616, 13628, 13630, 13648, 13650, 13656, 13667, 13668, 13669, 13671, 13683, 13684, 13686, 13687, 13695, 13702, 13703, 13705, 13713, 13714, 13732, 13738, 13743, 13751, 13753, 13755, 13761, 13762, 13763, 13764, 13765, 13766, 13770, 13771, 13773, 13776, 13777, 13778, 13780, 13782, 13784, 13785, 13786, 13787, 13790, 13791, 13793, 13794, 13804, 13806, 13807, 13812, 13819, 13820, 13821, 14346, 14347, 14360, 14359, 14370, 14371, 14373, 14376, 14377, 14378, 14379, 14380, 14381, 14382, 14383, 14384, 14385, 14386, 14387, 14388, 14389, 14390, 14391, 14392, 14393, 14394, 14395, 14396, 14397, 14398, 14399, 14400, 14401, 14402, 14610, 14611, 14612, 14613, 14614, 14615, 14616, 14617, 14368, 14618, 14619, 14620, 14621, 14622, 14623, 14624, 14625, 14626, 14627, 14628, 14629, 14149, 14150, 14151, 14152, 14159, 14172, 14174, 14176, 14178, 14180, 14184, 14186, 14188, 14189, 14196, 14197, 14348, 14349, 14140, 14142, 14350, 14351, 14352, 14143, 14353, 14354, 14355, 14356, 14357, 14363, 14364, 14365, 14366, 14367, 14372, 14144, 14145, 14146, 14147, 14198, 14199, 14217, 14223, 14226, 14227, 14229, 14234, 14241, 14243, 14244, 14248, 14251, 14253, 14254, 14255, 14256, 14267, 14268, 14270, 14271, 14272, 14275, 14281, 14284, 14285, 14286, 14290, 14292, 14294, 14295, 14297, 14299, 14300, 14301, 14303, 14304, 14305, 14312, 14313, 14314, 14315, 14317, 14319, 14320, 14403, 14404, 14405, 14406, 14407, 14408, 14409, 14410, 14411, 14412, 14413, 14414, 14415, 14416, 14417, 14418, 14419, 14420, 14421, 14422, 14423, 14424, 14425, 14426, 14427, 14428, 14429, 14430, 14431, 14432, 14433, 14434, 14435, 14436, 14437, 14438, 14439, 14440, 14441, 14442, 14443, 14444, 14445, 14446, 14447, 14448, 14449, 14450, 14451, 14452, 14453, 14454, 14455, 14456, 14457, 14458, 14459, 14460, 14461, 14462, 14463, 14464, 14465, 14466, 14467, 14468, 14469, 14470, 14471, 14472, 14473, 14474, 14475, 14476, 14477, 14478, 14479, 14480, 14481, 14482, 14483, 14484, 14485, 14486, 14487, 14488, 14489, 14490, 14491, 14492, 14493, 14494, 14495, 14496, 14497, 14498, 14499, 14500, 14501, 14502, 14503, 14504, 14505, 14506, 14507, 14508, 14509, 14510, 14511, 14512, 14513, 14514, 14515, 14516, 14517, 14518, 14519, 14520, 14521, 14522, 14523, 14524, 14525, 14526, 14527, 14528, 14529, 14530, 14531, 14532, 14533, 14534, 14535, 14536, 14537, 14538, 14539, 14540, 14541, 14542, 14543, 14544, 14545, 14546, 14547, 14548, 14549, 14550, 14551, 14552, 14553, 14554, 14555, 14556, 14557, 14558, 14559, 14560, 14561, 14562, 14563, 14564, 14565, 14566, 14567, 14568, 14569, 14570, 14571, 14572, 14573, 14574, 14575, 14576, 14577, 14578, 14579, 14580, 14581, 14582, 14583, 14584, 14585, 14586, 14587, 14588, 14589, 14590, 14591, 14592, 14593, 14594, 14595, 14596, 14597, 14598, 14599, 14600, 14601, 14602, 14603, 14604, 14605, 14606, 14607, 14608, 14609, 14630, 14631, 14632, 14633, 14634, 14635, 14636, 14637, 14638, 14639, 14640, 14641, 14642, 14643, 14644, 14645, 14646, 15029, 15030, 15031, 15032, 15033, 15034, 15035, 15036, 14647, 14648, 14649, 14650, 14651, 14652, 14653, 14654, 14655, 14656, 14657, 14658, 14659, 14660, 14661, 14662, 14663, 14664, 14665, 14666, 14667, 14668, 14669, 14670, 14671, 14672, 14673, 14674, 14675, 14676, 14677, 14678, 14679, 14680, 14681, 14682, 14683, 14684, 14685, 14686, 14687, 14688, 14689, 14690, 14691, 14692, 14693, 14694, 14695, 14696, 14697, 14698, 14699, 14700, 14701, 14702, 14703, 14704, 14705, 14706, 14707, 14708, 14709, 14710, 14711, 14712, 14713, 14714, 14715, 14716, 14717, 14718, 14719, 14720, 14721, 14722, 14723, 14724, 14725, 14726, 14727, 14728, 14729, 14730, 14731, 14732, 14733, 14734, 15037, 15038, 15039, 15040, 15041, 15042, 15043, 15044, 15045, 15046, 15047, 15048, 15049, 15050, 15051, 15052, 15053, 15054, 15055, 15056, 15057, 14735, 14736, 14737, 14738, 14739, 14740, 14741, 14742, 14743, 14744, 14745, 14746, 14747, 14748, 14749, 14750, 14751, 14752, 14753, 14754, 14755, 14756, 14757, 14758, 14759, 14760, 14761, 14762, 14763, 14764, 14765, 14766, 14767, 14768, 14769, 14770, 14771, 14772, 14773, 14774, 14775, 14776, 14777, 14778, 14779, 14780, 14781, 14782, 14783, 14784, 14785, 14786, 14787, 14788, 14789, 14790, 14791, 14792, 14793, 14794, 14795, 14796, 14797, 14798, 14799, 14800, 14801, 14802, 14803, 14804, 14805, 14806, 14807, 14808, 14809, 14810, 14811, 14812, 14813, 14814, 14815, 14816, 14817, 14818, 14819, 14820, 14821, 14822, 14823, 14824, 14825, 14826, 14827, 14828, 14829, 14830, 14831, 14832, 14833, 14834, 14835, 14836, 14837, 14838, 14839, 14840, 14841, 14842, 14843, 14844, 14845, 14846, 14847, 14848, 14849, 14850, 14851, 14852, 14853, 14854, 14855, 14856, 14857, 14858, 14859, 14860, 14861, 14862, 14863, 14864, 14865, 14866, 14867, 14868, 14869, 14870, 14871, 14872, 14873, 14874, 14875, 14876, 14877, 14878, 14879, 14880, 14881, 14882, 14883, 14884, 14885, 14886, 14887, 14888, 14889, 14890, 14891, 14892, 14893, 14322, 14323, 14324, 14325, 14329, 14330, 14331, 14332, 14333, 14334, 14335, 14342, 14894, 14895, 14896, 14897, 14898, 14899, 14900, 14901, 14902, 14903, 14904, 14905, 14906, 14907, 14908, 14909, 14910, 14911, 14912, 14913, 14914, 14915, 14916, 14917, 14918, 14919, 14920, 14369, 14921, 14922, 14923, 14924, 14925, 14926, 14927, 14928, 14929, 14930, 14931, 14932, 14933, 14934, 14935, 14936, 14937, 14938, 14939, 14940, 14941, 14942, 14943, 14944, 14945, 14946, 14947, 14948, 14949, 14950, 14951, 14952, 14953, 14954, 14955, 14956, 14957, 14958, 14959, 14960, 14961, 14962, 14293, 14296, 14298, 14302, 14306, 14307, 14308, 14963, 15058, 15059, 15060, 15061, 15062, 15063, 15064, 14964, 14965, 14966, 14967, 14968, 14969, 14970, 14971, 14972, 14973, 14974, 14975, 14309, 14310, 14311, 14316, 14318, 14321, 14326, 14141, 14148, 14153, 14154, 14155, 14156, 14157, 14158, 14160, 14976, 14977, 14978, 14979, 14980, 14981, 14982, 14983, 14984, 14985, 14986, 14987, 14988, 14161, 14162, 14163, 14164, 14165, 14166, 14167, 14168, 14169, 14170, 14171, 14173, 14175, 14177, 14179, 14181, 14989, 14990, 14991, 14992, 14993, 14994, 14995, 14996, 14997, 14998, 14999, 15000, 15001, 15002, 15003, 15004, 15005, 15006, 15007, 15008, 15009, 15010, 15011, 15012, 14182, 14183, 14185, 14187, 14190, 14191, 14192, 14193, 14194, 14195, 14200, 14201, 14202, 14203, 14204, 14205, 14206, 14207, 14208, 14209, 14210, 14211, 14212, 14213, 14214, 14215, 14216, 14218, 14219, 14220, 14221, 14222, 14224, 14225, 14228, 14230, 15013, 15014, 15015, 15016, 15017, 15018, 15019, 15020, 15021, 15022, 15023, 15024, 15025, 15026, 15027, 15028, 15065, 15066, 15067, 15068, 15069, 15070, 15071, 15072, 15073, 15074, 15075, 15076, 15077, 15078, 15079, 15080, 15081, 15082, 15083, 15084, 15085, 15086, 15087, 15088, 15089, 15090, 15091, 15092, 15093, 15094, 15095, 15096, 15097, 15098, 15099, 15100, 15101, 15102, 15103, 15104, 15105, 15106, 15107, 15108, 15109, 15110, 15111, 15112, 15113, 15114, 15115, 15116, 15117, 15118, 15119, 15120, 15121, 15122, 15123, 15124, 15125, 15126, 15127, 15128, 15129, 15130, 15131, 15132, 15133, 15134, 15135, 15136, 15137, 15138, 15139, 15140, 15141, 15142, 15143, 15144, 15145, 15146, 15147, 14231, 14232, 14233, 14235, 14236, 14237, 14238, 14239, 14240, 14242, 14245, 14246, 14247, 14249, 14250, 14252, 14257, 14258, 14259, 14260, 14261, 14262, 14327, 14328, 14336, 14337, 14338, 14339, 14340, 14341, 14263, 14264, 14265, 14266, 14269, 14273, 14274, 14276, 14277, 14278, 14279, 14280, 14282, 14283, 14287, 14288, 14289, 14291, 16246, 15410, 15411, 15412, 15413, 15414, 15415, 15416, 15417, 15418, 15419, 15420, 15421, 15422, 15423, 15424, 15425, 15426, 15427, 15428, 15429, 15430, 15431, 15432, 15433, 15434, 15435, 15436, 15437, 15438, 15439, 15440, 15441, 15442, 15443, 15444, 15445, 15446, 15447, 15448, 15449, 15450, 15451, 15452, 15453, 15454, 15455, 15456, 15457, 15458, 15459, 15460, 15461, 15462, 15463, 15464, 15465, 15466, 15467, 15468, 15469, 15470, 15471, 15472, 15473, 15474, 15475, 15476, 15477, 15478, 15479, 15480, 15481, 15482, 15483, 15484, 15485, 15486, 15487, 15488, 15489, 15490, 15491, 15492, 15493, 15494, 15495, 15496, 16805, 16806, 15497, 15498, 15499, 15500, 15501, 15502, 15503, 15504, 15505, 15506, 15507, 15508, 15509, 15510, 15511, 15512, 15513, 15514, 15515, 15516, 15517, 15518, 15519, 15520, 15521, 15522, 15523, 15524, 15525, 15526, 15527, 15528, 15529, 15530, 15531, 15532, 15533, 15534, 15535, 15536, 15537, 15538, 15539, 15540, 15541, 15542, 15543, 15544, 15545, 15546, 15547, 15548, 15549, 15550, 15551, 15552, 15553, 15554, 15555, 15556, 15557, 15558, 15559, 15560, 15561, 15562, 15563, 15564, 15565, 15566, 15567, 15568, 15569, 15570, 15571, 15572, 15573, 15574, 15575, 15576, 15577, 15578, 15579, 15580, 15581, 15582, 15583, 15584, 15585, 15586, 15587, 15588, 15589, 15590, 15591, 15592, 15593, 15594, 15595, 16013, 15596, 16057, 16058, 15879, 16918, 15597, 15598, 16937, 15905, 15904, 15599, 16312, 15600, 16306, 16944, 16946, 16947, 16968, 16969, 15601, 15602, 15603, 15604, 15605, 15606, 15607, 15608, 15609, 15610, 15611, 15612, 15613, 15614, 15615, 15616, 15617, 15618, 15619, 15620, 15621, 15622, 15623, 15624, 16938, 16942, 16943, 16945, 16952, 16953, 16954, 16955, 15625, 15626, 15627, 15628, 15872, 15873, 16133, 15876, 15877, 15880, 15881, 15882, 15883, 15887, 15884, 15886, 15885, 15888, 15900, 15906, 16970, 15909, 15912, 15913, 16971, 16972, 15917, 15918, 15919, 15920, 15921, 15922, 15923, 15924, 15925, 15926, 15927, 15928, 15929, 15930, 15931, 15932, 15933, 15934, 15935, 15936, 15938, 15939, 15940, 15941, 15943, 15944, 15945, 15946, 15947, 15948, 15955, 15951, 15914, 15954, 15963, 15967, 15968, 15981, 15982, 15987, 15988, 15957, 15958, 15959, 15960, 15961, 15969, 15970, 15978, 15992, 15995, 15997, 16002, 16003, 16005, 16006, 16007, 16008, 16009, 16010, 16011, 16012, 16043, 16059, 16060, 16061, 16078, 16077, 16102, 16103, 16110, 16111, 16116, 15962, 16126, 16127, 16128, 16130, 16132, 16140, 16141, 16149, 15977, 16146, 15991, 16056, 16182, 16150, 16152, 16153, 16154, 16158, 15986, 16121, 16189, 16190, 16191, 16192, 16204, 16248, 16249, 16254, 16255, 16256, 16207, 16258, 16260, 16261, 16262, 16264, 16294, 16295, 15875, 16305, 15990, 16309, 16310, 16311, 15629, 16314, 16142, 16184, 16321, 16137, 16335, 16336, 16346, 16347, 16350, 16351, 16162, 15973, 16134, 15999, 16253, 16250, 16138, 16183, 16135, 16151, 16145, 15996, 16285, 16466, 16467, 16468, 16469, 16470, 16471, 16472, 16473, 16474, 16475, 16476, 16477, 15956, 16267, 16341, 16342, 16343, 16348, 16055, 16131, 16136, 15975, 16322, 15976, 16798, 16799, 15630, 16004, 16287, 16284, 16283, 16292, 16334, 15979, 15998, 16288, 16293, 16286, 16282, 16280, 16281, 16291, 16279, 16290, 16278, 16277, 16273, 16275, 16274, 16276, 16272, 16206, 16289, 16271, 16268, 16270, 16269, 16074, 16076, 16936, 16104, 16109, 16148, 15994, 15631, 15632, 15633, 15634, 15635, 15636, 15637, 15638, 15639, 15640, 15850, 15851, 15852, 15853, 15854, 15855, 15641, 15642, 15643, 15644, 15645, 15646, 15647, 15648, 15649, 15650, 15651, 15652, 15653, 15654, 15655, 15656, 15657, 15658, 15659, 15660, 15661, 15662, 15663, 15664, 15665, 15666, 15667, 15668, 15669, 15670, 15671, 15672, 15673, 15674, 15675, 15676, 15974, 15677, 15678, 15679, 15680, 15681, 15682, 15683, 15684, 15685, 15686, 15687, 15688, 15689, 15690, 15691, 15692, 15693, 15694, 15695, 15696, 15697, 15698, 15699, 15700, 15701, 15702, 15703, 15704, 15705, 15706, 15707, 15708, 15709, 15710, 15856, 15857, 15858, 15859, 15860, 15861, 15862, 15863, 15711, 15712, 15713, 15714, 15715, 15716, 15717, 15718, 15719, 15720, 15721, 15722, 15723, 15724, 15725, 15726, 15727, 15728, 15729, 15730, 15731, 15732, 15733, 15734, 15735, 15736, 15737, 15738, 15739, 15740, 15741, 15742, 15743, 15744, 15745, 15746, 15747, 15748, 15749, 15750, 15751, 15752, 15753, 15754, 15755, 15756, 15757, 15758, 15759, 15760, 15761, 15762, 15763, 15764, 15765, 15766, 15767, 15768, 15769, 15770, 15771, 15772, 15773, 15774, 15775, 15776, 15777, 15778, 15779, 15780, 15781, 15782, 15783, 15784, 15785, 15786, 15787, 15788, 15789, 15790, 15791, 15792, 15793, 15794, 15795, 15796, 15797, 15798, 15799, 15800, 15953, 15801, 15802, 15803, 15804, 15805, 15806, 15807, 15808, 15809, 15148, 15149, 15150, 15151, 15152, 15153, 15154, 15155, 15156, 15157, 15158, 15159, 15160, 15161, 15162, 15163, 15164, 15165, 15166, 15167, 15168, 15169, 15170, 15171, 15172, 15173, 15174, 15175, 15176, 15177, 15178, 15179, 15180, 15181, 15182, 15183, 15184, 15185, 15186, 15187, 15188, 15189, 15190, 15191, 15192, 15193, 15194, 15195, 15196, 15197, 15198, 15199, 15200, 15201, 15202, 15203, 15204, 15205, 15206, 15207, 15208, 15209, 15210, 15211, 15212, 15213, 15214, 15215, 15216, 15217, 15218, 15219, 15220, 15221, 15222, 15223, 15224, 15225, 15226, 15227, 15228, 15229, 15230, 15231, 15232, 15233, 15234, 15235, 15236, 15237, 15238, 15239, 15240, 15241, 15242, 15243, 15244, 15245, 15246, 15247, 15248, 15249, 15250, 15251, 15252, 15253, 15254, 15255, 15256, 15257, 15258, 15259, 15260, 15261, 15262, 15263, 15264, 15265, 15266, 15267, 15268, 15269, 15270, 15271, 15272, 15273, 15274, 15275, 15276, 15277, 15278, 15279, 15280, 15281, 15282, 15283, 15284, 15285, 15286, 15287, 15288, 15289, 15290, 15291, 15292, 15293, 15294, 15295, 15296, 15297, 15298, 15299, 15300, 15301, 15302, 15303, 15304, 15305, 15306, 15307, 15308, 15309, 15310, 15311, 15312, 15313, 15314, 15315, 15316, 15317, 15318, 15810, 15811, 15812, 15813, 15814, 15815, 15816, 15817, 15818, 15819, 15820, 15821, 15822, 15823, 15824, 15825, 15826, 15827, 15828, 15829, 15830, 15831, 15864, 15865, 15866, 15867, 15868, 15869, 15870, 15871, 16975, 16976, 16977, 16978, 16979, 16980, 15319, 15320, 15321, 15322, 15323, 15324, 15325, 15326, 15327, 15328, 15329, 15330, 15331, 15332, 15333, 15334, 15335, 15336, 15337, 15338, 15339, 15340, 15341, 15342, 15343, 15344, 15345, 15346, 15347, 15348, 15349, 15350, 15351, 15352, 15353, 15354, 15355, 15356, 15357, 15358, 15359, 15360, 15361, 15362, 15363, 15364, 15365, 15366, 15367, 15368, 15369, 15370, 15371, 15372, 15373, 15374, 15375, 15376, 15377, 15378, 15379, 15380, 15381, 15382, 15383, 15384, 15385, 15386, 15387, 15388, 15389, 15390, 15391, 15392, 15393, 15394, 15395, 15396, 15397, 15398, 15399, 15400, 15401, 15402, 15403, 15404, 15405, 15406, 15407, 15408, 15409, 15832, 15833, 15834, 15835, 15836, 15837, 15838, 15839, 15840, 15841, 15842, 15843, 15844, 15845, 15846, 15847, 15848, 15849, 16981, 16982, 16983, 16984, 16985, 16986, 16987, 16988, 16989, 16990, 16991, 16992, 16993, 16994, 16995, 17371, 17372, 17373, 17374, 17375, 17376, 17377, 17378, 17379, 17380, 17381, 17382, 17383, 17384, 17385, 17386, 17387, 17388, 17389, 17390, 17391, 17392, 17393, 17394, 17395, 17396, 17397, 17398, 17399, 17977, 17400, 17401, 17402, 17403, 17404, 17405, 17406, 17407, 17408, 17409, 17410, 17411, 17412, 17413, 17414, 17415, 17416, 17417, 17418, 17419, 17420, 17421, 17422, 17423, 17424, 17425, 17426, 17427, 17428, 17429, 17430, 17431, 17432, 17433, 17434, 17435, 17436, 17437, 17438, 17439, 17440, 17441, 17442, 17882, 17883, 17443, 17444, 17445, 17446, 17447, 17448, 17449, 17450, 17451, 17452, 17453, 17454, 17455, 17456, 17457, 17458, 17459, 17460, 17461, 17462, 17463, 17473, 17474, 17475, 17476, 17477, 17478, 17464, 17465, 17466, 17590, 18221, 18219, 18218, 18210, 18207, 18217, 18203, 18202, 18215, 18211, 18208, 18205, 17467, 17468, 17469, 17470, 17471, 18214, 17607, 18213, 18212, 18201, 17472, 17514, 17515, 17516, 17517, 17518, 17519, 17520, 17521, 17522, 17523, 17524, 17525, 17526, 17527, 17528, 17479, 17480, 17481, 17482, 17483, 17484, 17485, 17486, 17487, 17488, 17489, 17490, 17491, 17492, 17493, 17494, 17495, 17496, 17497, 17498, 17499, 17500, 17501, 17502, 17503, 17504, 17505, 17506, 17507, 17508, 17509, 17510, 17511, 17512, 17513, 18438, 18432, 18433, 17529, 17530, 17531, 17532, 17533, 17534, 17535, 17536, 17537, 17538, 17539, 17540, 17541, 17542, 17543, 17544, 17545, 17546, 17547, 17548, 17549, 17550, 17551, 17552, 17553, 17554, 17555, 17556, 17557, 17558, 17559, 17560, 17561, 17562, 17563, 17564, 17565, 17566, 17567, 17568, 17569, 17570, 17571, 17572, 17573, 17574, 17575, 17576, 17577, 17578, 17579, 17580, 17581, 17582, 17583, 17584, 17585, 17586, 17587, 18484, 18435, 29575, 29576, 29581, 29582, 29583, 29584, 29585, 29586, 29587, 29105, 18439, 29522, 29588, 29589, 17133, 29120, 29187, 29635, 29639, 29640, 29641, 29642, 29648, 29649, 29650, 29651, 29652, 29653, 29654, 29655, 29656, 29657, 29658, 29659, 29636, 29637, 29669, 29190, 29194, 29195, 29197, 29545, 29643, 29644, 29645, 29646, 29647, 18206, 29191, 29192, 29193, 29196, 29198, 29199, 29200, 17865, 18463, 18464, 17877, 18604, 18647, 18649, 18650, 18651, 18838, 18877, 18878, 18879, 18880, 18881, 18886, 18882, 18883, 18891, 18892, 18893, 18894, 18895, 18896, 18897, 18898, 18899, 18900, 18901, 18902, 18903, 18904, 18905, 18906, 18907, 18908, 18909, 18910, 18911, 18912, 18913, 18914, 18915, 18916, 18917, 18918, 18919, 18920, 18921, 18922, 18923, 18924, 18925, 18926, 18927, 18930, 18931, 18932, 18933, 18934, 18935, 18936, 18937, 18938, 18939, 18940, 18941, 18942, 18943, 18944, 18945, 18946, 18947, 18948, 18949, 18950, 18951, 18952, 18953, 18954, 18955, 18956, 18957, 18958, 18959, 18960, 18961, 18969, 18970, 18973, 18974, 18975, 18976, 18977, 18978, 18979, 18980, 18981, 18982, 18983, 18984, 18985, 18986, 18987, 18988, 18989, 18990, 18991, 18992, 18993, 18994, 18995, 18996, 18997, 18998, 19003, 19005, 19014, 19018, 19019, 29201, 29202, 29638, 29203, 29204, 29205, 29206, 29519, 29207, 29546, 29547, 29548, 29208, 29211, 29212, 29213, 29214, 29550, 29554, 29555, 18466, 29209, 29210, 29238, 29215, 29216, 29217, 18472, 18473, 29218, 29553, 18465, 29219, 29221, 29220, 29227, 29228, 29229, 29232, 29233, 29234, 29236, 29237, 18471, 29222, 29556, 29558, 29559, 29230, 29231, 29235, 29239, 29240, 29168, 29557, 29243, 29560, 29561, 18444, 18445, 18446, 18443, 18442, 18448, 18436, 18428, 18485, 29459, 18447, 18440, 18486, 18434, 16996, 16997, 16998, 16999, 17000, 17001, 17002, 17003, 17004, 17005, 17006, 17007, 17008, 17009, 17010, 17011, 17012, 17013, 17014, 17015, 17016, 17017, 17018, 17019, 17020, 17021, 17022, 17023, 17024, 17025, 17026, 17027, 17028, 17029, 17030, 17031, 17032, 17033, 17034, 17035, 17036, 17037, 17038, 17039, 17040, 17041, 17042, 17043, 17044, 17045, 17046, 17047, 17048, 17049, 17050, 17051, 17052, 17053, 17054, 17055, 17056, 17057, 17058, 17059, 17060, 17061, 17062, 17063, 17064, 17065, 17066, 17067, 17068, 17069, 17070, 17071, 17072, 17073, 17074, 17075, 17076, 17077, 17078, 17079, 17080, 17081, 17082, 17083, 17084, 17085, 17086, 17087, 17088, 17089, 17090, 17091, 17092, 17093, 17094, 17095, 17096, 17097, 17098, 17099, 17100, 17101, 17102, 17103, 17104, 17105, 17106, 17107, 17108, 17109, 17110, 17111, 17112, 17113, 17114, 17115, 17116, 17117, 17118, 17119, 17120, 17121, 17122, 17123, 17124, 17125, 17126, 17127, 17128, 17129, 17130, 17131, 17132, 17134, 17135, 17136, 17137, 17138, 17139, 17140, 17141, 17142, 17143, 17144, 17145, 17146, 17147, 17148, 17149, 17150, 17151, 17152, 17153, 17154, 17155, 17156, 17157, 17158, 17159, 17160, 17161, 17162, 17163, 17164, 17165, 17166, 17167, 17168, 17169, 17170, 17171, 17172, 17173, 17174, 17175, 17176, 17177, 17178, 17179, 17180, 17181, 17182, 17183, 17184, 17185, 17186, 17187, 17188, 17189, 17190, 17191, 17192, 17193, 17194, 17195, 17196, 17197, 17198, 17199, 17200, 17201, 17202, 17203, 17204, 17205, 17206, 17207, 17208, 17209, 17210, 17211, 17212, 17213, 17214, 17215, 17216, 17217, 17218, 17219, 17220, 17221, 17222, 17223, 17224, 17225, 17226, 17227, 17228, 17229, 17230, 17231, 17232, 17233, 17234, 17235, 17236, 17237, 17238, 17239, 17240, 17241, 17242, 17243, 17244, 17245, 17246, 17247, 17248, 17249, 17250, 17251, 17252, 17253, 17254, 17255, 17256, 17257, 17258, 17259, 17260, 17261, 17262, 17263, 17264, 17265, 17266, 17267, 17268, 17269, 17270, 17271, 17272, 17273, 17274, 17275, 17276, 17277, 17278, 17279, 17280, 17281, 17282, 17283, 17284, 17285, 17286, 17287, 17288, 17289, 17290, 17291, 17292, 17293, 17294, 17295, 17296, 17297, 17298, 17299, 17300, 17301, 17302, 17303, 17304, 17305, 17306, 17307, 17308, 17309, 17310, 17311, 17312, 17313, 17314, 17315, 17316, 17317, 17318, 17319, 17320, 17321, 17322, 17323, 17324, 17325, 17326, 17327, 17328, 17329, 17330, 17331, 17332, 17333, 17334, 17335, 17336, 17337, 17338, 17339, 17340, 17341, 17342, 17343, 17344, 17345, 17346, 17347, 17348, 17349, 17350, 17351, 17352, 17353, 17354, 17355, 17356, 17357, 17358, 17359, 17360, 17361, 17362, 17363, 17364, 17365, 17366, 17367, 17368, 17369, 17370, 18441, 18449, 18451, 18429, 18450, 18430, 18431, 18437, 29544, 29562, 29563, 29564, 29565, 29580, 29750, 29751, 29752, 29753, 29754, 29755, 29756, 29757, 29758, 29759, 29761, 29762, 29763, 29764, 29765, 29766, 29767, 29768, 29769, 29770, 29771, 29772, 29773, 29774, 29775, 29776, 29777, 29779, 29780, 29781, 29782, 29783, 29784, 29785, 29786, 29787, 29789, 29760, 29792, 29793, 29794, 29795, 29796, 29797, 29798, 29799, 29800, 29801, 29802, 30050, 30051, 30052, 30053, 30054, 30055, 30056, 30057, 30058, 30059, 30060, 30061, 30062, 30063, 30064, 30065, 30066, 30067, 30068, 30069, 30070, 30071, 30072, 30080, 30081, 30085, 30073, 30074, 30075, 30076, 30077, 30078, 30079, 30082, 30083, 30084, 30086, 30087, 30088, 30089, 30090, 30091, 30092, 30093, 30094, 29169, 29566, 29509, 29494, 29502, 29513, 29511, 29507, 29508, 29518, 29505, 29514, 29504, 29492, 29506, 29567, 29568, 29577, 29521, 29495, 29516, 29517, 29512, 29515, 29510, 29503, 29493, 18334, 30095, 30096, 30097, 30098, 30099, 30100, 30107, 30118, 30129, 30119, 30123, 30124, 30128, 30130, 30131, 30101, 30102, 30105, 30108, 30111, 30112, 30113, 30114, 30115, 30120, 30121, 30122, 30125, 30126, 30127, 30133, 30103, 30104, 30106, 30109, 30110, 30116, 30134, 30135, 30136, 30137, 30138, 30139, 30140, 30141, 30142, 30143, 30144, 30145, 30146, 30149, 30150, 30151, 30152, 30153, 30154, 30155, 30156, 30157, 30158, 30159, 30160, 30161, 30162, 30163, 30164, 30165, 30166, 30167, 30168, 30169, 30170, 30171, 30172, 30173, 30174, 30175, 30176, 30177, 30178, 30179, 30180, 30182, 30187, 30185, 30183, 30213, 30214, 30223, 30224, 30231, 30260, 30262, 30263, 30264, 30265, 30266, 30296, 30302, 30303, 30310, 30311, 30312, 30566, 30570, 30571, 30576, 30181, 30184, 30246, 30247, 30248, 30253, 30255, 30268, 30270, 30276, 30279, 30280, 30288, 30289, 30291, 30295, 30297, 30298, 30299, 30300, 30304, 30305, 30309, 30598, 30599, 30600, 30189, 30190, 30192, 30193, 30194, 30195, 30186, 30188, 30191, 30211, 30212, 30215, 30216, 30217, 30218, 30219, 30220, 30221, 30228, 30229, 30230, 30232, 30233, 30234, 30235, 30236, 30240, 30241, 30242, 30579, 30580, 30222, 30225, 30226, 30227, 30237, 30238, 30239, 30243, 30244, 30259, 30269, 30271, 30272, 30284, 30285, 30286, 30581, 30582, 30583, 30584, 30587, 30588, 30132, 30595, 30596, 30597, 30601, 30602, 30250, 30254, 30256, 30257, 30258, 30261, 30267, 30273, 30274, 30275, 30277, 30278, 30281, 30282, 30283, 30287, 30290, 30292, 30293, 30567, 30593, 30594, 30301, 30306, 30307, 30308, 30565, 30568, 30569, 30572, 30573, 30574, 30575, 30577, 30578, 30604, 30605, 30606, 30607, 30608, 30609, 30610, 30611, 30612, 30613, 30614, 30615, 30616, 30617, 30618, 30619, 30620, 30621, 30622, 30623, 30624, 30625, 30626, 30627, 30628, 30629, 30630, 30631, 30632, 30633, 30634, 30635, 30636, 30638, 30639, 30640, 30641, 30642, 30643, 30644, 30645, 30646, 30647, 30648, 30649, 30650, 30651, 30652, 30653, 30654, 30655, 30656, 30657, 30658, 30659, 30660, 30661, 30662, 30663, 30664, 30665, 30666, 30667, 30668, 30669, 30670, 30671, 30672, 30673, 30674, 30675, 30676, 30678, 30689, 30693, 30694, 30695, 30696, 30697, 30698, 30702, 30703, 30704, 30706, 30707, 30715, 30718, 30719, 30720, 30722, 30723, 30726, 30730, 30738, 30739, 30746, 30750, 30752, 30753, 30754, 30755, 30757, 30758, 30764, 30768, 30769, 30778, 30780, 30781, 30789, 30804, 30805, 30806, 31128, 31129, 31169, 31174, 31182, 31183, 31184, 31185, 31192, 31193, 31194, 31195, 31199, 31200, 31204, 31205, 31216, 31220, 31221, 31224, 31225, 31226, 31227, 31228, 31229, 31230, 31238, 31245, 30677, 30679, 30681, 30682, 30683, 30684, 30685, 30687, 30690, 30691, 30692, 30700, 30680, 30686, 30688, 30699, 30701, 31080, 31081, 31084, 31085, 31089, 31093, 31094, 31095, 31096, 31098, 31101, 31104, 31105, 31106, 31107, 31108, 31115, 31116, 31117, 31118, 31119, 31120, 31122, 31124, 31140, 31141, 31142, 31143, 31144, 31145, 31146, 31147, 31157, 30705, 30708, 30709, 30710, 30711, 30712, 30713, 30714, 30716, 30717, 30721, 30724, 30725, 30727, 30728, 30729, 30731, 30732, 30733, 30734, 30735, 30736, 30737, 30740, 30741, 30742, 30743, 30744, 30745, 30747, 30748, 30751, 30756, 30759, 30760, 30761, 30762, 30763, 30765, 30766, 30767, 30770, 30771, 30772, 30773, 30774, 30775, 30776, 30777, 30779, 30782, 30783, 30784, 30785, 30786, 30787, 30788, 30790, 30791, 30792, 30793, 30794, 30795, 30796, 30797, 30798, 30802, 30803, 30807, 30808, 31079, 31082, 31083, 31086, 31209, 31223, 31087, 31088, 31090, 31091, 31092, 31097, 31099, 31100, 31102, 31103, 31109, 31110, 31111, 31112, 31113, 31114, 31121, 31123, 31125, 31126, 31127, 30749, 31133, 31134, 31135, 31136, 31137, 31138, 31139, 31148, 31149, 31150, 31151, 31152, 31153, 31154, 31155, 31156, 31158, 31159, 31160, 31161, 31162, 31163, 31164, 31165, 31166, 31167, 31171, 31172, 31173, 31175, 31176, 31177, 31178, 31179, 31180, 31181, 31186, 31187, 31188, 31189, 31190, 31191, 31196, 31197, 31198, 31201, 31202, 31203, 31206, 31207, 31208, 31222, 31210, 31211, 31212, 31213, 31214, 31215, 31217, 31218, 31219, 31243, 31244, 31231, 31232, 31233, 31234, 31235, 31236, 31237, 31239, 31240, 31241, 31242, 31246, 31248, 31251, 31247, 31249, 31250, 31252, 31253, 31254, 31255, 31256, 31257, 31258, 31259, 31260, 31261, 31262, 31263, 31264, 31271, 31272, 31273, 31274, 31290, 31291, 31293, 31311, 31312, 31314, 31315, 31317, 31265, 31266, 31267, 31268, 31269, 31270, 31275, 31276, 31277, 31278, 31279, 31280, 31281, 31282, 31283, 31284, 31285, 31286, 31292, 31287, 31288, 31289, 31295, 31296, 31297, 31300, 31301, 31589, 31590, 31591, 31294, 31298, 31299, 31302, 31303, 31304, 31305, 31306, 31307, 31308, 31309, 31310, 31313, 31318, 31319, 31320, 31321, 31322, 31323, 31324, 31576, 31577, 31578, 31579, 31586, 31587, 31588, 31592, 31593, 31629, 31630, 31631, 31632, 31633, 31634, 31635, 31636, 31637, 31638, 31639, 31640, 31641, 31642, 31643, 31644, 31645, 31646, 31649, 31647, 31648, 31656, 31659, 31660, 31662, 31666, 31667, 31669, 31670, 31671, 31678, 31679, 31680, 31681, 31685, 31686, 31687, 31688, 31689, 31690, 31691, 31692, 31693, 31694, 31695, 31696, 31699, 31701, 31702, 31707, 31708, 31717, 31722, 31723, 31724, 31725, 31726, 31728, 31729, 31650, 31657, 31658, 31668, 31682, 31683, 31684, 31697, 31698, 31700, 31710, 31711, 31712, 31713, 31651, 31652, 31653, 31654, 31655, 31703, 31704, 31705, 31706, 31709, 31718, 31719, 31720, 31721, 31727, 31730, 31810, 31811, 31813, 31814, 31815, 31816, 31817, 31818, 31819, 31820, 31821, 31822, 31823, 31824, 31825, 31826, 31827, 31828, 31829, 31830, 31831, 31832, 31833, 31834, 31835, 31836, 31837, 31838, 31839, 31840, 31841, 31842, 31843, 31844, 31845, 31846, 31847, 31848, 31849, 31850, 31851, 31852, 31853, 31854, 31856, 31870, 31875, 31876, 31880, 31882, 31916, 31919, 31921, 31855, 31860, 31865, 31867, 31868, 31869, 31877, 31878, 31879, 31891, 31892, 31894, 31897, 31898, 31901, 31902, 31907, 31911, 31857, 31812, 31866, 31871, 31872, 31873, 31874, 31881, 31883, 31884, 31885, 31886, 31887, 31888, 31889, 31890, 31893, 31895, 31896, 31899, 31900, 31903, 31904, 31905, 31906, 31908, 31909, 31910, 31912, 31913, 31914, 31915, 31918, 31920, 31922, 31923, 31924, 31925, 31926, 31927, 31928, 31929, 31931, 31932, 31933, 31934, 31935, 31936, 31937, 31938, 31939, 31940, 31941, 31942, 31943, 31944, 31945, 31946, 31947, 31948, 31949, 31950, 31951, 31952, 31953, 31954, 31955, 31956, 31957, 31958, 31959, 31960, 31961, 31962, 31963, 31964, 31965, 31966, 31967, 31968, 31969, 31970, 31971, 31972, 31973, 31974, 31975, 31976, 31977, 31978, 31983, 31986, 31987, 31989, 31995, 32001, 32002, 32006, 32014, 32015, 31979, 31984, 31985, 31988, 31994, 32004, 32009, 32010, 32011, 32012, 31980, 31981, 31982, 31990, 31991, 31992, 31993, 31996, 31997, 31998, 31999, 32000, 32003, 32008, 32013, 32007, 32005, 31917, 32078, 32079, 32081, 32082, 32083, 32084, 32085, 32086, 32087, 32091, 32095, 32096, 32097, 32098, 32100, 32099, 32104, 32105, 32106, 32107, 32108, 32111, 32112, 32113, 32114, 32115, 31170, 32016, 32017, 32018, 32019, 30637, 32116, 32117, 32118, 32119, 32121, 32122, 32123, 32124, 32120, 32134, 32135, 32136, 32137]
 
 
artifacts/vidul/BGE_FT_VA/config.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "architectures": [
3
- "BertModel"
4
- ],
5
- "attention_probs_dropout_prob": 0.1,
6
- "classifier_dropout": null,
7
- "dtype": "float16",
8
- "gradient_checkpointing": false,
9
- "hidden_act": "gelu",
10
- "hidden_dropout_prob": 0.1,
11
- "hidden_size": 1024,
12
- "id2label": {
13
- "0": "LABEL_0"
14
- },
15
- "initializer_range": 0.02,
16
- "intermediate_size": 4096,
17
- "label2id": {
18
- "LABEL_0": 0
19
- },
20
- "layer_norm_eps": 1e-12,
21
- "max_position_embeddings": 512,
22
- "model_type": "bert",
23
- "num_attention_heads": 16,
24
- "num_hidden_layers": 24,
25
- "pad_token_id": 0,
26
- "position_embedding_type": "absolute",
27
- "transformers_version": "4.56.1",
28
- "type_vocab_size": 2,
29
- "use_cache": true,
30
- "vocab_size": 30522
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/backend/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """ENCODE Part A backend package."""
src/backend/app.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI app for CPU-only, fine-tuned dense retrieval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import io
7
+ import json
8
+ import os
9
+ from functools import lru_cache
10
+ from pathlib import Path
11
+
12
+ from fastapi import Body, FastAPI, HTTPException
13
+ from fastapi.responses import FileResponse, StreamingResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+
16
+ _NO_CACHE = "no-cache, must-revalidate"
17
+
18
+
19
+ class NoCacheStaticFiles(StaticFiles):
20
+ """Serve the vanilla frontend with revalidation so a rebuilt app.js/styles.css
21
+ is never served stale from the browser cache (bit us during development)."""
22
+
23
+ async def get_response(self, path, scope):
24
+ resp = await super().get_response(path, scope)
25
+ resp.headers["Cache-Control"] = _NO_CACHE
26
+ return resp
27
+
28
+ from .codesearch import CodeSearchEngine, make_embedder
29
+ from .encode import EncodeEngine
30
+ from .graph import KnowledgeGraph
31
+ from .retriever import DenseEmbedder
32
+
33
+ ROOT = Path(__file__).resolve().parents[2]
34
+ FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
35
+ ANNOT = Path(os.environ.get("ENCODE_ANNOT_DIR", ROOT / "artifacts" / "annotations"))
36
+
37
+ app = FastAPI(title="ENCODE — Embedding Empowered Code Search")
38
+
39
+
40
+ @lru_cache(maxsize=1)
41
+ def _embedder() -> DenseEmbedder:
42
+ return make_embedder()
43
+
44
+
45
+ @lru_cache(maxsize=1)
46
+ def codes() -> CodeSearchEngine:
47
+ return CodeSearchEngine(_embedder())
48
+
49
+
50
+ @lru_cache(maxsize=1)
51
+ def engine() -> EncodeEngine:
52
+ # Phenotype embeddings use the same BGE_FT_VA weights with their own recorded
53
+ # pooling/prefix convention. No alternate model or second stage is loaded.
54
+ return EncodeEngine()
55
+
56
+
57
+ @lru_cache(maxsize=1)
58
+ def graph() -> KnowledgeGraph:
59
+ return KnowledgeGraph(diagnosis_records=codes().records("diagnosis"),
60
+ procedure_records=codes().records("procedure"))
61
+
62
+
63
+ @app.on_event("startup")
64
+ def _warm() -> None:
65
+ codes() # load the fine-tuned model; FAISS indexes stay lazy by category
66
+
67
+
68
+ @app.get("/healthz")
69
+ def health() -> dict:
70
+ return {"status": "ok", "device": _embedder().device}
71
+
72
+
73
+ # -- code search (primary) -------------------------------------------------
74
+
75
+ @app.get("/api/code/categories")
76
+ def code_categories() -> dict:
77
+ return {"categories": codes().categories()}
78
+
79
+
80
+ @app.get("/api/code/search")
81
+ def code_search(category: str, q: str, k: int = 50) -> dict:
82
+ if not q.strip():
83
+ raise HTTPException(400, "Empty query")
84
+ try:
85
+ return codes().search(category, q, k=min(max(k, 1), 100))
86
+ except KeyError:
87
+ raise HTTPException(404, f"Unknown category '{category}'")
88
+
89
+
90
+ @app.get("/api/code/export")
91
+ def code_export(category: str, q: str, k: int = 50):
92
+ try:
93
+ data = codes().search(category, q, k=min(max(k, 1), 100))
94
+ except KeyError:
95
+ raise HTTPException(404, f"Unknown category '{category}'")
96
+ buf = io.StringIO()
97
+ w = csv.writer(buf)
98
+ w.writerow(["rank", "code_type", "code", "description", "relevance"])
99
+ for r in data["results"]:
100
+ w.writerow([r["rank"], r["code_type"], r["code"], r["description"], r["relevance"]])
101
+ buf.seek(0)
102
+ fname = f"encode_{category}_{q.strip().replace(' ', '_')[:30]}.csv"
103
+ return StreamingResponse(iter([buf.getvalue()]), media_type="text/csv",
104
+ headers={"Content-Disposition": f'attachment; filename="{fname}"'})
105
+
106
+
107
+ @app.post("/api/code/annotations")
108
+ def code_annotations(payload: dict = Body(...)) -> dict:
109
+ records = payload.get("annotations", [])
110
+ if not records:
111
+ raise HTTPException(400, "No annotations")
112
+ ANNOT.mkdir(parents=True, exist_ok=True)
113
+ row = {"annotator": (payload.get("annotator") or "anonymous").strip(),
114
+ "category": payload.get("category"), "query": payload.get("query"),
115
+ "annotations": records}
116
+ with (ANNOT / "code_annotations.jsonl").open("a") as fh:
117
+ fh.write(json.dumps(row) + "\n")
118
+ return {"saved": len(records), "annotator": row["annotator"]}
119
+
120
+
121
+ # -- knowledge graph (click a code -> parent/child ontology) ---------------
122
+
123
+ @app.get("/api/graph")
124
+ def code_graph(code: str, code_type: str | None = None, drug_name: str | None = None) -> dict:
125
+ if not code.strip():
126
+ raise HTTPException(400, "Empty code")
127
+ return graph().neighbors(code, code_type, drug_name)
128
+
129
+
130
+ # -- phenotype discovery ---------------------------------------------------
131
+
132
+ def _cats(categories: str | None) -> set[str] | None:
133
+ return {c for c in categories.split(",") if c} if categories else None
134
+
135
+
136
+ @app.get("/api/categories")
137
+ def categories() -> dict:
138
+ return {"categories": engine().categories()}
139
+
140
+
141
+ @app.get("/api/search")
142
+ def search(q: str, k: int = 10, categories: str | None = None, validated_only: bool = False) -> dict:
143
+ if not q.strip():
144
+ raise HTTPException(400, "Empty query")
145
+ return engine().search(q, k=min(max(k, 1), 50),
146
+ categories=_cats(categories), validated_only=validated_only)
147
+
148
+
149
+ @app.get("/api/export")
150
+ def export(q: str, k: int = 10, categories: str | None = None, validated_only: bool = False):
151
+ data = engine().search(q, k=min(max(k, 1), 50),
152
+ categories=_cats(categories), validated_only=validated_only)
153
+ buf = io.StringIO()
154
+ w = csv.writer(buf)
155
+ w.writerow(["rank", "phenotype_id", "title", "category", "validated", "relevance", "code_systems"])
156
+ for i, r in enumerate(data["results"], 1):
157
+ w.writerow([i, r["phenotype_id"], r["title"], r["category"], r["validated"],
158
+ r["scores"]["relevance"], "; ".join(r["code_systems"])])
159
+ buf.seek(0)
160
+ fname = f"encode_phenotype_{q.strip().replace(' ', '_')[:30]}.csv"
161
+ return StreamingResponse(iter([buf.getvalue()]), media_type="text/csv",
162
+ headers={"Content-Disposition": f'attachment; filename="{fname}"'})
163
+
164
+
165
+ @app.get("/api/phenotype/{pid}")
166
+ def phenotype(pid: int) -> dict:
167
+ detail = engine().phenotype(pid)
168
+ if detail is None:
169
+ raise HTTPException(404, "Phenotype not found")
170
+ return detail
171
+
172
+
173
+ @app.get("/api/phenotype/{pid}/graph")
174
+ def phenotype_graph(pid: int, focus: str | None = None) -> dict:
175
+ g = engine().phenotype_code_graph(pid, focus=focus)
176
+ if g is None:
177
+ raise HTTPException(404, "Phenotype not found")
178
+ return g
179
+
180
+
181
+ @app.post("/api/annotations")
182
+ def annotations(payload: dict = Body(...)) -> dict:
183
+ """Persist phenotype-level relevance labels (the Part A evaluation gold set)."""
184
+ records = payload.get("annotations", [])
185
+ if not records:
186
+ raise HTTPException(400, "No annotations")
187
+ ANNOT.mkdir(parents=True, exist_ok=True)
188
+ row = {"annotator": (payload.get("annotator") or "anonymous").strip(),
189
+ "query": payload.get("query"), "annotations": records}
190
+ with (ANNOT / "query_phenotype_gold.jsonl").open("a") as fh:
191
+ fh.write(json.dumps(row) + "\n")
192
+ return {"saved": len(records), "annotator": row["annotator"]}
193
+
194
+
195
+ @app.get("/")
196
+ def index() -> FileResponse:
197
+ return FileResponse(FRONTEND / "index.html", headers={"Cache-Control": _NO_CACHE})
198
+
199
+
200
+ app.mount("/", NoCacheStaticFiles(directory=FRONTEND), name="static")
src/backend/codesearch.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Vidul's fine-tuned dense retrieval for medical-code search.
2
+
3
+ The runtime uses the original BGE_FT_VA model and FAISS indexes with no score
4
+ blend or fallback model. Indexes are loaded lazily by category so a CPU Space
5
+ does not pay the memory cost for categories nobody has queried.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import csv
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+
15
+ import faiss
16
+ import numpy as np
17
+
18
+ from .retriever import DenseEmbedder
19
+
20
+ ROOT = Path(__file__).resolve().parents[2]
21
+ BASE = Path(os.environ.get("ENCODE_VIDUL_ROOT", ROOT / "artifacts" / "vidul"))
22
+ MODEL_DIR = BASE / "BGE_FT_VA"
23
+
24
+ CATEGORY_INDEXES = {
25
+ "diagnosis": [("icd_index", None)],
26
+ "medication": [("med_index", "Local Drug SID"),
27
+ ("ndc_index", "NDC Package Code")],
28
+ "lab": [("labchem_index", "LabChemTestSID")],
29
+ "procedure": [("cpt_index", "CPT")],
30
+ }
31
+
32
+
33
+ def make_embedder() -> DenseEmbedder:
34
+ """Reproduce Vidul's SentenceTransformer query convention on CPU."""
35
+ if not MODEL_DIR.exists():
36
+ raise RuntimeError(f"Missing Vidul fine-tuned model: {MODEL_DIR}")
37
+ return DenseEmbedder(str(MODEL_DIR), pooling="mean", query_prefix="passage: ")
38
+
39
+
40
+ class CodeSearchEngine:
41
+ def __init__(self, embedder: DenseEmbedder):
42
+ self.embedder = embedder
43
+ self._loaded: dict[str, list[dict]] = {}
44
+ missing = [name for specs in CATEGORY_INDEXES.values() for name, _ in specs
45
+ if not self._complete_index(BASE / name)]
46
+ if missing:
47
+ raise RuntimeError(
48
+ f"Missing Vidul FAISS index assets under {BASE}: {', '.join(missing)}")
49
+
50
+ @staticmethod
51
+ def _complete_index(directory: Path) -> bool:
52
+ return all((directory / name).exists()
53
+ for name in ("index.faiss", "meta.csv", "config.json"))
54
+
55
+ def categories(self) -> list[str]:
56
+ return list(CATEGORY_INDEXES)
57
+
58
+ def _load_category(self, category: str) -> list[dict]:
59
+ if category not in CATEGORY_INDEXES:
60
+ raise KeyError(category)
61
+ if category in self._loaded:
62
+ return self._loaded[category]
63
+
64
+ loaded = []
65
+ for directory_name, fixed_code_type in CATEGORY_INDEXES[category]:
66
+ directory = BASE / directory_name
67
+ config = json.loads((directory / "config.json").read_text())
68
+ if config.get("model") != "BGE_FT_VA" or config.get("dim") != 1024:
69
+ raise RuntimeError(f"Unexpected retrieval config: {directory / 'config.json'}")
70
+ index = faiss.read_index(str(directory / "index.faiss"))
71
+ with (directory / "meta.csv").open(newline="", encoding="utf-8") as handle:
72
+ records = list(csv.DictReader(handle))
73
+ if index.ntotal != len(records):
74
+ raise RuntimeError(
75
+ f"Index/metadata mismatch in {directory}: {index.ntotal} != {len(records)}")
76
+ loaded.append({"index": index, "records": records,
77
+ "code_type": fixed_code_type})
78
+ self._loaded[category] = loaded
79
+ return loaded
80
+
81
+ def records(self, category: str) -> list[dict]:
82
+ """Metadata rows in the shape consumed by the optional graph view."""
83
+ rows = []
84
+ for source in self._load_category(category):
85
+ for record in source["records"]:
86
+ rows.append({"code": record["code"],
87
+ "description": record["description"],
88
+ "code_type": source["code_type"] or record.get("version", "")})
89
+ return rows
90
+
91
+ def search(self, category: str, query: str, k: int = 50) -> dict:
92
+ sources = self._load_category(category)
93
+ query_vector = self.embedder.encode([query.strip()]).astype(np.float32)
94
+ candidates = []
95
+ for source in sources:
96
+ scores, indices = source["index"].search(query_vector, k)
97
+ for score, index_position in zip(scores[0], indices[0]):
98
+ if index_position < 0:
99
+ continue
100
+ record = source["records"][int(index_position)]
101
+ code_type = source["code_type"] or record.get("version", "Diagnosis")
102
+ candidates.append({
103
+ "code": record["code"],
104
+ "code_type": code_type,
105
+ "description": record["description"],
106
+ "_score": float(np.clip(score, -1.0, 1.0)),
107
+ })
108
+
109
+ candidates.sort(key=lambda row: row["_score"], reverse=True)
110
+ results = candidates[:k]
111
+ for rank, result in enumerate(results, 1):
112
+ result["relevance"] = round(result.pop("_score"), 4)
113
+ result["rank"] = rank
114
+ return {
115
+ "query": query,
116
+ "category": category,
117
+ "total": sum(source["index"].ntotal for source in sources),
118
+ "count": len(results),
119
+ "model": "BGE_FT_VA",
120
+ "results": results,
121
+ }
src/backend/encode.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Natural-language request to ranked CIPHER phenotypes.
2
+
3
+ Dense, two-surface retrieval over prebuilt Vidul BGE_FT_VA embeddings:
4
+
5
+ - phenotype_metadata surface -> phenotype-level semantic similarity
6
+ - code_evidence surface -> code-level supporting evidence (max over chunks)
7
+
8
+ A phenotype is a document, not a bag of codes. Each candidate carries both channel
9
+ scores separately, a grounded summary drawn strictly from source metadata, the
10
+ resolved code set and source-grounded metadata. Ranking is fine-tuned dense
11
+ retrieval only.
12
+
13
+ No fallback: the embeddings index and the model must exist, or startup fails.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+
25
+ from .retriever import DenseEmbedder
26
+
27
+ ROOT = Path(__file__).resolve().parents[2]
28
+ CANON = ROOT / "artifacts" / "part_a_canonical"
29
+ # The config beside the vectors records the query encoding convention.
30
+ EMB = Path(os.environ.get("ENCODE_PARTA_EMB_DIR", ROOT / "artifacts" / "part_a_embeddings"))
31
+ # Optional, source-grounded detail sidecars.
32
+ CODE_DESC = CANON / "code_descriptions.jsonl"
33
+ ALGO_COMPONENTS = CANON / "algorithm_components.jsonl"
34
+
35
+ # Algorithm-component fields to surface, in display order (professor 1.4:
36
+ # "refer to CIPHER algorithm components"). Only fields present for a phenotype
37
+ # are shown; the rest are omitted (never fabricated).
38
+ _ALGO_FIELDS = [
39
+ ("methods_used", "Methods"),
40
+ ("related_diseases", "Related diseases"),
41
+ ("data_period", "Data period"),
42
+ ("validations", "Validation"),
43
+ ("adjudication_method", "Adjudication"),
44
+ ]
45
+
46
+ # CIPHER public phenotype library. Detail page:
47
+ # /web/cipher/phenotype-viewer?uqid={uqid}&name={slug}
48
+ # where slug = the full name with every non-alphanumeric char replaced by '_'.
49
+ # uqid comes from the cipher_links sidecar (present for ~79% of phenotypes);
50
+ # the rest fall back to the library root. Domain overridable for other deployments.
51
+ CIPHER_BASE = os.environ.get("ENCODE_CIPHER_BASE", "https://phenomics.va.ornl.gov").rstrip("/")
52
+ CIPHER_LINKS = CANON / "cipher_links.jsonl"
53
+
54
+ # Friendly stand-ins so a still-unresolved code never shows the raw
55
+ # 'unsupported_code_system' token (professor feedback 1.1). (label, source)
56
+ _UNRESOLVED_NOTE = {
57
+ 466: ("Medication code", "needs_vocab:RxNorm/NDC"),
58
+ 467: ("LOINC lab code", "needs_vocab:LOINC"),
59
+ 471: ("OMOP concept ID", "needs_vocab:OMOP"),
60
+ 465: ("VA clinic stop code", "needs_vocab:VA_STOP"),
61
+ 468: ("Study-defined variable", "study_specific"),
62
+ 519: ("Study-defined variable", "study_specific"),
63
+ }
64
+
65
+ class EncodeEngine:
66
+ def __init__(self, embedder: DenseEmbedder | None = None, emb_dir: Path | str | None = None):
67
+ emb = Path(emb_dir) if emb_dir else EMB
68
+ self.emb_dir = emb
69
+ if not (emb / "config.json").exists():
70
+ raise RuntimeError(f"Missing runtime phenotype embeddings: {emb}")
71
+ self.config = json.loads((emb / "config.json").read_text())
72
+
73
+ self.phenotypes = {}
74
+ for line in (CANON / "canonical_phenotypes.jsonl").open():
75
+ r = json.loads(line)
76
+ self.phenotypes[r["phenotype_id"]] = r
77
+
78
+ # Metadata surface: one row per phenotype, aligned to meta_ids.
79
+ self.meta_ids: list[int] = json.loads((emb / "metadata_ids.json").read_text())
80
+ self.meta_mat = np.load(emb / "metadata.npy").astype(np.float32)
81
+ self._row = {pid: i for i, pid in enumerate(self.meta_ids)}
82
+
83
+ # Code surface: many rows per phenotype; map each to its phenotype row.
84
+ code_meta = json.loads((emb / "code_meta.json").read_text())
85
+ self.code_mat = np.load(emb / "code.npy").astype(np.float32)
86
+ self._code_row = np.array([self._row.get(m["phenotype_id"], -1) for m in code_meta])
87
+
88
+ # Per-phenotype filters aligned to meta_ids.
89
+ self._cat = np.array([self.phenotypes[p].get("category") or "(none)" for p in self.meta_ids])
90
+ self._validated = np.array([bool(self.phenotypes[p].get("validated")) for p in self.meta_ids])
91
+
92
+ # Part B: resolved code descriptions + algorithm components (optional sidecars).
93
+ self.code_desc = {}
94
+ if CODE_DESC.exists():
95
+ for line in CODE_DESC.open():
96
+ r = json.loads(line)
97
+ self.code_desc[f"{r['code_type_id']}|{r['sub_type_id']}|{(r['code'] or '').upper()}"] = r
98
+ self.cipher_uqid = {}
99
+ if CIPHER_LINKS.exists():
100
+ for line in CIPHER_LINKS.open():
101
+ r = json.loads(line)
102
+ self.cipher_uqid[r["phenotype_id"]] = r["uqid"]
103
+
104
+ # Extracted CIPHER algorithm components (methods / related diseases /
105
+ # validation / adjudication / data period); one record per phenotype.
106
+ self.algo_components = {}
107
+ if ALGO_COMPONENTS.exists():
108
+ for line in ALGO_COMPONENTS.open():
109
+ r = json.loads(line)
110
+ self.algo_components[r["phenotype_id"]] = r
111
+
112
+ # Query embedder must match how the documents were encoded (model +
113
+ # pooling + prefix + max-seq all travel in config.json).
114
+ model_name = os.environ.get("ENCODE_PARTA_MODEL", self.config["model"])
115
+ self.model_name = model_name
116
+ self.embedder = embedder or DenseEmbedder(
117
+ model_name,
118
+ pooling=self.config.get("pooling", "default"),
119
+ query_prefix=self.config.get("query_prefix", ""),
120
+ max_seq_length=self.config.get("max_seq_length"))
121
+
122
+ def categories(self) -> list[str]:
123
+ return sorted(set(self._cat.tolist()))
124
+
125
+ # -- retrieval ---------------------------------------------------------
126
+ def search(self, query: str, k: int = 10, categories: set[str] | None = None,
127
+ validated_only: bool = False) -> dict:
128
+ q = self.embedder.encode([query])[0]
129
+ meta_score = self.meta_mat @ q # (n_pheno,)
130
+
131
+ # Max code-evidence similarity aggregated to phenotype level.
132
+ code_sims = self.code_mat @ q # (n_code,)
133
+ code_score = np.zeros(len(self.meta_ids), dtype=np.float32)
134
+ valid = self._code_row >= 0
135
+ np.maximum.at(code_score, self._code_row[valid], code_sims[valid])
136
+
137
+ retrieval = 0.5 * meta_score + 0.5 * code_score # bi-encoder candidate score
138
+
139
+ mask = np.ones(len(self.meta_ids), dtype=bool)
140
+ if categories:
141
+ mask &= np.isin(self._cat, list(categories))
142
+ if validated_only:
143
+ mask &= self._validated
144
+
145
+ candidates = np.flatnonzero(mask)
146
+ results = []
147
+ if len(candidates):
148
+ candidate_scores = retrieval[candidates]
149
+ for j in np.argsort(candidate_scores)[::-1][:k]:
150
+ i = candidates[j]
151
+ results.append(self._result(self.meta_ids[i], float(meta_score[i]),
152
+ float(code_score[i]),
153
+ float(candidate_scores[j])))
154
+ return {
155
+ "query": query,
156
+ "model": self.model_name,
157
+ "count": len(results),
158
+ "results": results,
159
+ }
160
+
161
+ def _result(self, pid, m, c, relevance) -> dict:
162
+ p = self.phenotypes[pid]
163
+ evidence = [
164
+ {"code_system": g.get("code_type_label"),
165
+ "code_count": g.get("code_count") or len(g.get("codes", [])),
166
+ "samples": [x.get("code") for x in g.get("codes", [])[:6]]}
167
+ for g in p.get("associated_code_groups", [])
168
+ ]
169
+ return {
170
+ "phenotype_id": pid,
171
+ "title": p.get("title"),
172
+ "category": p.get("category"),
173
+ "validated": p.get("validated"),
174
+ "summary": self._summary(p),
175
+ "keywords": p.get("keywords") or [],
176
+ "scores": {"relevance": round(relevance, 4),
177
+ "metadata": round(m, 4), "code_evidence": round(c, 4)},
178
+ "code_systems": [e["code_system"] for e in evidence],
179
+ "code_evidence": evidence,
180
+ "warnings": [],
181
+ }
182
+
183
+ @staticmethod
184
+ def _summary(p: dict) -> str:
185
+ """Grounded summary drawn strictly from source metadata (no generation)."""
186
+ desc = (p.get("description") or p.get("algorithm_description") or "").strip()
187
+ if len(desc) > 360:
188
+ desc = desc[:360].rsplit(" ", 1)[0] + "…"
189
+ return desc
190
+
191
+ def _algo_component_rows(self, pid: int) -> list[dict]:
192
+ """Source-grounded algorithm components as ordered (label, value) rows;
193
+ omit absent fields (never fabricate). Lists are joined for display."""
194
+ rec = self.algo_components.get(pid)
195
+ if not rec:
196
+ return []
197
+ rows = []
198
+ for key, label in _ALGO_FIELDS:
199
+ val = rec.get(key)
200
+ if not val:
201
+ continue
202
+ if isinstance(val, list):
203
+ text = ", ".join(map(str, val))
204
+ elif isinstance(val, dict):
205
+ text = (f"{val.get('start', '?')} to {val.get('end', '?')}"
206
+ if ("start" in val or "end" in val)
207
+ else ", ".join(f"{k}: {v}" for k, v in val.items()))
208
+ else:
209
+ text = str(val)
210
+ rows.append({"label": label, "value": text})
211
+ return rows
212
+
213
+ def _cipher_url(self, pid: int, title: str | None) -> str:
214
+ uqid = self.cipher_uqid.get(pid)
215
+ if uqid:
216
+ slug = re.sub(r"[^A-Za-z0-9]", "_", title or "")
217
+ return f"{CIPHER_BASE}/web/cipher/phenotype-viewer?uqid={uqid}&name={slug}"
218
+ return CIPHER_BASE + "/"
219
+
220
+ def _code_display(self, code_type_id, sub_type_id, entry: dict) -> tuple[str | None, str]:
221
+ """Resolve a code to (description, source), replacing 'unsupported_code_system'
222
+ with a real description where one exists and an honest note otherwise."""
223
+ status = entry.get("label_status")
224
+ labels = entry.get("labels") or []
225
+ if status in ("exact", "prefix_expanded") and labels:
226
+ return labels[0], status
227
+ if status == "unsupported_code_system":
228
+ hit = self.code_desc.get(
229
+ f"{code_type_id}|{sub_type_id}|{(entry.get('code') or '').upper()}")
230
+ if hit:
231
+ return hit["description"], hit["source"]
232
+ return _UNRESOLVED_NOTE.get(code_type_id, ("No standard description", "unresolved"))
233
+ return (labels[0] if labels else None), status
234
+
235
+ # -- detail ------------------------------------------------------------
236
+ def _code_graphable(self, ct: int | None, code: str | None) -> bool:
237
+ """Does this specific code have a knowledge-graph? ICD diagnosis (460/461)
238
+ resolves to an ICD family; medication terms (466) can use a packaged map
239
+ or a conservative RxNAV lookup when the user opens the graph."""
240
+ if ct in (460, 461):
241
+ return True
242
+ if ct == 466:
243
+ return bool((code or "").strip())
244
+ return False
245
+
246
+ def _code_group_detail(self, g: dict) -> dict:
247
+ ct, sub = g.get("code_type_id"), g.get("sub_type_id")
248
+ codes, resolved = [], 0
249
+ for x in g.get("codes", [])[:200]:
250
+ desc, source = self._code_display(ct, sub, x)
251
+ if desc and source not in ("study_specific", "unresolved", "label_missing") \
252
+ and not source.startswith("needs_vocab"):
253
+ resolved += 1
254
+ codes.append({"code": x.get("code"), "description": desc,
255
+ "description_source": source, "label_status": x.get("label_status"),
256
+ "graphable": self._code_graphable(ct, x.get("code"))})
257
+ return {"code_system": g.get("code_type_label"), "sub_type": g.get("sub_type_label"),
258
+ "code_count": g.get("code_count") or len(g.get("codes", [])),
259
+ "resolved_count": resolved, "codes": codes,
260
+ "graphable": any(c["graphable"] for c in codes)}
261
+
262
+ def phenotype(self, pid: int) -> dict | None:
263
+ p = self.phenotypes.get(pid)
264
+ if not p:
265
+ return None
266
+ return {
267
+ "phenotype_id": pid,
268
+ "algorithm_id": p.get("algorithm_id"),
269
+ "title": p.get("title"),
270
+ "category": p.get("category"),
271
+ "validated": p.get("validated"),
272
+ "validation_description": p.get("validation_description"),
273
+ "description": p.get("description"),
274
+ "algorithm_description": p.get("algorithm_description"),
275
+ "keywords": p.get("keywords") or [],
276
+ "authors": p.get("authors") or [],
277
+ "publications": p.get("publications") or [],
278
+ "population_description": p.get("population_description"),
279
+ "last_modified": p.get("last_modified"),
280
+ "cipher_url": self._cipher_url(pid, p.get("title")),
281
+ "algorithm_components": self._algo_component_rows(pid),
282
+ "code_groups": [self._code_group_detail(g)
283
+ for g in p.get("associated_code_groups", [])],
284
+ }
285
+
286
+ # -- phenotype code hierarchy (phecode -> ICD main -> ICD sub) ----------
287
+ def phenotype_code_graph(self, pid: int, focus: str | None = None) -> dict | None:
288
+ """A node+edge tree of the phenotype's diagnosis codes: the phenotype (as
289
+ the phecode-level concept) -> each ICD main category (3-char stem) -> the
290
+ specific ICD sub-codes under it (460 ICD-9, 461 ICD-10).
291
+
292
+ To stay readable it stays bounded: when a code is clicked (`focus`) only
293
+ that code's main branch is expanded (sub-codes windowed around the focus);
294
+ the other mains collapse to a labelled count so you still see the breadth.
295
+ Overflow within a branch is shown as a single "+N more" stub (an edge to a
296
+ count, not every node). `path` = the root→main→code chain to highlight."""
297
+ p = self.phenotypes.get(pid)
298
+ if not p:
299
+ return None
300
+ FOCUS_SUBS, MAIN_SUBS, MAX_MAINS = 14, 6, 20
301
+ focus = (focus or "").strip() or None
302
+ focus_main = focus.split(".")[0] if focus else None
303
+ root = "pheno"
304
+ title = p.get("title") or f"Phenotype {pid}"
305
+ nodes = [{"id": root, "label": title[:60], "sub": "CIPHER phenotype (phecode)",
306
+ "tier": 0, "current": focus is None, "path": True}]
307
+ edges, path = [], [root]
308
+ note = None
309
+
310
+ for g in p.get("associated_code_groups", []):
311
+ ct = g.get("code_type_id")
312
+ if ct not in (460, 461):
313
+ continue
314
+ system = "ICD-9" if ct == 460 else "ICD-10"
315
+ sub_type = g.get("sub_type_id")
316
+ by_main: dict[str, list[dict]] = {}
317
+ for x in g.get("codes", []):
318
+ code = (x.get("code") or "").strip()
319
+ if code:
320
+ by_main.setdefault(code.split(".")[0], []).append(x)
321
+ # focus can be a leaf code (clicked in the drawer) or a main category
322
+ # (clicked to expand it) — both expand that main; a leaf also highlights.
323
+ group_has_focus = focus is not None and focus_main in by_main
324
+ focus_is_leaf = group_has_focus and any(x.get("code") == focus for x in by_main[focus_main])
325
+
326
+ mains = sorted(by_main)
327
+ sel_mains = mains[:MAX_MAINS]
328
+ if group_has_focus and focus_main not in sel_mains:
329
+ sel_mains = sel_mains[:MAX_MAINS - 1] + [focus_main]
330
+ if len(mains) > len(sel_mains):
331
+ note = "Some categories are truncated."
332
+
333
+ for cat in sel_mains:
334
+ main_id = f"{system}:{cat}"
335
+ subs = by_main[cat]
336
+ is_focus_main = group_has_focus and cat == focus_main
337
+ # When focused, only the focused branch expands; others collapse.
338
+ collapse = focus is not None and not is_focus_main
339
+ nodes.append({"id": main_id, "label": cat, "tier": 1, "current": False,
340
+ "sub": (f"{system} · {len(subs)} code" + ("s" if len(subs) != 1 else "")
341
+ if collapse else f"{system} category"),
342
+ "path": is_focus_main, "nav": {"code": cat, "code_type": system}})
343
+ edges.append([root, main_id])
344
+ if is_focus_main:
345
+ path.append(main_id)
346
+ if collapse:
347
+ continue
348
+
349
+ cap = FOCUS_SUBS if is_focus_main else MAIN_SUBS
350
+ if is_focus_main and focus_is_leaf and len(subs) > cap: # window around the focus
351
+ idx = next(i for i, x in enumerate(subs) if x.get("code") == focus)
352
+ start = max(0, min(idx - cap // 2, len(subs) - cap))
353
+ sel = subs[start:start + cap]
354
+ else:
355
+ sel = subs[:cap]
356
+ for x in sel:
357
+ desc, _ = self._code_display(ct, sub_type, x)
358
+ code = x["code"]
359
+ sid = f"{system}:{code}"
360
+ is_cur = code == focus
361
+ nodes.append({"id": sid, "label": code, "sub": desc or "", "tier": 2,
362
+ "current": is_cur, "path": is_cur,
363
+ "nav": {"code": code, "code_type": system}})
364
+ edges.append([main_id, sid])
365
+ if is_cur:
366
+ path.append(sid)
367
+ if len(subs) > len(sel): # overflow stub, not every node
368
+ more_id = f"more:{main_id}"
369
+ nodes.append({"id": more_id, "label": f"+{len(subs) - len(sel)} more",
370
+ "sub": "", "tier": 2, "more": True})
371
+ edges.append([main_id, more_id])
372
+
373
+ if len(nodes) == 1:
374
+ return {"available": False, "kind": "phenotype", "code": str(pid),
375
+ "reason": "This phenotype has no ICD diagnosis codes to chart."}
376
+ return {"available": True, "kind": "phenotype", "code": str(pid), "title": title,
377
+ "subtitle": "Phecode → ICD main categories → ICD sub-codes"
378
+ + (" (click a category to expand it)" if focus else ""),
379
+ "nodes": nodes, "edges": edges, "path": path, "note": note}
src/backend/graph.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Knowledge graph: click a code -> its parent/child ontology as nodes + edges.
2
+
3
+ Every builder returns one uniform shape the frontend renders as a node-edge tree:
4
+
5
+ {available, kind, title, subtitle,
6
+ nodes: [{id, label, sub, tier, current}], edges: [[parentId, childId], ...],
7
+ note}
8
+
9
+ Source-grounded edge families, built only from data in the repo (VA-faced: CIPHER
10
+ + VA CDW + RxNorm + LOINC — no OMOP/UMLS):
11
+
12
+ - Diagnosis (ICD-9/10) & ICD-9-Proc: dotted-code family (category stem -> the
13
+ codes sharing it), descriptions from the VA dictionary that backs code search.
14
+ - Procedures (ICD-10-PCS): 7-char positional codes -> the 3-char "table" family.
15
+ - Medication: RxNorm ingredient(s) -> the drug -> sibling products, from the
16
+ med_ingredient_map (drug names) and rxcui_ingredient_map (numeric RxNorm codes).
17
+ - Lab (LOINC): analyte COMPONENT -> the LOINC codes measuring it (loinc_terms).
18
+
19
+ A code with no known relations returns available:false with an honest reason —
20
+ never an invented tree. CPT (range-based, no clean prefix hierarchy) and VA-local
21
+ lab/med SIDs are honest gaps.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ from collections import defaultdict
28
+ from pathlib import Path
29
+
30
+ from .rxnav import RxNavClient
31
+
32
+ ROOT = Path(__file__).resolve().parents[2]
33
+ CANON = ROOT / "artifacts" / "part_a_canonical"
34
+ MED_MAP = CANON / "med_ingredient_map.jsonl"
35
+ RXCUI_MAP = CANON / "rxcui_ingredient_map.jsonl"
36
+ LOINC_TERMS = CANON / "loinc_terms.jsonl"
37
+ DIAG_RECORDS = ROOT / "artifacts" / "code_search" / "diagnosis" / "records.jsonl"
38
+ PROC_RECORDS = ROOT / "artifacts" / "code_search" / "procedure" / "records.jsonl"
39
+
40
+ MEMBER_CAP = 26 # sibling codes shown per family (windowed around the clicked code; rest -> "+N more")
41
+
42
+
43
+ def _dotted_cat(code: str) -> str:
44
+ """Category stem for a dotted code (E11.9 -> E11, 345.01 -> 345)."""
45
+ return code.split(".")[0].strip()
46
+
47
+
48
+ def _pcs_cat(code: str) -> str:
49
+ """ICD-10-PCS 'table' = first 3 positional chars (0SGK44Z -> 0SG)."""
50
+ return code[:3]
51
+
52
+
53
+ def _load_jsonl(path: Path):
54
+ if path.exists():
55
+ with path.open(encoding="utf-8") as handle:
56
+ for line in handle:
57
+ if line.strip():
58
+ yield json.loads(line)
59
+
60
+
61
+ class KnowledgeGraph:
62
+ def __init__(self, diagnosis_records=None, procedure_records=None,
63
+ rxnav_client: RxNavClient | None = None):
64
+ # --- medication (RxNorm) -----------------------------------------
65
+ self.med = {str(r["med_code"]).upper(): r for r in _load_jsonl(MED_MAP)}
66
+ self.rxcui = {str(r["rxcui"]): r for r in _load_jsonl(RXCUI_MAP)}
67
+ self.rxnav = rxnav_client or RxNavClient()
68
+
69
+ # --- lab (LOINC): term table + analyte(component) -> codes -------
70
+ self.loinc: dict[str, dict] = {}
71
+ self.loinc_by_comp: dict[str, list[str]] = defaultdict(list)
72
+ for r in _load_jsonl(LOINC_TERMS):
73
+ self.loinc[r["loinc"]] = r
74
+ self.loinc_by_comp[r["component"].lower()].append(r["loinc"])
75
+
76
+ # --- ICD diagnosis (dotted) --------------------------------------
77
+ self.icd, self.icd_cat = self._index_codes(
78
+ diagnosis_records if diagnosis_records is not None else _load_jsonl(DIAG_RECORDS),
79
+ keep="ICD", cat_fn=_dotted_cat)
80
+
81
+ # --- procedures: ICD-10-PCS (3-char) + ICD-9-Proc (dotted) -------
82
+ self.pcs, self.pcs_cat = {}, defaultdict(list)
83
+ self.icd9p, self.icd9p_cat = {}, defaultdict(list)
84
+ for r in (procedure_records if procedure_records is not None else _load_jsonl(PROC_RECORDS)):
85
+ ct = str(r.get("code_type", ""))
86
+ desc = r.get("description", "")
87
+ for code in str(r.get("code", "")).split(","):
88
+ code = code.strip()
89
+ if not code:
90
+ continue
91
+ if "PCS" in ct and code not in self.pcs:
92
+ self.pcs[code] = {"code_type": "ICD-10-PCS", "description": desc}
93
+ self.pcs_cat[_pcs_cat(code)].append(code)
94
+ elif "ICD-9" in ct and "." in code and code not in self.icd9p:
95
+ self.icd9p[code] = {"code_type": "ICD-9-Proc", "description": desc}
96
+ self.icd9p_cat[_dotted_cat(code)].append(code)
97
+
98
+ @staticmethod
99
+ def _index_codes(records, keep: str, cat_fn):
100
+ table: dict[str, dict] = {}
101
+ buckets: dict[str, list[str]] = defaultdict(list)
102
+ for r in records:
103
+ ct = str(r.get("code_type", ""))
104
+ if keep not in ct:
105
+ continue
106
+ desc = r.get("description", "")
107
+ for code in str(r.get("code", "")).split(","): # split merged rows
108
+ code = code.strip()
109
+ if code and code not in table:
110
+ table[code] = {"code_type": ct, "description": desc}
111
+ buckets[cat_fn(code)].append(code)
112
+ return table, buckets
113
+
114
+ # -- routing -----------------------------------------------------------
115
+ def neighbors(self, code: str, code_type: str | None = None,
116
+ drug_name: str | None = None) -> dict:
117
+ code = (code or "").strip()
118
+ if not code:
119
+ return self._none(code, "No code given.")
120
+ ct = (code_type or "").upper()
121
+
122
+ if "PCS" in ct or (not ct and code in self.pcs):
123
+ return self._family(code.split(",")[0].strip(), self.pcs, self.pcs_cat, _pcs_cat,
124
+ "procedure", "ICD-10-PCS procedure table — codes sharing this 3-character root.")
125
+ if ("ICD" in ct and "PROC" in ct) or ("ICD-9" in ct and code in self.icd9p):
126
+ return self._family(code.split(",")[0].strip(), self.icd9p, self.icd9p_cat, _dotted_cat,
127
+ "procedure", "ICD-9 procedure family — codes in this category.")
128
+ if "ICD" in ct or (not ct and (code in self.icd or _dotted_cat(code) in self.icd_cat)):
129
+ return self._family(code.split(",")[0].strip(), self.icd, self.icd_cat, _dotted_cat,
130
+ "icd", "Diagnosis code family — the codes in this ICD category.")
131
+ if "LOINC" in ct or code.split(",")[0].strip() in self.loinc:
132
+ return self._loinc(code.split(",")[0].strip())
133
+ if code.upper() in self.med or code in self.rxcui:
134
+ return self._med(code)
135
+ if self.rxnav.supports(code, code_type):
136
+ mapping = self.rxnav.resolve(code, code_type, drug_name)
137
+ if mapping:
138
+ return self._med(code, mapping)
139
+ return self._none(code, "No unambiguous RxNAV/RxNorm ingredient mapping is available for "
140
+ f"{drug_name or code}.", "medication")
141
+ return self._none(code, "No parent/child relations available for this code in the "
142
+ "CIPHER + VA + RxNorm + LOINC data. (CPT and VA-local lab/med "
143
+ "SIDs have no simple hierarchy here.)")
144
+
145
+ # -- dotted / positional code family (nodes + edges) -------------------
146
+ def _family(self, code, table, buckets, cat_fn, kind, subtitle) -> dict:
147
+ cat = cat_fn(code)
148
+ members = sorted(buckets.get(cat, []))
149
+ if not members:
150
+ return self._none(code, f"No {kind} family found for {code}.", kind)
151
+ sel = self._window(members, code, MEMBER_CAP) # siblings around the clicked code
152
+ root_desc = (table.get(cat) or {}).get("description")
153
+ nodes = [{"id": cat, "label": cat, "tier": 0, "current": cat == code, "path": True,
154
+ "sub": root_desc or f"{table.get(code, {}).get('code_type', 'ICD')} category {cat}"}]
155
+ edges, path = [], [cat]
156
+ for c in sel:
157
+ if c == cat:
158
+ continue
159
+ is_cur = c == code
160
+ nodes.append({"id": c, "label": c, "sub": table[c]["description"], "tier": 1,
161
+ "current": is_cur, "path": is_cur,
162
+ "nav": {"code": c, "code_type": table[c]["code_type"]}})
163
+ edges.append([cat, c])
164
+ if is_cur:
165
+ path.append(c)
166
+ if len(members) > len(sel):
167
+ nodes.append({"id": f"more:{cat}", "label": f"+{len(members) - len(sel)} more",
168
+ "sub": "", "tier": 1, "more": True})
169
+ edges.append([cat, f"more:{cat}"])
170
+ return {"available": True, "kind": kind, "code": code, "title": code, "subtitle": subtitle,
171
+ "nodes": nodes, "edges": edges, "path": path, "note": None}
172
+
173
+ @staticmethod
174
+ def _window(items: list[str], focus: str, cap: int) -> list[str]:
175
+ """Up to `cap` items centred on `focus` (so the clicked code keeps context)."""
176
+ if len(items) <= cap:
177
+ return items
178
+ if focus in items:
179
+ idx = items.index(focus)
180
+ start = max(0, min(idx - cap // 2, len(items) - cap))
181
+ return items[start:start + cap]
182
+ return items[:cap]
183
+
184
+ # -- LOINC lab: analyte(component) -> the codes measuring it -----------
185
+ def _loinc(self, code) -> dict:
186
+ term = self.loinc.get(code)
187
+ if not term:
188
+ return self._none(code, f"{code} is not in the LOINC table.", "lab")
189
+ comp = term["component"]
190
+ members = sorted(self.loinc_by_comp.get(comp.lower(), [code]))
191
+ sel = self._window(members, code, MEMBER_CAP)
192
+ comp_id = "comp:" + comp
193
+ nodes = [{"id": comp_id, "label": comp, "tier": 0, "current": False, "path": True,
194
+ "sub": ("LOINC component · " + term.get("class", "")).strip(" ·")}]
195
+ edges, path = [], [comp_id]
196
+ for c in sel:
197
+ t = self.loinc.get(c, {})
198
+ sub = " · ".join(x for x in [t.get("system"), t.get("name")] if x)[:90]
199
+ is_cur = c == code
200
+ nodes.append({"id": c, "label": c, "sub": sub, "tier": 1, "current": is_cur,
201
+ "path": is_cur, "nav": {"code": c, "code_type": "LOINC"}})
202
+ edges.append([comp_id, c])
203
+ if is_cur:
204
+ path.append(c)
205
+ if len(members) > len(sel):
206
+ nodes.append({"id": "more:" + comp_id, "label": f"+{len(members) - len(sel)} more",
207
+ "sub": "", "tier": 1, "more": True})
208
+ edges.append([comp_id, "more:" + comp_id])
209
+ return {"available": True, "kind": "lab", "code": code, "title": code,
210
+ "subtitle": f"Lab codes measuring {comp} (LOINC analyte).",
211
+ "nodes": nodes, "edges": edges, "path": path, "note": None}
212
+
213
+ # -- medication: ingredient(s) -> drug -> sibling products ------------
214
+ def _med(self, code, mapping=None) -> dict:
215
+ e = mapping or self.med.get(code.upper()) or self.rxcui.get(code)
216
+ if not e:
217
+ return self._none(code, f"No RxNorm ingredient mapping for {code}.", "medication")
218
+ label = e.get("name") or code
219
+ ings = e.get("ingredients", [])
220
+ if not ings:
221
+ return self._none(code, f"No RxNorm ingredient is mapped for {code}.", "medication")
222
+ nodes, edges, seen = [], [], set()
223
+ drug_id = "drug:" + str(code)
224
+ path = [drug_id]
225
+ for ing in ings:
226
+ iid = "ing:" + str(ing.get("rxcui") or ing["name"])
227
+ nodes.append({"id": iid, "label": ing["name"], "sub": "RxNorm ingredient",
228
+ "tier": 0, "current": False, "path": True})
229
+ edges.append([iid, drug_id])
230
+ path.append(iid)
231
+ nodes.append({"id": drug_id, "label": label, "sub": "this medication",
232
+ "tier": 1, "current": True, "path": True})
233
+ n_prod = 0
234
+ for ing in ings:
235
+ iid = "ing:" + str(ing.get("rxcui") or ing["name"])
236
+ for prod in (e.get("child_drugs", {}) or {}).get(ing["name"], []):
237
+ if prod.upper() == label.upper() or prod in seen:
238
+ continue
239
+ seen.add(prod)
240
+ pid = "prod:" + prod
241
+ nodes.append({"id": pid, "label": prod, "sub": "", "tier": 2, "current": False})
242
+ edges.append([iid, pid])
243
+ n_prod += 1
244
+ source = e.get("mapping_source", "Packaged RxNorm mapping")
245
+ total_products = e.get("related_product_count", n_prod)
246
+ shown_products = e.get("related_products_shown", n_prod)
247
+ note = f"{total_products} related products."
248
+ if total_products > shown_products:
249
+ note += f" Showing the first {shown_products}."
250
+ return {"available": True, "kind": "medication", "code": code,
251
+ "title": label, "subtitle": "RxNorm ingredient(s) → this drug → other products built from them.",
252
+ "nodes": nodes, "edges": edges, "path": path,
253
+ "note": (note if n_prod else None), "source": source,
254
+ "mapped_rxcuis": e.get("matched_rxcuis", [])}
255
+
256
+ @staticmethod
257
+ def _none(code, reason, kind=None) -> dict:
258
+ return {"available": False, "code": code, "kind": kind, "reason": reason}
src/backend/retriever.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU embedding wrapper for the Vidul BGE_FT_VA model.
2
+
3
+ `DenseEmbedder` wraps the model used to encode queries at runtime (the documents
4
+ are encoded offline). The query must use the same pooling, prefix, and sequence
5
+ length convention as the stored vectors.
6
+
7
+ `pooling='cls'|'mean'` forces pooling for raw BERT dirs (e.g. bge_ft_va, which
8
+ ships without a pooling module); `query_prefix` is prepended before encoding.
9
+
10
+ Runtime inference is CPU-only.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import numpy as np
16
+ from sentence_transformers import SentenceTransformer, models
17
+
18
+
19
+ def load_st_model(model_name: str, pooling: str = "default",
20
+ max_seq_length: int | None = None) -> SentenceTransformer:
21
+ """Load a SentenceTransformer with an explicit pooling head when asked.
22
+ 'default' uses whatever the model dir declares (a raw BERT dir -> mean)."""
23
+ if pooling in (None, "", "default"):
24
+ model = SentenceTransformer(model_name, device="cpu")
25
+ else:
26
+ word = models.Transformer(model_name)
27
+ pool = models.Pooling(word.get_embedding_dimension(), pooling_mode=pooling)
28
+ model = SentenceTransformer(modules=[word, pool], device="cpu")
29
+ if max_seq_length:
30
+ model.max_seq_length = max_seq_length
31
+ return model
32
+
33
+
34
+ class DenseEmbedder:
35
+ def __init__(self, model_name: str, *, pooling: str = "default", query_prefix: str = "",
36
+ max_seq_length: int | None = None):
37
+ self.model_name = model_name
38
+ self.device = "cpu"
39
+ self.query_prefix = query_prefix or ""
40
+ self.model = load_st_model(model_name, pooling, max_seq_length)
41
+
42
+ def encode(self, texts: list[str]) -> np.ndarray:
43
+ # encode() is the runtime query path, so the query prefix applies here
44
+ # (documents get their prefix in the offline build script).
45
+ if self.query_prefix:
46
+ texts = [self.query_prefix + t for t in texts]
47
+ return self.model.encode(texts, normalize_embeddings=True,
48
+ convert_to_numpy=True).astype(np.float32)
src/backend/rxnav.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small, conservative RxNAV client for completing medication graph mappings.
2
+
3
+ RxNAV is queried only after ENCODE's packaged mappings do not resolve a
4
+ medication. Name lookups use RxNAV's exact-or-normalized mode; approximate
5
+ matches are deliberately excluded so the graph never turns a fuzzy text match
6
+ into an asserted medication relationship.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from functools import lru_cache
14
+ from typing import Callable
15
+ from urllib.error import HTTPError, URLError
16
+ from urllib.parse import urlencode
17
+ from urllib.request import Request, urlopen
18
+
19
+ RXNAV_BASE = "https://rxnav.nlm.nih.gov"
20
+ _NDC = re.compile(r"^\d{10,11}$")
21
+ _NAME_STRIP = re.compile(r"[%\"']")
22
+ _MAX_RELATED_PRODUCTS = 26
23
+
24
+ FetchJson = Callable[[str, dict[str, str]], dict | None]
25
+
26
+
27
+ class RxNavClient:
28
+ """Resolve NDCs or unambiguous medication names into RxNorm ingredients.
29
+
30
+ Network failures and unknown/ambiguous responses return ``None``. The
31
+ caller can therefore state that no mapping is available without fabricating
32
+ a relationship. Public lookups are cached for the life of the API process.
33
+ """
34
+
35
+ def __init__(self, fetch_json: FetchJson | None = None, base_url: str = RXNAV_BASE,
36
+ timeout: float = 4.0):
37
+ self.fetch_json = fetch_json
38
+ self.base_url = base_url.rstrip("/")
39
+ self.timeout = timeout
40
+
41
+ def supports(self, code: str, code_type: str | None) -> bool:
42
+ kind = (code_type or "").upper()
43
+ return "NDC" in kind or "MED" in kind or "DRUG" in kind or "RXNORM" in kind
44
+
45
+ def resolve(self, code: str, code_type: str | None, drug_name: str | None = None) -> dict | None:
46
+ kind = (code_type or "").upper()
47
+ if "NDC" in kind:
48
+ return self.resolve_ndc(code)
49
+ if "RXNORM" in kind and code.strip().isdigit():
50
+ return self.resolve_rxcui(code.strip())
51
+ if "MED" in kind or "DRUG" in kind:
52
+ return self.resolve_name(drug_name or code)
53
+ return None
54
+
55
+ @lru_cache(maxsize=4096)
56
+ def resolve_ndc(self, code: str) -> dict | None:
57
+ for ndc in self._ndc_candidates(code):
58
+ data = self._request("/REST/ndcstatus.json", {
59
+ "ndc": ndc,
60
+ "history": "1",
61
+ "altpkg": "1",
62
+ })
63
+ status = (data or {}).get("ndcStatus") or {}
64
+ rxcui = str(status.get("rxcui") or "").strip()
65
+ if not rxcui.isdigit():
66
+ continue
67
+ mapping = self.resolve_rxcui(rxcui)
68
+ if mapping:
69
+ return {**mapping, "matched_ndc": ndc,
70
+ "query_name": status.get("conceptName") or mapping["name"]}
71
+ return None
72
+
73
+ @lru_cache(maxsize=4096)
74
+ def resolve_name(self, name: str) -> dict | None:
75
+ cleaned = _NAME_STRIP.sub("", (name or "")).strip()
76
+ if not cleaned or len(cleaned) > 240:
77
+ return None
78
+ data = self._request("/REST/Prescribe/rxcui.json", {
79
+ "name": cleaned,
80
+ "search": "2", # exact; then RxNAV's normalized match when needed
81
+ })
82
+ ids = [str(value) for value in ((data or {}).get("idGroup") or {}).get("rxnormId") or []
83
+ if str(value).isdigit()]
84
+ # RxNAV can return several exact/normalized concepts. There is no score on
85
+ # this endpoint, so choosing one would be an ungrounded guess.
86
+ if len(ids) != 1:
87
+ return None
88
+ mapping = self.resolve_rxcui(ids[0])
89
+ return {**mapping, "query_name": cleaned} if mapping else None
90
+
91
+ @lru_cache(maxsize=4096)
92
+ def resolve_rxcui(self, rxcui: str) -> dict | None:
93
+ if not rxcui.isdigit():
94
+ return None
95
+ properties = self._request(f"/REST/rxcui/{rxcui}/properties.json", {})
96
+ props = (properties or {}).get("properties") or {}
97
+ name = str(props.get("name") or rxcui)
98
+ ingredients = self._related(rxcui, "IN")
99
+ if str(props.get("tty") or "").upper() == "IN" and not ingredients:
100
+ ingredients = [{"rxcui": rxcui, "name": name}]
101
+ if not ingredients:
102
+ return None
103
+
104
+ child_drugs: dict[str, list[str]] = {}
105
+ product_count = 0
106
+ for ingredient in ingredients:
107
+ products = self._related(ingredient["rxcui"], "SCD SBD")
108
+ names = sorted({p["name"] for p in products if p["name"].lower() != name.lower()},
109
+ key=str.casefold)
110
+ product_count += len(names)
111
+ child_drugs[ingredient["name"]] = names[:_MAX_RELATED_PRODUCTS]
112
+
113
+ shown_count = sum(len(products) for products in child_drugs.values())
114
+ return {
115
+ "name": name,
116
+ "matched_rxcuis": [rxcui],
117
+ "ingredients": ingredients,
118
+ "child_drugs": child_drugs,
119
+ "mapping_source": "RxNAV / RxNorm live lookup",
120
+ "related_product_count": product_count,
121
+ "related_products_shown": shown_count,
122
+ }
123
+
124
+ def _related(self, rxcui: str, tty: str) -> list[dict[str, str]]:
125
+ data = self._request(f"/REST/Prescribe/rxcui/{rxcui}/related.json", {"tty": tty})
126
+ values: list[dict[str, str]] = []
127
+ for group in ((data or {}).get("relatedGroup") or {}).get("conceptGroup") or []:
128
+ for concept in group.get("conceptProperties") or []:
129
+ related_id = str(concept.get("rxcui") or "")
130
+ related_name = str(concept.get("name") or "")
131
+ if related_id.isdigit() and related_name:
132
+ values.append({"rxcui": related_id, "name": related_name})
133
+ return values
134
+
135
+ @staticmethod
136
+ def _ndc_candidates(code: str) -> list[str]:
137
+ candidates = []
138
+ for value in (code or "").split(","):
139
+ digits = re.sub(r"\D", "", value)
140
+ if _NDC.fullmatch(digits) and digits not in candidates:
141
+ candidates.append(digits)
142
+ return candidates
143
+
144
+ def _request(self, path: str, params: dict[str, str]) -> dict | None:
145
+ if self.fetch_json:
146
+ return self.fetch_json(path, params)
147
+ url = f"{self.base_url}{path}"
148
+ if params:
149
+ url += "?" + urlencode(params)
150
+ request = Request(url, headers={"Accept": "application/json", "User-Agent": "ENCODE/1.0"})
151
+ try:
152
+ with urlopen(request, timeout=self.timeout) as response:
153
+ return json.load(response)
154
+ except (HTTPError, URLError, OSError, ValueError):
155
+ return None
src/frontend/app.js ADDED
@@ -0,0 +1,666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const $ = (s) => document.querySelector(s);
2
+ const el = (t, c, txt) => { const n = document.createElement(t); if (c) n.className = c; if (txt != null) n.textContent = txt; return n; };
3
+
4
+ const CATS = {
5
+ diagnosis: { label: "Diagnosis", placeholder: "e.g. Type 2 diabetes mellitus", examples: ["Type 2 diabetes mellitus", "Acute myocardial infarction", "Major depressive disorder"] },
6
+ medication: { label: "Medication", placeholder: "e.g. atorvastatin 40 mg", examples: ["Atorvastatin", "Metformin", "Lisinopril"] },
7
+ lab: { label: "Lab", placeholder: "e.g. hemoglobin a1c", examples: ["Hemoglobin A1c", "Serum creatinine", "LDL cholesterol"] },
8
+ procedure: { label: "Procedure", placeholder: "e.g. colonoscopy", examples: ["Colonoscopy", "Coronary artery bypass", "Chest x-ray"] },
9
+ phenotype: { label: "Phenotype", placeholder: "e.g. adults hospitalized with heart failure after 2020", examples: ["Adults hospitalized with heart failure", "New-onset atrial fibrillation", "Chronic kidney disease stage 3+"] },
10
+ };
11
+
12
+ const state = { category: "phenotype", query: "", results: [], annotations: {}, phenoCats: new Set(), codeTypeFilter: new Set() };
13
+
14
+ async function apiGet(path) {
15
+ const r = await fetch(path);
16
+ if (!r.ok) throw new Error(((await r.json().catch(() => ({}))).detail) || r.statusText);
17
+ return r.json();
18
+ }
19
+
20
+ // Column tooltips (VA usability: explain what each column means, no jargon).
21
+ const COL_HELP = {
22
+ "Add": "Add this code to your collected list — it persists across searches and categories.",
23
+ "Relevant": "Directly answers the query — a strong match.",
24
+ "Related": "Loosely related — a partial or indirect match.",
25
+ "Unsure": "Uncertain whether this result matches.",
26
+ "Rank": "Result order by relevance (1 = closest match).",
27
+ "Code Type": "The coding system this code belongs to (ICD-9/10, RxNorm, NDC, LOINC, CPT…).",
28
+ "Code": "The code identifier.",
29
+ "Description": "The code's human-readable description.",
30
+ "Relevance": "Semantic similarity (0–1): 0.8+ strong · 0.6–0.8 moderate · below 0.6 weak.",
31
+ };
32
+
33
+ const isPheno = () => state.category === "phenotype";
34
+
35
+ // -- code basket: collect codes/phenotypes across searches & categories -----
36
+ // (the VA "curate a working code list" workflow; persists in localStorage).
37
+ const BASKET_KEY = "encode_basket_v1";
38
+ let basket = (() => { try { return JSON.parse(localStorage.getItem(BASKET_KEY)) || {}; } catch (_) { return {}; } })();
39
+
40
+ const basketKey = (it) => `${it.kind}:${it.category}:${it.code}`;
41
+ const inBasket = (it) => basketKey(it) in basket;
42
+ function saveBasket() { localStorage.setItem(BASKET_KEY, JSON.stringify(basket)); updateBasketCount(); }
43
+ function toggleBasket(it) {
44
+ const k = basketKey(it);
45
+ if (k in basket) delete basket[k];
46
+ else basket[k] = { ...it, query: state.query, added: new Date().toISOString() };
47
+ saveBasket();
48
+ }
49
+ function updateBasketCount() { const b = $("#basket-count"); if (b) b.textContent = String(Object.keys(basket).length); }
50
+ function collectBtn(it, compact) {
51
+ const b = el("button", "collect-btn" + (compact ? " collect-icon" : ""));
52
+ b.type = "button";
53
+ const mark = el("span", "collect-mark");
54
+ const label = compact ? null : el("span", "collect-label");
55
+ mark.setAttribute("aria-hidden", "true");
56
+ b.appendChild(mark);
57
+ if (label) b.appendChild(label);
58
+ const sync = () => {
59
+ const on = inBasket(it);
60
+ b.classList.toggle("on", on);
61
+ b.setAttribute("aria-pressed", String(on));
62
+ b.setAttribute("aria-label", on ? "Remove from collection" : "Add to collection");
63
+ mark.textContent = on ? "✓" : "+";
64
+ if (label) label.textContent = on ? "Collected" : "Collect";
65
+ b.title = on ? "Remove from collected codes" : "Add to collected codes";
66
+ };
67
+ b.onclick = (e) => { e.stopPropagation(); toggleBasket(it); sync(); };
68
+ sync();
69
+ return b;
70
+ }
71
+
72
+ // -- category / mode switching --------------------------------------------
73
+ function applyCategory() {
74
+ const meta = CATS[state.category];
75
+ $("#search-for").textContent = isPheno()
76
+ ? "Discover a phenotype from a natural-language request"
77
+ : `Search for ${meta.label}`;
78
+ $("#query").placeholder = meta.placeholder;
79
+ $("#pheno-filters").classList.toggle("hidden", !isPheno());
80
+ clearResults();
81
+ renderEmpty();
82
+ }
83
+
84
+ function clearResults() {
85
+ state.results = []; state.annotations = {};
86
+ $("#results").innerHTML = "";
87
+ $("#status").textContent = "";
88
+ $("#submit-row").classList.add("hidden");
89
+ $("#export").classList.add("hidden");
90
+ $("#submit-msg").textContent = "";
91
+ }
92
+
93
+ function renderEmpty() {
94
+ const box = $("#empty");
95
+ if (state.results.length || state.query) { box.innerHTML = ""; return; }
96
+ box.innerHTML = "";
97
+ box.appendChild(el("h2", "empty-title", isPheno() ? "Discover phenotypes" : "Search medical codes"));
98
+ box.appendChild(el("p", "empty-sub", isPheno()
99
+ ? "Describe a cohort in plain language to find matching CIPHER phenotypes."
100
+ : "Enter a clinical concept above to find matching diagnosis, medication, lab, or procedure codes."));
101
+ box.appendChild(el("p", "empty-try", "Examples:"));
102
+ box.appendChild(exampleChips());
103
+ }
104
+
105
+ // -- search ---------------------------------------------------------------
106
+ async function runSearch() {
107
+ const q = $("#query").value.trim();
108
+ if (!q) return;
109
+ state.query = q; state.annotations = {};
110
+ $("#empty").innerHTML = "";
111
+ $("#status").textContent = "Searching…";
112
+ $("#results").innerHTML = "";
113
+ $("#submit-row").classList.add("hidden");
114
+ $("#export").classList.add("hidden");
115
+ $("#submit-msg").textContent = "";
116
+ const t0 = performance.now();
117
+ try {
118
+ const url = isPheno()
119
+ ? `/api/search?q=${encodeURIComponent(q)}&k=${$("#k").value}${phenoFilterParams()}`
120
+ : `/api/code/search?category=${state.category}&q=${encodeURIComponent(q)}&k=${$("#k").value}`;
121
+ const data = await apiGet(url);
122
+ (isPheno() ? renderPhenotypes : renderCodes)(data, (performance.now() - t0) / 1000);
123
+ } catch (e) { $("#status").textContent = `Error: ${e.message}`; }
124
+ }
125
+
126
+ function exampleChips() {
127
+ const wrap = el("div", "example-chips");
128
+ (CATS[state.category].examples || []).forEach((ex) => {
129
+ const b = el("button", "example-chip", ex);
130
+ b.onclick = () => { $("#query").value = ex; runSearch(); };
131
+ wrap.appendChild(b);
132
+ });
133
+ return wrap;
134
+ }
135
+
136
+ function phenoFilterParams() {
137
+ let p = "";
138
+ if (state.phenoCats.size) p += `&categories=${[...state.phenoCats].join(",")}`;
139
+ if ($("#validated").checked) p += `&validated_only=true`;
140
+ return p;
141
+ }
142
+
143
+ function afterResults(count) {
144
+ if (!count) { $("#submit-row").classList.add("hidden"); $("#export").classList.add("hidden"); return; }
145
+ $("#submit-row").classList.remove("hidden");
146
+ $("#export").classList.remove("hidden");
147
+ }
148
+
149
+ // -- code table renderer --------------------------------------------------
150
+ function renderCodes(data, secs) {
151
+ state.results = data.results;
152
+ state.codeTypeFilter = new Set(); // active code-type filters (empty = show all)
153
+ $("#status").innerHTML =
154
+ `${data.count.toLocaleString()} of ${data.total.toLocaleString()} results for <b>"${data.query}"</b> (${secs.toFixed(1)}s)`;
155
+ const box = $("#results");
156
+ box.innerHTML = "";
157
+ if (!data.results.length) { $("#status").textContent = `No results for "${data.query}".`; afterResults(0); return; }
158
+
159
+ box.appendChild(codeFilterBar(data.results));
160
+ const host = el("div"); host.id = "code-table-host";
161
+ box.appendChild(host);
162
+ drawCodeTable();
163
+ afterResults(data.results.length);
164
+ }
165
+
166
+ // Rows visible under the active code-type filter (narrows the retrieved set
167
+ // client-side; ranks stay the original semantic ranks so gaps are meaningful).
168
+ function activeCodeRows() {
169
+ const f = state.codeTypeFilter;
170
+ return f.size ? state.results.filter((r) => f.has(r.code_type)) : state.results;
171
+ }
172
+
173
+ // Which code-search rows open a knowledge graph. Diagnosis = ICD (full); procedure
174
+ // = ICD-10-PCS / ICD-9-Proc (CPT is range-based, no clean hierarchy); lab = LOINC
175
+ // (VA-Lab SIDs have none). Medication rows are resolved through the packaged
176
+ // maps first, then a conservative RxNAV lookup when a graph is requested.
177
+ function codeSearchGraphable(cat, ct) {
178
+ ct = ct || "";
179
+ if (cat === "diagnosis") return true;
180
+ if (cat === "procedure") return ct.includes("PCS") || ct.includes("ICD-9-Proc");
181
+ if (cat === "lab") return ct.includes("LOINC");
182
+ if (cat === "medication") return true;
183
+ return false;
184
+ }
185
+
186
+ // Active-filter chips: one per distinct code type in the result set (only when
187
+ // there's more than one to choose between). Toggling narrows the table live.
188
+ function codeFilterBar(rows) {
189
+ const counts = {};
190
+ rows.forEach((r) => { counts[r.code_type] = (counts[r.code_type] || 0) + 1; });
191
+ const types = Object.keys(counts).sort((a, b) => counts[b] - counts[a] || a.localeCompare(b));
192
+ const bar = el("div", "filter-bar");
193
+ if (types.length <= 1) return bar; // single code type — nothing to filter
194
+ bar.appendChild(el("span", "filter-bar-label", "Code type"));
195
+ types.forEach((t) => {
196
+ const chip = el("button", "fchip", `${t} · ${counts[t]}`);
197
+ chip.onclick = () => {
198
+ state.codeTypeFilter.has(t) ? state.codeTypeFilter.delete(t) : state.codeTypeFilter.add(t);
199
+ chip.classList.toggle("on", state.codeTypeFilter.has(t));
200
+ drawCodeTable(); syncFilterBar(bar);
201
+ };
202
+ bar.appendChild(chip);
203
+ });
204
+ const clear = el("button", "fchip-clear hidden", "Clear filters");
205
+ clear.onclick = () => {
206
+ state.codeTypeFilter.clear();
207
+ bar.querySelectorAll(".fchip.on").forEach((c) => c.classList.remove("on"));
208
+ drawCodeTable(); syncFilterBar(bar);
209
+ };
210
+ bar.appendChild(clear);
211
+ bar.appendChild(el("span", "filter-count"));
212
+ return bar;
213
+ }
214
+
215
+ function syncFilterBar(bar) {
216
+ const active = state.codeTypeFilter.size;
217
+ bar.querySelector(".fchip-clear").classList.toggle("hidden", !active);
218
+ bar.querySelector(".filter-count").textContent =
219
+ active ? `showing ${activeCodeRows().length} of ${state.results.length}` : "";
220
+ }
221
+
222
+ function drawCodeTable() {
223
+ const host = $("#code-table-host");
224
+ host.innerHTML = "";
225
+ const rows = activeCodeRows();
226
+ const table = el("table", "code-table");
227
+ const thead = el("thead");
228
+ const hr = el("tr");
229
+ ["Add", "Relevant", "Related", "Unsure", "Rank", "Code Type", "Code", "Description", "Relevance"]
230
+ .forEach((h) => { const th = el("th", null, h); if (COL_HELP[h]) th.title = COL_HELP[h]; hr.appendChild(th); });
231
+ thead.appendChild(hr);
232
+ table.appendChild(thead);
233
+
234
+ const tb = el("tbody");
235
+ rows.forEach((r) => {
236
+ const tr = el("tr");
237
+ const collectTd = el("td", "ann-cell");
238
+ collectTd.appendChild(collectBtn({ kind: "code", category: state.category, code: r.code, code_type: r.code_type, description: r.description }, true));
239
+ tr.appendChild(collectTd);
240
+ ["relevant", "related", "unsure"].forEach((kind) => {
241
+ const td = el("td", "ann-cell");
242
+ const cb = el("input"); cb.type = "checkbox"; cb.checked = !!(state.annotations[r.rank] || {})[kind];
243
+ cb.onchange = () => { const a = state.annotations[r.rank] || (state.annotations[r.rank] = { row: r }); a[kind] = cb.checked; };
244
+ td.appendChild(cb); tr.appendChild(td);
245
+ });
246
+ tr.appendChild(el("td", "col-rank", String(r.rank)));
247
+ tr.appendChild(el("td", "col-type", r.code_type));
248
+ const codeTd = el("td", "col-code");
249
+ if (codeSearchGraphable(state.category, r.code_type)) { // has a parent/child hierarchy
250
+ const c = el("button", "code-link code-cell-btn", r.code);
251
+ c.title = state.category === "medication" ? "View RxNorm ingredient mapping" : "View parent / child codes";
252
+ c.onclick = () => openGraph(r.code, r.code_type, r.description);
253
+ codeTd.appendChild(c);
254
+ } else { codeTd.textContent = r.code; }
255
+ tr.appendChild(codeTd);
256
+ tr.appendChild(el("td", "col-desc", r.description));
257
+ tr.appendChild(el("td", "col-rel", r.relevance.toFixed(4)));
258
+ tb.appendChild(tr);
259
+ });
260
+ table.appendChild(tb);
261
+ const wrap = el("div", "table-wrap");
262
+ wrap.appendChild(table);
263
+ host.appendChild(wrap);
264
+ if (!rows.length) host.appendChild(el("p", "filter-empty", "No results of that code type in the current set — adjust the filters above."));
265
+ }
266
+
267
+ // -- phenotype card renderer (single relevance + rank; no pipeline internals)
268
+ function renderPhenotypes(data, secs) {
269
+ state.results = data.results;
270
+ $("#status").innerHTML =
271
+ `${data.count} phenotype candidate(s) for <b>"${data.query}"</b> (${secs.toFixed(1)}s)`;
272
+ const box = $("#results");
273
+ box.innerHTML = "";
274
+ if (!data.results.length) { $("#status").textContent = `No candidates for "${data.query}".`; afterResults(0); return; }
275
+ data.results.forEach((res, i) => box.appendChild(phenoCard(res, i + 1)));
276
+ afterResults(data.results.length);
277
+ }
278
+
279
+ function phenoCard(res, rank) {
280
+ const c = el("article", "card");
281
+ const head = el("div", "card-head");
282
+ const left = el("div", "card-head-left");
283
+ left.appendChild(el("span", "rank-badge", `#${rank}`));
284
+ left.appendChild(el("h3", null, res.title || `Phenotype ${res.phenotype_id}`));
285
+ if (res.category) left.appendChild(el("span", "tag", res.category));
286
+ if (res.validated) left.appendChild(el("span", "tag tag-ok", "validated"));
287
+ head.appendChild(left);
288
+
289
+ const right = el("div", "card-head-right");
290
+ right.appendChild(el("span", "rel-label", "Relevance"));
291
+ right.appendChild(el("span", "rel-value", res.scores.relevance.toFixed(4)));
292
+ right.appendChild(annBox(res.phenotype_id, res));
293
+ head.appendChild(right);
294
+ c.appendChild(head);
295
+
296
+ if (res.summary) c.appendChild(el("p", "summary", res.summary));
297
+
298
+ if (res.code_evidence.length) {
299
+ const chips = el("div", "chips");
300
+ res.code_evidence.forEach((e) => chips.appendChild(el("span", "chip-static", `${e.code_system} · ${e.code_count}`)));
301
+ c.appendChild(chips);
302
+ }
303
+ res.warnings.forEach((w) => c.appendChild(el("div", "warning", `⚠ ${w}`)));
304
+
305
+ const foot = el("div", "card-foot");
306
+ const detail = el("button", "link", "View codes & detail →");
307
+ detail.onclick = () => openDrawer(res.phenotype_id);
308
+ const actions = el("div", "card-foot-actions");
309
+ actions.appendChild(detail);
310
+ actions.appendChild(collectBtn({ kind: "phenotype", category: "phenotype", code: String(res.phenotype_id), code_type: "CIPHER phenotype", description: res.title }, false));
311
+ foot.appendChild(actions);
312
+ foot.appendChild(el("span", "pid", `CIPHER phenotype #${res.phenotype_id}`));
313
+ c.appendChild(foot);
314
+ return c;
315
+ }
316
+
317
+ function annBox(pid, res) {
318
+ const box = el("div", "ann-box");
319
+ ["relevant", "related", "unsure"].forEach((kind) => {
320
+ const lbl = el("label", "ann-opt");
321
+ const cb = el("input"); cb.type = "checkbox";
322
+ cb.onchange = () => { const a = state.annotations[pid] || (state.annotations[pid] = { row: res }); a[kind] = cb.checked; };
323
+ lbl.appendChild(cb);
324
+ lbl.appendChild(el("span", null, kind[0].toUpperCase() + kind.slice(1)));
325
+ box.appendChild(lbl);
326
+ });
327
+ return box;
328
+ }
329
+
330
+ // -- annotations submit ---------------------------------------------------
331
+ async function submitAnnotations() {
332
+ const marked = Object.entries(state.annotations)
333
+ .filter(([, a]) => a.relevant || a.related || a.unsure);
334
+ if (!marked.length) { $("#submit-msg").textContent = "Mark at least one result first."; return; }
335
+ const annotator = $("#annotator").value;
336
+ let res;
337
+ if (isPheno()) {
338
+ const annotations = marked.map(([pid, a]) => ({
339
+ phenotype_id: Number(pid), title: (a.row || {}).title,
340
+ relevant: !!a.relevant, related: !!a.related, unsure: !!a.unsure,
341
+ }));
342
+ res = await postJSON("/api/annotations", { annotator, query: state.query, annotations });
343
+ } else {
344
+ const annotations = marked.map(([rank, a]) => ({
345
+ rank: Number(rank), code: a.row.code, code_type: a.row.code_type, description: a.row.description,
346
+ relevant: !!a.relevant, related: !!a.related, unsure: !!a.unsure,
347
+ }));
348
+ res = await postJSON("/api/code/annotations", { annotator, category: state.category, query: state.query, annotations });
349
+ }
350
+ $("#submit-msg").textContent = `✓ Saved ${res.saved} label(s) as ${res.annotator}.`;
351
+ }
352
+
353
+ async function postJSON(url, body) {
354
+ return (await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })).json();
355
+ }
356
+
357
+ function exportCsv() {
358
+ if (!state.results.length) return;
359
+ const esc = (s) => `"${String(s == null ? "" : s).replace(/"/g, '""')}"`;
360
+ const [header, rows] = isPheno()
361
+ ? [["rank", "phenotype_id", "title", "category", "validated", "relevance", "code_systems"],
362
+ state.results.map((r, i) => [i + 1, r.phenotype_id, r.title, r.category, r.validated, r.scores.relevance, (r.code_systems || []).join("; ")])]
363
+ : [["rank", "code_type", "code", "description", "relevance"],
364
+ state.results.map((r) => [r.rank, r.code_type, r.code, r.description, r.relevance])];
365
+ const csv = [header.join(","), ...rows.map((row) => row.map(esc).join(","))].join("\n");
366
+ const blob = new Blob([csv], { type: "text/csv" });
367
+ const a = document.createElement("a");
368
+ a.href = URL.createObjectURL(blob);
369
+ a.download = `encode_${state.category}_${(state.query || "export").replace(/\s+/g, "_").slice(0, 30)}.csv`;
370
+ a.click(); URL.revokeObjectURL(a.href);
371
+ }
372
+
373
+ // -- phenotype detail drawer (+ About) ------------------------------------
374
+ // Description sources that mean "resolved to a real description" vs an honest gap.
375
+ const RESOLVED_SRC = (s) => s && !["study_specific", "unresolved", "label_missing"].includes(s) && !s.startsWith("needs_vocab");
376
+
377
+ async function openDrawer(pid) {
378
+ const body = $("#drawer-body");
379
+ body.innerHTML = "Loading…";
380
+ showDrawer();
381
+ const p = await apiGet(`/api/phenotype/${pid}`);
382
+ body.innerHTML = "";
383
+ body.appendChild(el("h2", null, p.title));
384
+ const meta = el("div", "drawer-meta");
385
+ if (p.category) meta.appendChild(el("span", "tag", p.category));
386
+ if (p.validated) meta.appendChild(el("span", "tag tag-ok", "validated"));
387
+ meta.appendChild(el("span", "pid", `#${p.phenotype_id}`));
388
+ body.appendChild(meta);
389
+ if (p.description) { body.appendChild(el("h4", null, "Description")); body.appendChild(el("p", "summary", p.description)); }
390
+ if (p.population_description) { body.appendChild(el("h4", null, "Population")); body.appendChild(el("p", "summary", p.population_description)); }
391
+
392
+ if (p.publications.length) {
393
+ body.appendChild(el("h4", null, "Publications"));
394
+ const ul = el("ul"); p.publications.forEach((x) => ul.appendChild(el("li", null, x))); body.appendChild(ul);
395
+ }
396
+
397
+ // CIPHER algorithm components (professor 1.4): methods, related diseases,
398
+ // validation, adjudication, data period — source-grounded, absent fields omitted.
399
+ if ((p.algorithm_components || []).length) {
400
+ body.appendChild(el("h4", null, "Algorithm components"));
401
+ const box = el("div", "algo-components");
402
+ p.algorithm_components.forEach((row) => {
403
+ const r = el("div", "algo-row");
404
+ r.appendChild(el("span", "algo-key", row.label));
405
+ r.appendChild(el("span", "algo-val", row.value));
406
+ box.appendChild(r);
407
+ });
408
+ body.appendChild(box);
409
+ }
410
+
411
+ const cgHead = el("div", "cg-head");
412
+ cgHead.appendChild(el("h4", null, `Associated code groups (${p.code_groups.length})`));
413
+ // Phecode → ICD main → ICD sub tree for this phenotype (professor 2).
414
+ if (p.code_groups.some((g) => (g.code_system || "").includes("ICD"))) {
415
+ const gb = el("button", "graph-btn", "⤳ View code hierarchy");
416
+ gb.title = "Phecode → ICD main categories → ICD sub-codes";
417
+ gb.onclick = () => openPhenotypeGraph(p.phenotype_id, p.title);
418
+ cgHead.appendChild(gb);
419
+ }
420
+ body.appendChild(cgHead);
421
+ p.code_groups.forEach((g) => {
422
+ const det = el("details", "codegroup");
423
+ const cap = g.resolved_count ? ` · ${g.resolved_count}/${g.codes.length} described` : "";
424
+ det.appendChild(el("summary", null, `${g.code_system}${g.sub_type ? " / " + g.sub_type : ""} — ${g.code_count} codes${cap}`));
425
+ const table = el("div", "codes");
426
+ g.codes.forEach((x) => {
427
+ const row = el("div", "code-row");
428
+ // ICD codes -> this phenotype's phecode → ICD main → ICD sub tree (clicked
429
+ // code highlighted); medication codes -> RxNorm ingredient graph (prof 2/3).
430
+ if (x.graphable) {
431
+ const c = el("button", "code code-link", x.code);
432
+ const isIcd = (g.code_system || "").includes("ICD");
433
+ c.title = isIcd ? "Show in phecode → ICD hierarchy" : "View ingredient graph";
434
+ c.onclick = isIcd
435
+ ? () => openPhenotypeGraph(p.phenotype_id, p.title, x.code)
436
+ : () => openGraph(x.code, g.code_system);
437
+ row.appendChild(c);
438
+ } else {
439
+ row.appendChild(el("span", "code", x.code));
440
+ }
441
+ const desc = el("span", "code-label", x.description || `(${x.label_status})`);
442
+ if (!RESOLVED_SRC(x.description_source)) desc.classList.add("code-label-gap");
443
+ row.appendChild(desc);
444
+ table.appendChild(row);
445
+ });
446
+ det.appendChild(table);
447
+ body.appendChild(det);
448
+ });
449
+
450
+ // More information -> CIPHER original website (professor 1.5).
451
+ if (p.cipher_url) {
452
+ const more = el("div", "drawer-more");
453
+ more.appendChild(el("h4", null, "More information"));
454
+ const a = el("a", "cipher-link", "View this phenotype on CIPHER ↗");
455
+ a.href = p.cipher_url; a.target = "_blank"; a.rel = "noopener";
456
+ more.appendChild(a);
457
+ body.appendChild(more);
458
+ }
459
+ }
460
+
461
+ function showAbout() {
462
+ const body = $("#drawer-body");
463
+ body.innerHTML = "";
464
+ body.appendChild(el("h2", null, "About ENCODE"));
465
+ body.appendChild(el("p", "summary",
466
+ "ENCODE turns a plain-language clinical concept into ranked medical codes. Pick a category — Diagnosis, Medication, Lab, or Procedure — type a concept, and results are ordered by semantic relevance to your query."));
467
+ body.appendChild(el("p", "summary",
468
+ "Relevance is a single similarity score between your query and each code description. Switch to Phenotype to discover CIPHER phenotype definitions instead of individual codes."));
469
+ showDrawer();
470
+ }
471
+
472
+ // -- collected-codes basket drawer ----------------------------------------
473
+ function openBasket() {
474
+ const body = $("#drawer-body");
475
+ body.innerHTML = "";
476
+ const items = Object.values(basket);
477
+ body.appendChild(el("h2", null, `Collected codes (${items.length})`));
478
+ if (!items.length) {
479
+ body.appendChild(el("p", "summary",
480
+ "Collect codes and phenotypes with “+ Collect”. Your list persists across searches and categories, and exports as a CSV."));
481
+ showDrawer(); return;
482
+ }
483
+ const actions = el("div", "basket-actions");
484
+ const exp = el("button", "cipher-link", "Export CSV ↓"); exp.onclick = exportBasket;
485
+ const clr = el("button", "link", "Clear all");
486
+ clr.onclick = () => { if (confirm("Clear the whole collected list?")) { basket = {}; saveBasket(); openBasket(); } };
487
+ actions.appendChild(exp); actions.appendChild(clr);
488
+ body.appendChild(actions);
489
+
490
+ const list = el("div", "basket-list");
491
+ items.forEach((it) => {
492
+ const row = el("div", "basket-row");
493
+ const main = el("div", "basket-main");
494
+ main.appendChild(el("span", "chip-static", it.category));
495
+ main.appendChild(el("span", "code", it.code));
496
+ main.appendChild(el("span", "basket-desc", it.description || ""));
497
+ row.appendChild(main);
498
+ const rm = el("button", "basket-rm", "×"); rm.title = "Remove";
499
+ rm.onclick = () => { delete basket[basketKey(it)]; saveBasket(); openBasket(); };
500
+ row.appendChild(rm);
501
+ list.appendChild(row);
502
+ });
503
+ body.appendChild(list);
504
+ showDrawer();
505
+ }
506
+
507
+ function exportBasket() {
508
+ const items = Object.values(basket);
509
+ if (!items.length) return;
510
+ const esc = (s) => `"${String(s == null ? "" : s).replace(/"/g, '""')}"`;
511
+ const rows = [
512
+ "# ENCODE collected codes",
513
+ `# Exported: ${new Date().toISOString().slice(0, 16).replace("T", " ")}`,
514
+ `# Items: ${items.length}`,
515
+ ["kind", "category", "code_type", "code", "description", "source_query"].join(","),
516
+ ...items.map((it) => [it.kind, it.category, it.code_type, it.code, it.description, it.query].map(esc).join(",")),
517
+ ];
518
+ const blob = new Blob([rows.join("\n")], { type: "text/csv" });
519
+ const url = URL.createObjectURL(blob);
520
+ const a = document.createElement("a");
521
+ a.href = url; a.download = `encode_collected_${new Date().toISOString().slice(0, 10)}.csv`;
522
+ a.click(); URL.revokeObjectURL(url);
523
+ }
524
+
525
+ function showDrawer() { $("#drawer").classList.remove("hidden"); $("#overlay").classList.remove("hidden"); }
526
+ function closeDrawer() { $("#drawer").classList.add("hidden"); $("#overlay").classList.add("hidden"); }
527
+
528
+ // -- knowledge graph: click a code -> parent/child ontology (SVG node+edge) --
529
+ async function openGraph(code, codeType, drugName) {
530
+ const body = $("#graph-body");
531
+ body.innerHTML = "Loading…"; showGraph();
532
+ let g;
533
+ try {
534
+ g = await apiGet(`/api/graph?code=${encodeURIComponent(code)}${codeType ? "&code_type=" + encodeURIComponent(codeType) : ""}${drugName ? "&drug_name=" + encodeURIComponent(drugName) : ""}`);
535
+ } catch (e) { body.innerHTML = ""; body.appendChild(el("p", "summary", "Could not load the code graph.")); return; }
536
+ renderGraph(body, g, code);
537
+ }
538
+
539
+ async function openPhenotypeGraph(pid, title, focus) {
540
+ const body = $("#graph-body");
541
+ body.innerHTML = "Loading…"; showGraph();
542
+ let g;
543
+ try {
544
+ g = await apiGet(`/api/phenotype/${pid}/graph${focus ? "?focus=" + encodeURIComponent(focus) : ""}`);
545
+ } catch (e) { body.innerHTML = ""; body.appendChild(el("p", "summary", "Could not load the phenotype graph.")); return; }
546
+ renderGraph(body, g, title || `Phenotype ${pid}`);
547
+ }
548
+
549
+ function renderGraph(body, g, fallbackTitle) {
550
+ body.innerHTML = "";
551
+ body.appendChild(el("h2", null, g.title || fallbackTitle));
552
+ if (!g.available) {
553
+ body.appendChild(el("p", "summary", g.reason || "No parent/child relations available for this code."));
554
+ return;
555
+ }
556
+ if (g.subtitle) body.appendChild(el("p", "graph-sub", g.subtitle));
557
+ if (g.source) body.appendChild(el("p", "graph-note", `Mapping source: ${g.source}`));
558
+ body.appendChild(buildGraphSvg(g));
559
+ if (g.note) body.appendChild(el("p", "graph-note", g.note));
560
+ }
561
+
562
+ const SVGNS = "http://www.w3.org/2000/svg";
563
+ const svgEl = (t, a) => { const n = document.createElementNS(SVGNS, t); for (const k in a) n.setAttribute(k, a[k]); return n; };
564
+ const truncate = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + "…" : (s || ""));
565
+
566
+ // Tidy left-to-right tree: x = tier column, y = DFS leaf-stacking (parent centred
567
+ // on its children). Real nodes + edges; nodes with a `nav` hint re-centre on click.
568
+ function buildGraphSvg(g) {
569
+ const NODEW = 214, NODEH = 40, COLW = 274, ROWH = 52, PADX = 16, PADY = 16;
570
+ const pathSet = new Set(g.path || []);
571
+ const byId = {}; g.nodes.forEach((n) => (byId[n.id] = n));
572
+ const kids = {}; g.nodes.forEach((n) => (kids[n.id] = []));
573
+ const hasParent = {};
574
+ g.edges.forEach(([a, b]) => { if (kids[a]) kids[a].push(b); hasParent[b] = true; });
575
+ const roots = g.nodes.filter((n) => !hasParent[n.id]).map((n) => n.id);
576
+ const pos = {}; let leaf = 0;
577
+ const place = (id, seen) => {
578
+ if (seen.has(id)) return (pos[id] || { y: 0 }).y;
579
+ seen.add(id);
580
+ const cs = kids[id] || [];
581
+ const x = (byId[id].tier || 0) * COLW + PADX;
582
+ if (!cs.length) { const y = leaf * ROWH + PADY; leaf++; pos[id] = { x, y }; return y; }
583
+ const ys = cs.map((c) => place(c, seen));
584
+ const y = (ys[0] + ys[ys.length - 1]) / 2; pos[id] = { x, y }; return y;
585
+ };
586
+ const seen = new Set();
587
+ roots.forEach((r) => place(r, seen));
588
+ g.nodes.forEach((n) => { if (!pos[n.id]) { pos[n.id] = { x: (n.tier || 0) * COLW + PADX, y: leaf * ROWH + PADY }; leaf++; } });
589
+ const maxTier = Math.max(0, ...g.nodes.map((n) => n.tier || 0));
590
+ const W = (maxTier + 1) * COLW + PADX, H = Math.max(leaf * ROWH + PADY, 72);
591
+ const svg = svgEl("svg", { width: W, height: H, class: "graph-svg", viewBox: `0 0 ${W} ${H}` });
592
+
593
+ g.edges.forEach(([a, b]) => {
594
+ const pa = pos[a], pb = pos[b]; if (!pa || !pb) return;
595
+ const onPath = pathSet.has(a) && pathSet.has(b);
596
+ const x1 = pa.x + NODEW, y1 = pa.y + NODEH / 2, x2 = pb.x, y2 = pb.y + NODEH / 2, mx = (x1 + x2) / 2;
597
+ svg.appendChild(svgEl("path", { d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
598
+ class: "graph-edge" + (onPath ? " graph-edge-path" : "") }));
599
+ });
600
+ g.nodes.forEach((n) => {
601
+ const p = pos[n.id]; if (!p) return;
602
+ let cls = "graph-rect";
603
+ if (n.more) cls += " graph-rect-more";
604
+ else if (n.current) cls += " graph-rect-current";
605
+ else if (n.path) cls += " graph-rect-path";
606
+ else if (n.tier === 0) cls += " graph-rect-root";
607
+ const grp = svgEl("g", { transform: `translate(${p.x},${p.y})` });
608
+ grp.appendChild(svgEl("rect", { width: NODEW, height: NODEH, rx: 8, class: cls }));
609
+ const labCls = "graph-node-label" + (n.current ? " graph-node-cur" : "") + (n.more ? " graph-node-more" : "");
610
+ const lab = svgEl("text", { x: 12, y: 17, class: labCls }); lab.textContent = truncate(n.label, 30); grp.appendChild(lab);
611
+ if (n.sub) { const s = svgEl("text", { x: 12, y: 31, class: "graph-node-sub" }); s.textContent = truncate(n.sub, 36); grp.appendChild(s); }
612
+ const tt = svgEl("title"); tt.textContent = n.label + (n.sub ? " — " + n.sub : ""); grp.appendChild(tt);
613
+ if (n.nav && !n.current && !n.more) {
614
+ grp.setAttribute("class", "graph-node-clickable");
615
+ grp.onclick = (g.kind === "phenotype" && (n.tier || 0) === 1)
616
+ ? () => openPhenotypeGraph(g.code, g.title, n.nav.code) // re-expand this category branch
617
+ : () => openGraph(n.nav.code, n.nav.code_type); // drill to the code's full family
618
+ }
619
+ svg.appendChild(grp);
620
+ });
621
+ const wrap = el("div", "graph-scroll"); wrap.appendChild(svg);
622
+ return wrap;
623
+ }
624
+
625
+ function showGraph() { $("#graph-drawer").classList.remove("hidden"); $("#graph-overlay").classList.remove("hidden"); }
626
+ function closeGraph() { $("#graph-drawer").classList.add("hidden"); $("#graph-overlay").classList.add("hidden"); }
627
+
628
+ // -- phenotype sub-category filters ---------------------------------------
629
+ async function loadPhenoCategories() {
630
+ try {
631
+ const { categories } = await apiGet("/api/categories");
632
+ const box = $("#categories");
633
+ box.innerHTML = "";
634
+ categories.forEach((c) => {
635
+ const row = el("label", "filter-opt");
636
+ const cb = el("input"); cb.type = "checkbox";
637
+ cb.onchange = () => { cb.checked ? state.phenoCats.add(c) : state.phenoCats.delete(c); if (state.query && isPheno()) runSearch(); };
638
+ row.appendChild(cb); row.appendChild(el("span", null, c));
639
+ box.appendChild(row);
640
+ });
641
+ } catch (_) { /* phenotype index optional */ }
642
+ }
643
+
644
+ // -- wiring ---------------------------------------------------------------
645
+ $("#category").onchange = (e) => { state.category = e.target.value; state.query = ""; $("#query").value = ""; applyCategory(); };
646
+ $("#search").onclick = runSearch;
647
+ $("#query").addEventListener("keydown", (e) => { if (e.key === "Enter") runSearch(); });
648
+ $("#submit").onclick = submitAnnotations;
649
+ $("#export").onclick = exportCsv;
650
+ $("#about-btn").onclick = showAbout;
651
+ $("#basket-btn").onclick = openBasket;
652
+ $("#drawer-close").onclick = closeDrawer;
653
+ $("#overlay").onclick = closeDrawer;
654
+ $("#graph-close").onclick = closeGraph;
655
+ $("#graph-overlay").onclick = closeGraph;
656
+ $("#k").oninput = (e) => { $("#k-value").textContent = e.target.value; };
657
+ $("#k").onchange = () => { if (state.query) runSearch(); };
658
+ $("#validated").onchange = () => { if (state.query && isPheno()) runSearch(); };
659
+
660
+ async function init() {
661
+ loadPhenoCategories();
662
+ applyCategory();
663
+ updateBasketCount();
664
+ $("#query").focus();
665
+ }
666
+ init();
src/frontend/index.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>ENCODE — Embedding Empowered Code Search</title>
7
+ <link rel="stylesheet" href="/styles.css?v=demo1" />
8
+ </head>
9
+ <body>
10
+ <header class="topbar">
11
+ <span class="brand">ENCODE</span>
12
+ <span class="topbar-title">Embedding Empowered Code Search</span>
13
+ <button id="basket-btn" class="basket-btn">Collection <span id="basket-count" class="basket-count">0</span></button>
14
+ <button id="about-btn" class="about-btn">About</button>
15
+ </header>
16
+
17
+ <div class="layout">
18
+ <aside class="sidebar">
19
+ <label class="side-label" for="category">Code Search Category</label>
20
+ <select id="category" class="category-select">
21
+ <option value="phenotype">Phenotype</option>
22
+ <option value="diagnosis">Diagnosis</option>
23
+ <option value="medication">Medication</option>
24
+ <option value="lab">Lab</option>
25
+ <option value="procedure">Procedure</option>
26
+ </select>
27
+
28
+ <label class="side-label">Number of Results</label>
29
+ <output id="k-value" class="k-value">50</output>
30
+ <input id="k" type="range" min="10" max="100" step="10" value="50" />
31
+
32
+ <div id="pheno-filters" class="hidden">
33
+ <label class="side-label">Phenotype Category</label>
34
+ <div id="categories" class="filters"></div>
35
+ <label class="side-label">Validation</label>
36
+ <label class="filter-opt"><input id="validated" type="checkbox" /><span>Validated only</span></label>
37
+ </div>
38
+
39
+ <button id="export" class="export hidden">Export CSV</button>
40
+ </aside>
41
+
42
+ <main class="content">
43
+ <p id="search-for" class="search-for">Search for Diagnosis</p>
44
+ <div class="search-row">
45
+ <input id="query" class="query" type="text" autocomplete="off"
46
+ placeholder="e.g. Type 2 diabetes mellitus" />
47
+ <button id="search" class="search-btn">Search</button>
48
+ </div>
49
+
50
+ <p id="status" class="status"></p>
51
+ <section id="results" class="results"></section>
52
+ <div id="empty" class="empty"></div>
53
+
54
+ <div id="submit-row" class="submit-row hidden">
55
+ <input id="annotator" class="annotator" type="text" placeholder="Your name" />
56
+ <button id="submit" class="submit-btn">Submit Annotations</button>
57
+ <span id="submit-msg" class="submit-msg"></span>
58
+ </div>
59
+ </main>
60
+ </div>
61
+
62
+ <aside id="drawer" class="drawer hidden">
63
+ <button id="drawer-close" class="drawer-close" aria-label="Close">×</button>
64
+ <div id="drawer-body"></div>
65
+ </aside>
66
+ <div id="overlay" class="overlay hidden"></div>
67
+
68
+ <aside id="graph-drawer" class="drawer graph-drawer hidden">
69
+ <button id="graph-close" class="drawer-close" aria-label="Close">×</button>
70
+ <div id="graph-body"></div>
71
+ </aside>
72
+ <div id="graph-overlay" class="overlay graph-overlay hidden"></div>
73
+
74
+ <script src="/app.js?v=cpu1"></script>
75
+ </body>
76
+ </html>
src/frontend/styles.css ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --dark: #141c25; --blue: #3b8bbf; --blue-d: #357aa8; --line: #e2e6ea;
3
+ --text: #2c3744; --muted: #8a97a4; --ok: #2e7d52; --warn: #8a5a00;
4
+ }
5
+ * { box-sizing: border-box; }
6
+ body { margin: 0; color: var(--text); background: #f7f9fb;
7
+ font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
8
+
9
+ .topbar { display: flex; align-items: center; background: var(--dark); color: #fff; height: 56px; padding: 0 18px; }
10
+ .brand { font-weight: 800; letter-spacing: 1px; font-size: 19px; }
11
+ .topbar-title { flex: 1; text-align: center; font-weight: 700; font-size: 18px; }
12
+ .about-btn { background: var(--blue); color: #fff; border: 0; border-radius: 6px; padding: 7px 16px; font-size: 14px; cursor: pointer; }
13
+ .about-btn:hover { background: var(--blue-d); }
14
+ .basket-btn { display: inline-flex; align-items: center; gap: 7px; margin-right: 10px; background: transparent; color: #dbe5ec; border: 1px solid #43515f; border-radius: 6px; padding: 6px 10px; font: inherit; font-size: 13px; cursor: pointer; }
15
+ .basket-btn:hover { color: #fff; background: #1d2a36; border-color: #667786; }
16
+ .basket-btn:focus-visible, .about-btn:focus-visible { outline: 2px solid #8bc7ed; outline-offset: 2px; }
17
+ .basket-count { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; color: #fff; background: var(--blue); border-radius: 10px; padding: 0 6px; font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
18
+
19
+ .category-select { width: 100%; padding: 10px 12px; border-radius: 7px; border: 1px solid #33424f; background: #1d2731; color: #fff; font-size: 15px; cursor: pointer; }
20
+ .category-select:focus { outline: none; border-color: var(--blue); }
21
+
22
+ .layout { display: flex; min-height: calc(100vh - 56px); }
23
+ .sidebar { width: 270px; flex-shrink: 0; background: var(--dark); color: #cdd6df; padding: 22px 20px; }
24
+ .side-label { display: block; font-size: 12px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; margin: 22px 0 8px; color: #fff; }
25
+ .side-label:first-child { margin-top: 0; }
26
+ .k-value { display: block; font-size: 15px; color: #fff; margin: 2px 0 4px; }
27
+ .sidebar input[type=range] { width: 100%; accent-color: var(--blue); }
28
+ .filters { margin: 4px 0; }
29
+ .filter-opt { display: flex; align-items: center; gap: 9px; padding: 4px 0; font-size: 14px; cursor: pointer; }
30
+ .filter-opt input { accent-color: var(--blue); width: 15px; height: 15px; }
31
+ .export { width: 100%; margin-top: 30px; background: var(--blue); color: #fff; border: 0; border-radius: 6px; padding: 11px; font-size: 14px; cursor: pointer; }
32
+ .export:hover { background: var(--blue-d); }
33
+
34
+ .content { flex: 1; min-width: 0; max-width: 1080px; margin: 0 auto; padding: 30px 24px 70px; width: 100%; }
35
+ .search-for { text-align: center; font-size: 12px; letter-spacing: 1px; color: var(--muted); text-transform: uppercase; margin: 6px 0 14px; }
36
+ .search-row { display: flex; gap: 8px; }
37
+ .query { flex: 1; padding: 13px 16px; border: 1px solid var(--line); border-radius: 9px; background: #fff; font-size: 16px; }
38
+ .query:focus { outline: none; border-color: var(--blue); }
39
+ .search-btn { background: var(--blue); color: #fff; border: 0; border-radius: 9px; padding: 0 26px; font-size: 15px; font-weight: 600; cursor: pointer; }
40
+ .search-btn:hover { background: var(--blue-d); }
41
+ .examples { text-align: center; color: var(--muted); font-size: 13px; margin-top: 10px; }
42
+ .examples a { color: var(--blue); cursor: pointer; }
43
+ .examples a:hover { text-decoration: underline; }
44
+
45
+ .status { color: #5a6773; font-size: 14px; margin: 22px 0 10px; min-height: 20px; }
46
+ .results { display: flex; flex-direction: column; gap: 14px; }
47
+
48
+ /* empty state */
49
+ .empty { text-align: center; color: var(--muted); padding: 48px 10px; }
50
+ .empty-title { font-size: 26px; font-weight: 400; color: #6b7885; margin: 0 0 10px; }
51
+ .empty-sub { font-size: 15px; margin: 0 0 18px; }
52
+ .empty-try { font-size: 14px; margin-bottom: 8px; }
53
+ .example-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
54
+ .example-chip { background: #fff; border: 1px solid var(--line); color: var(--blue); border-radius: 16px; padding: 6px 14px; font-size: 13px; cursor: pointer; }
55
+ .example-chip:hover { background: var(--blue); color: #fff; border-color: var(--blue); }
56
+ .empty-link { color: var(--blue); cursor: pointer; }
57
+ .empty-link:hover { text-decoration: underline; }
58
+ .empty-dot { color: var(--muted); }
59
+
60
+ /* code-result filter chips (narrow the table by code type) */
61
+ .filter-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 12px; }
62
+ .filter-bar:empty { display: none; }
63
+ .filter-bar-label { font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; color: var(--muted); margin-right: 2px; }
64
+ .fchip { background: #fff; border: 1px solid var(--line); color: #5a6773; border-radius: 14px; padding: 4px 12px; font-size: 12px; cursor: pointer; font-variant-numeric: tabular-nums; }
65
+ .fchip:hover { border-color: var(--blue); color: var(--blue); }
66
+ .fchip.on { background: var(--blue); border-color: var(--blue); color: #fff; }
67
+ .fchip-clear { background: none; border: 0; color: var(--blue); font-size: 12px; cursor: pointer; padding: 4px 6px; }
68
+ .fchip-clear:hover { text-decoration: underline; }
69
+ .filter-count { font-size: 12px; color: var(--muted); }
70
+ .filter-empty { color: var(--muted); font-size: 14px; padding: 18px 4px; text-align: center; }
71
+
72
+ /* code results table */
73
+ .table-wrap { border: 1px solid var(--line); border-radius: 10px; overflow-x: auto; background: #fff; }
74
+ .code-table { width: 100%; border-collapse: collapse; font-size: 14px; }
75
+ .code-table th { text-align: left; font-weight: 600; color: #6b7885; background: #f6f8fa; border-bottom: 1px solid var(--line); padding: 12px 14px; white-space: nowrap; }
76
+ .code-table td { border-bottom: 1px solid #eef1f4; padding: 11px 14px; vertical-align: top; }
77
+ .code-table tbody tr:last-child td { border-bottom: 0; }
78
+ .code-table tbody tr:hover { background: #f9fbfc; }
79
+ .ann-cell { text-align: center; width: 62px; }
80
+ .ann-cell input { accent-color: var(--blue); width: 15px; height: 15px; cursor: pointer; }
81
+ .col-rank { color: var(--muted); font-variant-numeric: tabular-nums; width: 54px; }
82
+ .col-type { color: #5a6773; white-space: nowrap; }
83
+ .col-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #2c5e7e; word-break: break-all; max-width: 280px; }
84
+ .col-desc { color: var(--text); min-width: 160px; }
85
+ .col-rel { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); width: 84px; white-space: nowrap; }
86
+
87
+ /* phenotype card head: rank badge + single relevance */
88
+ .rank-badge { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--blue); background: #e8f2f9; border: 1px solid #bcdcec; border-radius: 6px; padding: 1px 8px; font-size: 13px; }
89
+ .card-head-right { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
90
+ .rel-label { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); }
91
+ .rel-value { font-size: 17px; font-weight: 700; color: #1f6f9e; font-variant-numeric: tabular-nums; }
92
+
93
+ .card { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 16px 18px; }
94
+ .card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
95
+ .card-head-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
96
+ .card-head h3 { margin: 0; font-size: 17px; }
97
+ .tag { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); border: 1px solid var(--line); border-radius: 5px; padding: 2px 7px; }
98
+ .tag-ok { color: var(--ok); border-color: #bfe3cd; }
99
+
100
+ .ann-box { display: flex; gap: 12px; flex-shrink: 0; }
101
+ .ann-opt { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #5a6773; cursor: pointer; }
102
+ .ann-opt input { accent-color: var(--blue); width: 15px; height: 15px; }
103
+
104
+ .summary { color: #5a6773; margin: 10px 0 12px; }
105
+ .scores { display: flex; flex-direction: column; gap: 5px; max-width: 440px; margin-bottom: 10px; }
106
+ .score { display: flex; align-items: center; gap: 9px; font-size: 12px; }
107
+ .score-primary .score-label, .score-primary .score-val { color: var(--text); font-weight: 700; }
108
+ .score-primary .score-fill { background: #1f6f9e; }
109
+ .score-label { width: 64px; color: var(--muted); }
110
+ .score-track { flex: 1; height: 6px; background: #eef1f4; border-radius: 4px; overflow: hidden; }
111
+ .score-fill { height: 100%; background: var(--blue); }
112
+ .score-val { width: 44px; text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
113
+
114
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
115
+ .chip-static { font-size: 11px; color: #5a6773; background: #f1f5f8; border: 1px solid var(--line); border-radius: 5px; padding: 2px 8px; }
116
+ .warning { background: #fdf6e3; border: 1px solid #e8d9a8; color: var(--warn); border-radius: 7px; padding: 7px 10px; font-size: 13px; margin: 6px 0; }
117
+ .card-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
118
+ .card-foot-actions { display: flex; align-items: center; gap: 14px; }
119
+ .link { background: none; border: 0; color: var(--blue); cursor: pointer; font-size: 14px; padding: 0; }
120
+ .link:hover { text-decoration: underline; }
121
+ .pid { color: var(--muted); font-size: 12px; }
122
+
123
+ .submit-row { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 26px; }
124
+ .annotator { padding: 10px 14px; border: 1px solid var(--line); border-radius: 7px; font-size: 14px; min-width: 220px; }
125
+ .submit-btn { background: var(--blue); color: #fff; border: 0; border-radius: 7px; padding: 11px 22px; font-size: 14px; cursor: pointer; }
126
+ .submit-btn:hover { background: var(--blue-d); }
127
+ .submit-msg { color: var(--ok); font-size: 13px; }
128
+
129
+ .hidden { display: none; }
130
+
131
+ .drawer { position: fixed; top: 0; right: 0; height: 100%; width: min(560px, 92vw); background: #fff; border-left: 1px solid var(--line); padding: 24px 26px; overflow-y: auto; z-index: 20; box-shadow: -8px 0 30px rgba(0,0,0,.15); }
132
+ .drawer.hidden, .overlay.hidden { display: none; }
133
+ .drawer-close { position: absolute; top: 14px; right: 16px; background: none; border: 0; color: var(--muted); font-size: 26px; cursor: pointer; line-height: 1; }
134
+ .drawer h2 { margin: 0 36px 8px 0; }
135
+ .drawer h4 { margin: 18px 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: .4px; color: var(--blue); }
136
+ .drawer-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
137
+ .drawer ul { margin: 6px 0; padding-left: 18px; color: #5a6773; font-size: 13px; }
138
+ .overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 10; }
139
+ .codegroup { border: 1px solid var(--line); border-radius: 8px; margin: 6px 0; padding: 4px 10px; }
140
+ .codegroup summary { cursor: pointer; font-size: 14px; }
141
+ .codes { margin-top: 8px; max-height: 280px; overflow-y: auto; }
142
+ .code-row { display: flex; gap: 10px; padding: 3px 0; border-top: 1px solid #eef1f4; font-size: 12px; }
143
+ .code { color: #2c5e7e; font-family: ui-monospace, monospace; min-width: 90px; }
144
+ .code-label { color: var(--text); }
145
+ .code-label-gap { color: var(--muted); font-style: italic; }
146
+
147
+ /* Algorithm components (professor 1.4) */
148
+ .algo-components { border: 1px solid var(--line); border-radius: 8px; padding: 4px 12px; }
149
+ .algo-row { display: flex; gap: 12px; padding: 6px 0; border-top: 1px solid #eef1f4; font-size: 13px; }
150
+ .algo-row:first-child { border-top: 0; }
151
+ .algo-key { flex-shrink: 0; width: 120px; color: var(--muted); font-weight: 600; }
152
+ .algo-val { color: #5a6773; }
153
+
154
+ /* More information -> CIPHER (professor 1.5) */
155
+ .drawer-more { margin-top: 18px; }
156
+ .cipher-link { display: inline-block; color: var(--blue); font-weight: 600; text-decoration: none; background: none; border: 0; cursor: pointer; padding: 0; font-size: 14px; }
157
+ .cipher-link:hover { text-decoration: underline; }
158
+
159
+ /* knowledge-graph drawer (click a code -> parent/child ontology) */
160
+ .graph-drawer { z-index: 40; width: clamp(560px, 66vw, 1400px); }
161
+ .graph-overlay { z-index: 30; background: rgba(0,0,0,.25); }
162
+ .code-link { background: none; border: 0; padding: 0; font: inherit; color: #2c5e7e; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
163
+ .code-link:hover { color: var(--blue); text-decoration-style: solid; }
164
+ .code-cell-btn { word-break: break-all; text-align: left; }
165
+ .graph-sub { color: #5a6773; font-size: 13px; margin: 2px 0 14px; }
166
+ .graph-note { color: var(--muted); font-size: 12px; margin: 8px 2px 0; }
167
+ .graph-scroll { overflow: auto; max-width: 100%; max-height: calc(100vh - 200px); border: 1px solid var(--line); border-radius: 10px; background: #fbfdfe; margin: 4px 0; }
168
+ .graph-svg { display: block; }
169
+ .graph-edge { fill: none; stroke: #cfd8e0; stroke-width: 1.4; }
170
+ .graph-edge-path { stroke: var(--blue); stroke-width: 2.5; } /* the clicked root→…→code path */
171
+ .graph-rect { fill: #fff; stroke: var(--line); stroke-width: 1; }
172
+ .graph-rect-root { fill: #eef5fa; stroke: #bcdcec; }
173
+ .graph-rect-path { fill: #eaf3fb; stroke: #7fb4d6; stroke-width: 1.5; } /* ancestors on the path */
174
+ .graph-rect-current { fill: #cfe6f6; stroke: var(--blue); stroke-width: 2.5; } /* the clicked code */
175
+ .graph-rect-more { fill: #f6f8fa; stroke: #cbd5dd; stroke-dasharray: 4 3; } /* "+N more" stub */
176
+ .graph-node-label { font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #2c5e7e; }
177
+ .graph-node-cur { fill: #124b6b; font-weight: 800; }
178
+ .graph-node-more { fill: var(--muted); font-family: system-ui, sans-serif; font-weight: 500; }
179
+ .graph-node-sub { font: 11px system-ui, sans-serif; fill: #6b7885; }
180
+ .graph-node-clickable { cursor: pointer; }
181
+ .graph-node-clickable:hover .graph-rect { stroke: var(--blue); }
182
+ .graph-node-clickable:hover .graph-node-label { fill: var(--blue); }
183
+
184
+ /* phenotype "view code hierarchy" trigger next to the code-groups heading */
185
+ .cg-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
186
+ .graph-btn { background: #eef5fa; color: var(--blue); border: 1px solid #bcdcec; border-radius: 6px; padding: 5px 11px; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; }
187
+ .graph-btn:hover { background: var(--blue); color: #fff; border-color: var(--blue); }
188
+
189
+ /* collected-codes basket */
190
+ .collect-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-height: 28px; background: #f8fbfd; border: 1px solid #bcdcec; color: var(--blue); border-radius: 6px; padding: 4px 9px; font: inherit; font-size: 12px; font-weight: 600; line-height: 1; cursor: pointer; white-space: nowrap; transition: background-color .15s, border-color .15s, color .15s; }
191
+ .collect-btn:hover { background: #eef5fa; border-color: #7fb4d6; }
192
+ .collect-btn:focus-visible { outline: 2px solid #8bc7ed; outline-offset: 2px; }
193
+ .collect-btn.on { background: #e8f2f9; color: #1f6f9e; border-color: #7fb4d6; }
194
+ .collect-mark { display: inline-flex; align-items: center; justify-content: center; width: 12px; height: 12px; font-size: 14px; font-weight: 700; line-height: 1; }
195
+ .collect-btn.on .collect-mark { font-size: 12px; }
196
+ .collect-icon { width: 30px; height: 30px; min-height: 30px; padding: 0; }
197
+ .collect-icon .collect-mark { width: auto; height: auto; }
198
+ .basket-actions { display: flex; gap: 18px; align-items: center; margin: 10px 0 16px; }
199
+ .basket-list { display: flex; flex-direction: column; gap: 6px; }
200
+ .basket-row { display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; }
201
+ .basket-main { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; flex-wrap: wrap; }
202
+ .basket-desc { color: #5a6773; font-size: 13px; }
203
+ .basket-rm { background: none; border: 0; color: var(--muted); font-size: 18px; cursor: pointer; line-height: 1; }
204
+ .basket-rm:hover { color: var(--warn); }
src/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Runtime dependencies. The Dockerfile installs the CPU-only torch wheel first.
2
+ torch==2.13.0
3
+ sentence-transformers==5.6.0
4
+ fastapi==0.139.0
5
+ uvicorn[standard]==0.51.0
6
+ numpy==2.5.1
7
+ faiss-cpu==1.14.3