nodae_bridge 1.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nodae_bridge-1.0.5/PKG-INFO +221 -0
- nodae_bridge-1.0.5/README.md +198 -0
- nodae_bridge-1.0.5/pyproject.toml +38 -0
- nodae_bridge-1.0.5/setup.cfg +4 -0
- nodae_bridge-1.0.5/src/nodae_bridge/__init__.py +28 -0
- nodae_bridge-1.0.5/src/nodae_bridge/adapter.py +230 -0
- nodae_bridge-1.0.5/src/nodae_bridge/bridge.py +422 -0
- nodae_bridge-1.0.5/src/nodae_bridge/run.py +148 -0
- nodae_bridge-1.0.5/src/nodae_bridge.egg-info/PKG-INFO +221 -0
- nodae_bridge-1.0.5/src/nodae_bridge.egg-info/SOURCES.txt +11 -0
- nodae_bridge-1.0.5/src/nodae_bridge.egg-info/dependency_links.txt +1 -0
- nodae_bridge-1.0.5/src/nodae_bridge.egg-info/requires.txt +3 -0
- nodae_bridge-1.0.5/src/nodae_bridge.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nodae_bridge
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: Nodae BYOM SDK — NodaeBridge and NodaeModelAdapter for sponsor containers
|
|
5
|
+
Author-email: Intheris Health <intheris.health@gmail.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://www.intheris-health.com
|
|
8
|
+
Keywords: federated-learning,healthcare,BYOM,nodae
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: pandas>=1.5
|
|
21
|
+
Requires-Dist: pyarrow>=12.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
|
|
24
|
+
# nodae_bridge
|
|
25
|
+
|
|
26
|
+
**Nodae BYOM SDK v1.0.4** — the bridge between your ML model and the [Nodae federated healthcare platform](https://www.intheris-health.com) by Intheris Health.
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
`nodae_bridge` is the Python package your Docker container uses when participating in federated training or inference across hospital sites. It provides:
|
|
31
|
+
|
|
32
|
+
- **`NodaeBridge`** — file-based I/O helpers (`/data` → `/output`)
|
|
33
|
+
- **`NodaeModelAdapter`** — the interface your model class must implement
|
|
34
|
+
- **`run.py`** — the container entrypoint dispatcher (`python -m nodae_bridge.run`)
|
|
35
|
+
|
|
36
|
+
Core principle: **algorithms travel, data stays local.** Patient data never leaves the hospital; your container receives a privacy-safe DataFrame and returns only model weights or aggregate statistics.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install nodae_bridge
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Minimum Python version: **3.10**. Add it alongside your ML framework in your Dockerfile:
|
|
45
|
+
|
|
46
|
+
```dockerfile
|
|
47
|
+
RUN pip install nodae_bridge scikit-learn # or torch, xgboost, etc.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from nodae_bridge import NodaeModelAdapter, ModelSpec, TrainingConfig, LocalTrainResult, InferenceResult
|
|
54
|
+
import pandas as pd
|
|
55
|
+
|
|
56
|
+
class MyModel(NodaeModelAdapter):
|
|
57
|
+
|
|
58
|
+
def get_model_spec(self) -> ModelSpec:
|
|
59
|
+
return ModelSpec(
|
|
60
|
+
model_id="my-model-v1",
|
|
61
|
+
name="My Federated Model",
|
|
62
|
+
version="1.0.0",
|
|
63
|
+
input_columns=["age", "length_of_stay", "drg_weight"],
|
|
64
|
+
output_names=["risk_score"],
|
|
65
|
+
min_samples=50,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def initialize(self, config: dict) -> None:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def get_weights(self) -> bytes:
|
|
72
|
+
return b""
|
|
73
|
+
|
|
74
|
+
def set_weights(self, weights: bytes) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def local_train(self, data: pd.DataFrame, round_number: int, config: TrainingConfig) -> LocalTrainResult:
|
|
78
|
+
return LocalTrainResult(weights=self.get_weights(), sample_count=len(data), loss=0.0)
|
|
79
|
+
|
|
80
|
+
def local_inference(self, data: pd.DataFrame) -> InferenceResult:
|
|
81
|
+
return InferenceResult(prediction_distribution={"low": len(data)}, sample_count=len(data))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Your `Dockerfile`:
|
|
85
|
+
|
|
86
|
+
```dockerfile
|
|
87
|
+
FROM python:3.11-slim
|
|
88
|
+
WORKDIR /app
|
|
89
|
+
RUN pip install nodae_bridge torch --index-url https://download.pytorch.org/whl/cpu
|
|
90
|
+
COPY model.py .
|
|
91
|
+
CMD ["python", "-m", "nodae_bridge.run"]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Data Schema
|
|
95
|
+
|
|
96
|
+
Your container receives local patient data at `/data/input.parquet` as a privacy-safe DataFrame. No PII is ever exposed.
|
|
97
|
+
|
|
98
|
+
| Column | Type | Description |
|
|
99
|
+
|--------|------|-------------|
|
|
100
|
+
| `patient_id` | str | Anonymized SHA-256 hash |
|
|
101
|
+
| `age` | int | Computed from DOB |
|
|
102
|
+
| `gender` | str | MALE / FEMALE / OTHER / UNKNOWN |
|
|
103
|
+
| `admission_type` | str | EMERGENCY / ELECTIVE / TRANSFER / AMBULATORY |
|
|
104
|
+
| `discharge_disposition` | str | HOME / TRANSFER / DECEASED / REHABILITATION |
|
|
105
|
+
| `length_of_stay` | int | Hospitalization days |
|
|
106
|
+
| `primary_diagnosis` | str | ICD-10 code |
|
|
107
|
+
| `num_secondary_diagnoses` | int | Count |
|
|
108
|
+
| `drg_code` | str | SwissDRG code |
|
|
109
|
+
| `drg_weight` | float | DRG cost weight |
|
|
110
|
+
| `num_procedures` | int | Count |
|
|
111
|
+
| `num_medications` | int | Count |
|
|
112
|
+
| `num_lab_results` | int | Count |
|
|
113
|
+
| `insurance_type` | str | Swiss insurance category |
|
|
114
|
+
|
|
115
|
+
Always call `fillna()` before passing data to your model — not all columns are populated for every patient.
|
|
116
|
+
|
|
117
|
+
## API Reference
|
|
118
|
+
|
|
119
|
+
### Core I/O
|
|
120
|
+
|
|
121
|
+
| Method | Description |
|
|
122
|
+
|--------|-------------|
|
|
123
|
+
| `NodaeBridge.get_data()` | Load `/data/input.parquet` as a DataFrame |
|
|
124
|
+
| `NodaeBridge.get_weights()` | Load global model weights; `b""` on round 0 |
|
|
125
|
+
| `NodaeBridge.get_config()` | Load training/inference config dict |
|
|
126
|
+
| `NodaeBridge.get_site_metadata()` | Convenience wrapper for `config["site_metadata"]` |
|
|
127
|
+
| `NodaeBridge.save_weights(bytes)` | Write updated weights to `/output/weights.bin` (**required**) |
|
|
128
|
+
| `NodaeBridge.save_metrics(dict)` | Write aggregate metrics to `/output/metrics.json` |
|
|
129
|
+
| `NodaeBridge.log(str)` | Print timestamped message to stdout |
|
|
130
|
+
|
|
131
|
+
### Extended scope (`byom_data_scope="extended"`)
|
|
132
|
+
|
|
133
|
+
Register with `byom_data_scope="extended"` to receive additional raw data files. Returns empty DataFrame/dict when absent (standard scope), so imports are always safe.
|
|
134
|
+
|
|
135
|
+
| Method | Returns | Content |
|
|
136
|
+
|--------|---------|---------|
|
|
137
|
+
| `NodaeBridge.get_labs()` | DataFrame | Per-result lab data: `patient_id, loinc_code, test_name, value, unit, flag, date, …` |
|
|
138
|
+
| `NodaeBridge.get_vitals()` | DataFrame | Latest vitals per patient: `bp_systolic, heart_rate, temperature, weight_kg, …` |
|
|
139
|
+
| `NodaeBridge.get_procedures()` | DataFrame | Per-procedure: `patient_id, code, description, date, source` |
|
|
140
|
+
| `NodaeBridge.get_signals()` | dict | Study-specific template signals: `{patient_id: {signal_key: value}}` |
|
|
141
|
+
|
|
142
|
+
Feature engineering is the container's responsibility — the platform exposes raw data only.
|
|
143
|
+
|
|
144
|
+
### FHIR scope (`byom_data_scope="fhir"`)
|
|
145
|
+
|
|
146
|
+
Available when registered with `byom_data_scope="fhir"`. The host writes one FHIR R4 Bundle per cohort patient under `/data/fhir/`. The standard `input.parquet` is always present alongside the bundles.
|
|
147
|
+
|
|
148
|
+
| Method | Returns | Description |
|
|
149
|
+
|--------|---------|-------------|
|
|
150
|
+
| `NodaeBridge.list_fhir_patients()` | `list[str]` | Patient IDs (anonymized hashes) with FHIR bundles; reads `index.json` |
|
|
151
|
+
| `NodaeBridge.get_fhir_bundle(patient_id)` | `Optional[dict]` | Full FHIR R4 Bundle dict for one patient, or `None` |
|
|
152
|
+
|
|
153
|
+
Each bundle contains: `Patient` (age, gender, LOS, DRG — no PHI), `Condition`s (ICD-10 + SNOMED), `Observation`s (LOINC-coded labs and vitals), `Procedure`s (CHOP), `MedicationStatement`s, `AllergyIntolerance`s, a `Basic` resource for template_signals, and an `ImagingStudy` reference when DICOM data is present.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
for pid in NodaeBridge.list_fhir_patients():
|
|
157
|
+
bundle = NodaeBridge.get_fhir_bundle(pid)
|
|
158
|
+
entries = {e["resource"]["resourceType"]: e["resource"] for e in bundle["entry"]}
|
|
159
|
+
# access any field directly — no platform-imposed field selection
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### SCAFFOLD (`aggregation_strategy="scaffold"`)
|
|
163
|
+
|
|
164
|
+
Used when the study uses SCAFFOLD to correct client drift in non-IID populations.
|
|
165
|
+
|
|
166
|
+
| Method | Description |
|
|
167
|
+
|--------|-------------|
|
|
168
|
+
| `NodaeBridge.get_control_variates()` | Load global control variate from `/data/control_variates.bin`; `b""` when not active |
|
|
169
|
+
| `NodaeBridge.save_control_variate_delta(bytes)` | Write per-site delta to `/output/control_variates.bin` |
|
|
170
|
+
|
|
171
|
+
`config["scaffold_enabled"]` is `true` when the host has sent a global control variate. If `save_control_variate_delta()` is not called, the aggregator falls back to FedAvg for this site (backward compatible).
|
|
172
|
+
|
|
173
|
+
### Imaging (`imaging_access=True`)
|
|
174
|
+
|
|
175
|
+
Available when registered with `imaging_access=True` and the SA host has `BYOM_DICOM_STORE_ENABLED=True`. A read-only DICOM store is mounted at `/data/imaging/`.
|
|
176
|
+
|
|
177
|
+
| Method | Returns | Description |
|
|
178
|
+
|--------|---------|-------------|
|
|
179
|
+
| `NodaeBridge.get_imaging()` | `Optional[Path]` | Path to `/data/imaging/`, or `None` if unavailable |
|
|
180
|
+
| `NodaeBridge.list_imaging_series()` | `list[dict]` | Manifest: `patient_id, study_uid, modality, slice_count, path` |
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import pydicom
|
|
184
|
+
from pathlib import Path
|
|
185
|
+
|
|
186
|
+
base = NodaeBridge.get_imaging()
|
|
187
|
+
for series in NodaeBridge.list_imaging_series():
|
|
188
|
+
if series["modality"] != "CT":
|
|
189
|
+
continue
|
|
190
|
+
slices = sorted((base / series["path"]).glob("*.dcm"))
|
|
191
|
+
datasets = [pydicom.dcmread(str(s)) for s in slices]
|
|
192
|
+
# HU windowing, segmentation, feature extraction — container's responsibility
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Changelog
|
|
196
|
+
|
|
197
|
+
### 1.0.5 (June 2026)
|
|
198
|
+
- Added `list_fhir_patients()`, `get_fhir_bundle()` — FHIR R4 bundle access for `byom_data_scope="fhir"`
|
|
199
|
+
- Added `FHIR_PATH` class constant
|
|
200
|
+
|
|
201
|
+
### 1.0.4 (June 2026)
|
|
202
|
+
- Added `get_imaging()`, `list_imaging_series()` — DICOM store access
|
|
203
|
+
- Updated `get_site_metadata()` docs to include `imaging.*` keys
|
|
204
|
+
|
|
205
|
+
### 1.0.3 (June 2026)
|
|
206
|
+
- Added `get_control_variates()`, `save_control_variate_delta()` — SCAFFOLD support
|
|
207
|
+
|
|
208
|
+
### 1.0.2 (June 2026)
|
|
209
|
+
- Added `get_signals()` — template signal access
|
|
210
|
+
- Added `get_labs()`, `get_vitals()`, `get_procedures()` — extended data scope
|
|
211
|
+
|
|
212
|
+
### 1.0.1 (June 2026)
|
|
213
|
+
- Added `get_site_metadata()` convenience wrapper
|
|
214
|
+
|
|
215
|
+
### 1.0.0 (June 2026)
|
|
216
|
+
- Initial release: `NodaeBridge`, `NodaeModelAdapter`, `run.py` entrypoint
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
Proprietary — Intheris Health. Contact [support@intheris-health.com](mailto:support@intheris-health.com) for access.
|
|
221
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# nodae_bridge
|
|
2
|
+
|
|
3
|
+
**Nodae BYOM SDK v1.0.4** — the bridge between your ML model and the [Nodae federated healthcare platform](https://www.intheris-health.com) by Intheris Health.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`nodae_bridge` is the Python package your Docker container uses when participating in federated training or inference across hospital sites. It provides:
|
|
8
|
+
|
|
9
|
+
- **`NodaeBridge`** — file-based I/O helpers (`/data` → `/output`)
|
|
10
|
+
- **`NodaeModelAdapter`** — the interface your model class must implement
|
|
11
|
+
- **`run.py`** — the container entrypoint dispatcher (`python -m nodae_bridge.run`)
|
|
12
|
+
|
|
13
|
+
Core principle: **algorithms travel, data stays local.** Patient data never leaves the hospital; your container receives a privacy-safe DataFrame and returns only model weights or aggregate statistics.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install nodae_bridge
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Minimum Python version: **3.10**. Add it alongside your ML framework in your Dockerfile:
|
|
22
|
+
|
|
23
|
+
```dockerfile
|
|
24
|
+
RUN pip install nodae_bridge scikit-learn # or torch, xgboost, etc.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from nodae_bridge import NodaeModelAdapter, ModelSpec, TrainingConfig, LocalTrainResult, InferenceResult
|
|
31
|
+
import pandas as pd
|
|
32
|
+
|
|
33
|
+
class MyModel(NodaeModelAdapter):
|
|
34
|
+
|
|
35
|
+
def get_model_spec(self) -> ModelSpec:
|
|
36
|
+
return ModelSpec(
|
|
37
|
+
model_id="my-model-v1",
|
|
38
|
+
name="My Federated Model",
|
|
39
|
+
version="1.0.0",
|
|
40
|
+
input_columns=["age", "length_of_stay", "drg_weight"],
|
|
41
|
+
output_names=["risk_score"],
|
|
42
|
+
min_samples=50,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def initialize(self, config: dict) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def get_weights(self) -> bytes:
|
|
49
|
+
return b""
|
|
50
|
+
|
|
51
|
+
def set_weights(self, weights: bytes) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def local_train(self, data: pd.DataFrame, round_number: int, config: TrainingConfig) -> LocalTrainResult:
|
|
55
|
+
return LocalTrainResult(weights=self.get_weights(), sample_count=len(data), loss=0.0)
|
|
56
|
+
|
|
57
|
+
def local_inference(self, data: pd.DataFrame) -> InferenceResult:
|
|
58
|
+
return InferenceResult(prediction_distribution={"low": len(data)}, sample_count=len(data))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Your `Dockerfile`:
|
|
62
|
+
|
|
63
|
+
```dockerfile
|
|
64
|
+
FROM python:3.11-slim
|
|
65
|
+
WORKDIR /app
|
|
66
|
+
RUN pip install nodae_bridge torch --index-url https://download.pytorch.org/whl/cpu
|
|
67
|
+
COPY model.py .
|
|
68
|
+
CMD ["python", "-m", "nodae_bridge.run"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Data Schema
|
|
72
|
+
|
|
73
|
+
Your container receives local patient data at `/data/input.parquet` as a privacy-safe DataFrame. No PII is ever exposed.
|
|
74
|
+
|
|
75
|
+
| Column | Type | Description |
|
|
76
|
+
|--------|------|-------------|
|
|
77
|
+
| `patient_id` | str | Anonymized SHA-256 hash |
|
|
78
|
+
| `age` | int | Computed from DOB |
|
|
79
|
+
| `gender` | str | MALE / FEMALE / OTHER / UNKNOWN |
|
|
80
|
+
| `admission_type` | str | EMERGENCY / ELECTIVE / TRANSFER / AMBULATORY |
|
|
81
|
+
| `discharge_disposition` | str | HOME / TRANSFER / DECEASED / REHABILITATION |
|
|
82
|
+
| `length_of_stay` | int | Hospitalization days |
|
|
83
|
+
| `primary_diagnosis` | str | ICD-10 code |
|
|
84
|
+
| `num_secondary_diagnoses` | int | Count |
|
|
85
|
+
| `drg_code` | str | SwissDRG code |
|
|
86
|
+
| `drg_weight` | float | DRG cost weight |
|
|
87
|
+
| `num_procedures` | int | Count |
|
|
88
|
+
| `num_medications` | int | Count |
|
|
89
|
+
| `num_lab_results` | int | Count |
|
|
90
|
+
| `insurance_type` | str | Swiss insurance category |
|
|
91
|
+
|
|
92
|
+
Always call `fillna()` before passing data to your model — not all columns are populated for every patient.
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### Core I/O
|
|
97
|
+
|
|
98
|
+
| Method | Description |
|
|
99
|
+
|--------|-------------|
|
|
100
|
+
| `NodaeBridge.get_data()` | Load `/data/input.parquet` as a DataFrame |
|
|
101
|
+
| `NodaeBridge.get_weights()` | Load global model weights; `b""` on round 0 |
|
|
102
|
+
| `NodaeBridge.get_config()` | Load training/inference config dict |
|
|
103
|
+
| `NodaeBridge.get_site_metadata()` | Convenience wrapper for `config["site_metadata"]` |
|
|
104
|
+
| `NodaeBridge.save_weights(bytes)` | Write updated weights to `/output/weights.bin` (**required**) |
|
|
105
|
+
| `NodaeBridge.save_metrics(dict)` | Write aggregate metrics to `/output/metrics.json` |
|
|
106
|
+
| `NodaeBridge.log(str)` | Print timestamped message to stdout |
|
|
107
|
+
|
|
108
|
+
### Extended scope (`byom_data_scope="extended"`)
|
|
109
|
+
|
|
110
|
+
Register with `byom_data_scope="extended"` to receive additional raw data files. Returns empty DataFrame/dict when absent (standard scope), so imports are always safe.
|
|
111
|
+
|
|
112
|
+
| Method | Returns | Content |
|
|
113
|
+
|--------|---------|---------|
|
|
114
|
+
| `NodaeBridge.get_labs()` | DataFrame | Per-result lab data: `patient_id, loinc_code, test_name, value, unit, flag, date, …` |
|
|
115
|
+
| `NodaeBridge.get_vitals()` | DataFrame | Latest vitals per patient: `bp_systolic, heart_rate, temperature, weight_kg, …` |
|
|
116
|
+
| `NodaeBridge.get_procedures()` | DataFrame | Per-procedure: `patient_id, code, description, date, source` |
|
|
117
|
+
| `NodaeBridge.get_signals()` | dict | Study-specific template signals: `{patient_id: {signal_key: value}}` |
|
|
118
|
+
|
|
119
|
+
Feature engineering is the container's responsibility — the platform exposes raw data only.
|
|
120
|
+
|
|
121
|
+
### FHIR scope (`byom_data_scope="fhir"`)
|
|
122
|
+
|
|
123
|
+
Available when registered with `byom_data_scope="fhir"`. The host writes one FHIR R4 Bundle per cohort patient under `/data/fhir/`. The standard `input.parquet` is always present alongside the bundles.
|
|
124
|
+
|
|
125
|
+
| Method | Returns | Description |
|
|
126
|
+
|--------|---------|-------------|
|
|
127
|
+
| `NodaeBridge.list_fhir_patients()` | `list[str]` | Patient IDs (anonymized hashes) with FHIR bundles; reads `index.json` |
|
|
128
|
+
| `NodaeBridge.get_fhir_bundle(patient_id)` | `Optional[dict]` | Full FHIR R4 Bundle dict for one patient, or `None` |
|
|
129
|
+
|
|
130
|
+
Each bundle contains: `Patient` (age, gender, LOS, DRG — no PHI), `Condition`s (ICD-10 + SNOMED), `Observation`s (LOINC-coded labs and vitals), `Procedure`s (CHOP), `MedicationStatement`s, `AllergyIntolerance`s, a `Basic` resource for template_signals, and an `ImagingStudy` reference when DICOM data is present.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
for pid in NodaeBridge.list_fhir_patients():
|
|
134
|
+
bundle = NodaeBridge.get_fhir_bundle(pid)
|
|
135
|
+
entries = {e["resource"]["resourceType"]: e["resource"] for e in bundle["entry"]}
|
|
136
|
+
# access any field directly — no platform-imposed field selection
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### SCAFFOLD (`aggregation_strategy="scaffold"`)
|
|
140
|
+
|
|
141
|
+
Used when the study uses SCAFFOLD to correct client drift in non-IID populations.
|
|
142
|
+
|
|
143
|
+
| Method | Description |
|
|
144
|
+
|--------|-------------|
|
|
145
|
+
| `NodaeBridge.get_control_variates()` | Load global control variate from `/data/control_variates.bin`; `b""` when not active |
|
|
146
|
+
| `NodaeBridge.save_control_variate_delta(bytes)` | Write per-site delta to `/output/control_variates.bin` |
|
|
147
|
+
|
|
148
|
+
`config["scaffold_enabled"]` is `true` when the host has sent a global control variate. If `save_control_variate_delta()` is not called, the aggregator falls back to FedAvg for this site (backward compatible).
|
|
149
|
+
|
|
150
|
+
### Imaging (`imaging_access=True`)
|
|
151
|
+
|
|
152
|
+
Available when registered with `imaging_access=True` and the SA host has `BYOM_DICOM_STORE_ENABLED=True`. A read-only DICOM store is mounted at `/data/imaging/`.
|
|
153
|
+
|
|
154
|
+
| Method | Returns | Description |
|
|
155
|
+
|--------|---------|-------------|
|
|
156
|
+
| `NodaeBridge.get_imaging()` | `Optional[Path]` | Path to `/data/imaging/`, or `None` if unavailable |
|
|
157
|
+
| `NodaeBridge.list_imaging_series()` | `list[dict]` | Manifest: `patient_id, study_uid, modality, slice_count, path` |
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import pydicom
|
|
161
|
+
from pathlib import Path
|
|
162
|
+
|
|
163
|
+
base = NodaeBridge.get_imaging()
|
|
164
|
+
for series in NodaeBridge.list_imaging_series():
|
|
165
|
+
if series["modality"] != "CT":
|
|
166
|
+
continue
|
|
167
|
+
slices = sorted((base / series["path"]).glob("*.dcm"))
|
|
168
|
+
datasets = [pydicom.dcmread(str(s)) for s in slices]
|
|
169
|
+
# HU windowing, segmentation, feature extraction — container's responsibility
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Changelog
|
|
173
|
+
|
|
174
|
+
### 1.0.5 (June 2026)
|
|
175
|
+
- Added `list_fhir_patients()`, `get_fhir_bundle()` — FHIR R4 bundle access for `byom_data_scope="fhir"`
|
|
176
|
+
- Added `FHIR_PATH` class constant
|
|
177
|
+
|
|
178
|
+
### 1.0.4 (June 2026)
|
|
179
|
+
- Added `get_imaging()`, `list_imaging_series()` — DICOM store access
|
|
180
|
+
- Updated `get_site_metadata()` docs to include `imaging.*` keys
|
|
181
|
+
|
|
182
|
+
### 1.0.3 (June 2026)
|
|
183
|
+
- Added `get_control_variates()`, `save_control_variate_delta()` — SCAFFOLD support
|
|
184
|
+
|
|
185
|
+
### 1.0.2 (June 2026)
|
|
186
|
+
- Added `get_signals()` — template signal access
|
|
187
|
+
- Added `get_labs()`, `get_vitals()`, `get_procedures()` — extended data scope
|
|
188
|
+
|
|
189
|
+
### 1.0.1 (June 2026)
|
|
190
|
+
- Added `get_site_metadata()` convenience wrapper
|
|
191
|
+
|
|
192
|
+
### 1.0.0 (June 2026)
|
|
193
|
+
- Initial release: `NodaeBridge`, `NodaeModelAdapter`, `run.py` entrypoint
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
Proprietary — Intheris Health. Contact [support@intheris-health.com](mailto:support@intheris-health.com) for access.
|
|
198
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nodae_bridge"
|
|
3
|
+
version = "1.0.5"
|
|
4
|
+
description = "Nodae BYOM SDK — NodaeBridge and NodaeModelAdapter for sponsor containers"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "LicenseRef-Proprietary"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Intheris Health", email = "intheris.health@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["federated-learning", "healthcare", "BYOM", "nodae"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pandas>=1.5",
|
|
25
|
+
"pyarrow>=12.0",
|
|
26
|
+
"pydantic>=2.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://www.intheris-health.com"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["setuptools>=68", "wheel"]
|
|
34
|
+
build-backend = "setuptools.build_meta"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
include = ["nodae_bridge*"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nodae_bridge — Nodae BYOM SDK for sponsor containers.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from nodae_bridge import NodaeBridge, NodaeModelAdapter
|
|
7
|
+
from nodae_bridge import ModelSpec, TrainingConfig, LocalTrainResult, InferenceResult
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .adapter import (
|
|
11
|
+
ContainerResourceRequirements,
|
|
12
|
+
InferenceResult,
|
|
13
|
+
LocalTrainResult,
|
|
14
|
+
ModelSpec,
|
|
15
|
+
NodaeModelAdapter,
|
|
16
|
+
TrainingConfig,
|
|
17
|
+
)
|
|
18
|
+
from .bridge import NodaeBridge
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"NodaeBridge",
|
|
22
|
+
"NodaeModelAdapter",
|
|
23
|
+
"ModelSpec",
|
|
24
|
+
"TrainingConfig",
|
|
25
|
+
"LocalTrainResult",
|
|
26
|
+
"InferenceResult",
|
|
27
|
+
"ContainerResourceRequirements",
|
|
28
|
+
]
|