iints-sdk-python35 1.5.18__py3-none-any.whl → 1.5.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iints/__init__.py +1 -1
- iints/cli/cli.py +321 -26
- iints/core/patient/hovorka_model.py +11 -5
- iints/core/patient/models.py +5 -2
- iints/core/simulator.py +4 -1
- iints/data/__init__.py +16 -0
- iints/data/certify.py +139 -1
- iints/data/contracts/diabetes_cgm_mdmp_contract.yaml +60 -0
- iints/data/runner.py +34 -0
- iints/highlevel.py +3 -2
- iints/mdmp/backend.py +29 -0
- iints/research/alphafold_engine.py +91 -0
- iints/research/anatomy.py +187 -0
- iints/research/genetics.py +178 -0
- iints/research/genomics_engine.py +185 -0
- iints/research/jetson_hf_trainer.py +14 -12
- iints/research/pharmacology.py +163 -0
- iints/research/physiology.py +114 -0
- iints/research/structure.py +387 -0
- iints/research/tissue_stressor.py +131 -0
- iints_desktop/__init__.py +14 -0
- iints_desktop/app.py +341 -0
- iints_desktop/assets/alphafold/AF-P01275-F1-model_v4.cif +2474 -0
- iints_desktop/assets/alphafold/AF-P01308-F1-model_v4.cif +1618 -0
- iints_desktop/assets/alphafold/AF-P06213-F1-model_v6.cif +15926 -0
- iints_desktop/assets/alphafold/AF-P14672-F1-model_v6.cif +5912 -0
- iints_desktop/assets/alphafold/AF-P47871-F1-model_v6.cif +5762 -0
- iints_desktop/assets/alphafold/gcgr_3D.png +0 -0
- iints_desktop/assets/alphafold/glucagon_3D.png +0 -0
- iints_desktop/assets/alphafold/glut4_3D.png +0 -0
- iints_desktop/assets/alphafold/insr_3D.png +0 -0
- iints_desktop/assets/alphafold/insulin_3D.png +0 -0
- iints_desktop/cocoa_app.py +317 -0
- iints_desktop/engine.py +398 -0
- iints_desktop/fetcher.py +64 -0
- iints_desktop/launcher.py +42 -0
- iints_desktop/local_ai.py +320 -0
- iints_desktop/mdmp.py +92 -0
- iints_desktop/molecule_viewer.py +224 -0
- iints_desktop/molecules.py +257 -0
- iints_desktop/qt_app.py +3091 -0
- iints_desktop/render_3dmol.py +65 -0
- iints_desktop/results.py +163 -0
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/METADATA +34 -66
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/RECORD +51 -19
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/WHEEL +1 -1
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/entry_points.txt +4 -0
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/top_level.txt +1 -0
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/licenses/LICENSE +0 -0
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/licenses/LICENSE-MIT-IINTS-LEGACY +0 -0
- {iints_sdk_python35-1.5.18.dist-info → iints_sdk_python35-1.5.20.dist-info}/licenses/NOTICE +0 -0
iints/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ except ImportError: # pragma: no cover - Python < 3.8 fallback
|
|
|
11
11
|
try:
|
|
12
12
|
__version__ = version("iints-sdk-python35")
|
|
13
13
|
except PackageNotFoundError: # pragma: no cover - source tree fallback
|
|
14
|
-
__version__ = "1.5.
|
|
14
|
+
__version__ = "1.5.20"
|
|
15
15
|
|
|
16
16
|
# Note to developers: this SDK is currently maintained by a single author.
|
|
17
17
|
# Please report bugs via GitHub issues and feel free to contribute fixes via PRs.
|
iints/cli/cli.py
CHANGED
|
@@ -109,6 +109,11 @@ from iints.data.realism_validator import (
|
|
|
109
109
|
validate_realism_dataset,
|
|
110
110
|
write_realism_report,
|
|
111
111
|
)
|
|
112
|
+
from iints.data.certify import (
|
|
113
|
+
certification_payload,
|
|
114
|
+
write_mdmp_certificate,
|
|
115
|
+
write_standard_diabetes_contract,
|
|
116
|
+
)
|
|
112
117
|
from iints.data.realism_governance import review_real_data_realism
|
|
113
118
|
from iints.data.realism_dashboard import write_realism_dashboard
|
|
114
119
|
from iints.demo_assets import export_live_stage_demo
|
|
@@ -4528,19 +4533,19 @@ def new_algo(
|
|
|
4528
4533
|
# Replace placeholders
|
|
4529
4534
|
# We expect the template class name to be {{ALGO_NAME}} and author {{AUTHOR_NAME}}
|
|
4530
4535
|
# But wait, the file I wrote uses {{ALGO_NAME}} as a class name, which is valid syntax only if I replace it.
|
|
4531
|
-
# The file on disk is valid python ONLY if those tokens are valid.
|
|
4536
|
+
# The file on disk is valid python ONLY if those tokens are valid.
|
|
4532
4537
|
# Actually, in the previous step I wrote {{ALGO_NAME}} literally into the python file.
|
|
4533
|
-
# That makes the template file itself invalid python syntax until replaced.
|
|
4534
|
-
# That's fine for a template file, but might confuse linters.
|
|
4538
|
+
# That makes the template file itself invalid python syntax until replaced.
|
|
4539
|
+
# That's fine for a template file, but might confuse linters.
|
|
4535
4540
|
# Ideally it's a .txt or .tmpl, but .py is fine if we accept it's a template.
|
|
4536
|
-
|
|
4541
|
+
|
|
4537
4542
|
final_content = template_content.replace("{{ALGO_NAME}}", f"{name}Algorithm")
|
|
4538
4543
|
final_content = final_content.replace("{{AUTHOR_NAME}}", author)
|
|
4539
4544
|
|
|
4540
4545
|
output_file = output_dir / f"{name.lower().replace(' ', '_')}_algorithm.py"
|
|
4541
4546
|
with open(output_file, "w") as f:
|
|
4542
4547
|
f.write(final_content)
|
|
4543
|
-
|
|
4548
|
+
|
|
4544
4549
|
typer.echo(f"Successfully created new algorithm template: {output_file}")
|
|
4545
4550
|
|
|
4546
4551
|
|
|
@@ -7909,9 +7914,26 @@ def data_contract_template(
|
|
|
7909
7914
|
@data_app.command(name="certify-template")
|
|
7910
7915
|
def data_certify_template(
|
|
7911
7916
|
output_path: Annotated[Path, typer.Option(help="Where to write the starter certification contract YAML")] = Path("data_contract.yaml"),
|
|
7917
|
+
profile: Annotated[
|
|
7918
|
+
str,
|
|
7919
|
+
typer.Option(help="Template profile: diabetes (default) or basic"),
|
|
7920
|
+
] = "diabetes",
|
|
7912
7921
|
):
|
|
7913
7922
|
"""Write a starter IINTS data-certification contract."""
|
|
7914
|
-
|
|
7923
|
+
console = Console()
|
|
7924
|
+
normalized = profile.strip().lower()
|
|
7925
|
+
if normalized in {"diabetes", "diabetes-cgm", "clinical-diabetes", "standard"}:
|
|
7926
|
+
write_standard_diabetes_contract(output_path)
|
|
7927
|
+
console.print(f"[green]Diabetes MDMP contract written:[/green] {output_path}")
|
|
7928
|
+
console.print("[cyan]Profile:[/cyan] diabetes-cgm research contract")
|
|
7929
|
+
return
|
|
7930
|
+
if normalized not in {"basic", "legacy"}:
|
|
7931
|
+
console.print("[bold red]Unknown profile. Use 'diabetes' or 'basic'.[/bold red]")
|
|
7932
|
+
raise typer.Exit(code=1)
|
|
7933
|
+
template = _build_mdmp_core_contract_template() if active_mdmp_backend() == "mdmp_core" else _build_data_contract_template()
|
|
7934
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
7935
|
+
output_path.write_text(yaml.safe_dump(template, sort_keys=False), encoding="utf-8")
|
|
7936
|
+
console.print(f"[green]Basic certification contract written:[/green] {output_path}")
|
|
7915
7937
|
|
|
7916
7938
|
|
|
7917
7939
|
@data_app.command(name="contract-run", hidden=True, deprecated=True)
|
|
@@ -7921,6 +7943,16 @@ def data_contract_run(
|
|
|
7921
7943
|
output_json: Annotated[Optional[Path], typer.Option(help="Optional output report JSON path")] = None,
|
|
7922
7944
|
apply_builtin_transforms: Annotated[bool, typer.Option(help="Apply built-in unit conversion transforms from the contract")] = True,
|
|
7923
7945
|
fail_on_noncompliant: Annotated[bool, typer.Option(help="Exit code 1 when compliance checks fail")] = False,
|
|
7946
|
+
quick: Annotated[bool, typer.Option("--quick", help="Quick scan: only load the first --quick-rows rows for large datasets")] = False,
|
|
7947
|
+
quick_rows: Annotated[int, typer.Option(help="Rows loaded when --quick is enabled")] = 5000,
|
|
7948
|
+
certificate_output: Annotated[Optional[Path], typer.Option(help="Optional MDMP certificate JSON output path")] = None,
|
|
7949
|
+
signing_key: Annotated[Optional[Path], typer.Option(help="Optional Ed25519 private key PEM for externally verifiable certificates")] = None,
|
|
7950
|
+
signing_key_id: Annotated[str, typer.Option(help="Key id written into a signed certificate")] = "iints_local_mdmp_v1",
|
|
7951
|
+
signed_by: Annotated[str, typer.Option(help="Signer label for generated MDMP certificates")] = "IINTS-AF Local MDMP",
|
|
7952
|
+
signing_key_passphrase_env: Annotated[
|
|
7953
|
+
Optional[str],
|
|
7954
|
+
typer.Option(help="Environment variable containing the private-key passphrase, if encrypted"),
|
|
7955
|
+
] = None,
|
|
7924
7956
|
min_mdmp_grade: Annotated[
|
|
7925
7957
|
Optional[str],
|
|
7926
7958
|
typer.Option(help="Optional MDMP grade gate (draft, research_grade, clinical_grade)"),
|
|
@@ -7937,7 +7969,7 @@ def data_contract_run(
|
|
|
7937
7969
|
|
|
7938
7970
|
try:
|
|
7939
7971
|
contract = load_mdmp_contract(contract_path)
|
|
7940
|
-
df = pd.read_csv(input_csv)
|
|
7972
|
+
df = pd.read_csv(input_csv, nrows=max(1, int(quick_rows)) if quick else None)
|
|
7941
7973
|
report = run_mdmp_validation(
|
|
7942
7974
|
contract,
|
|
7943
7975
|
df,
|
|
@@ -7956,6 +7988,9 @@ def data_contract_run(
|
|
|
7956
7988
|
summary.add_row("Backend", active_mdmp_backend())
|
|
7957
7989
|
summary.add_row("MDMP grade", report.mdmp_grade)
|
|
7958
7990
|
summary.add_row("MDMP protocol", report.mdmp_protocol_version)
|
|
7991
|
+
summary.add_row("Scan mode", "quick" if quick else "full")
|
|
7992
|
+
if quick:
|
|
7993
|
+
summary.add_row("Quick rows limit", str(max(1, int(quick_rows))))
|
|
7959
7994
|
summary.add_row(
|
|
7960
7995
|
"Certified",
|
|
7961
7996
|
"yes" if report.certified_for_medical_research else "no",
|
|
@@ -7977,12 +8012,34 @@ def data_contract_run(
|
|
|
7977
8012
|
check.detail,
|
|
7978
8013
|
)
|
|
7979
8014
|
console.print(checks_table)
|
|
8015
|
+
payload = certification_payload(
|
|
8016
|
+
report,
|
|
8017
|
+
quick=quick,
|
|
8018
|
+
quick_rows=max(1, int(quick_rows)) if quick else None,
|
|
8019
|
+
input_path=input_csv,
|
|
8020
|
+
)
|
|
7980
8021
|
|
|
7981
8022
|
if output_json is not None:
|
|
7982
8023
|
output_json.parent.mkdir(parents=True, exist_ok=True)
|
|
7983
|
-
output_json.write_text(json.dumps(
|
|
8024
|
+
output_json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
7984
8025
|
console.print(f"[green]Contract report written:[/green] {output_json}")
|
|
7985
8026
|
|
|
8027
|
+
if certificate_output is not None:
|
|
8028
|
+
certificate_path = write_mdmp_certificate(
|
|
8029
|
+
payload,
|
|
8030
|
+
certificate_output,
|
|
8031
|
+
issued_by=signed_by,
|
|
8032
|
+
signing_key=signing_key,
|
|
8033
|
+
key_id=signing_key_id,
|
|
8034
|
+
passphrase_env=signing_key_passphrase_env,
|
|
8035
|
+
)
|
|
8036
|
+
console.print(f"[green]MDMP certificate written:[/green] {certificate_path}")
|
|
8037
|
+
if signing_key is None:
|
|
8038
|
+
console.print(
|
|
8039
|
+
"[yellow]Certificate is unsigned SHA-256 evidence only. "
|
|
8040
|
+
"Pass --signing-key for externally verifiable Ed25519 signing.[/yellow]"
|
|
8041
|
+
)
|
|
8042
|
+
|
|
7986
8043
|
if min_mdmp_grade is not None:
|
|
7987
8044
|
normalized = min_mdmp_grade.strip().lower()
|
|
7988
8045
|
if normalized not in MDMP_GRADE_ORDER:
|
|
@@ -8006,6 +8063,16 @@ def data_certify(
|
|
|
8006
8063
|
output_json: Annotated[Optional[Path], typer.Option(help="Optional output report JSON path")] = None,
|
|
8007
8064
|
apply_builtin_transforms: Annotated[bool, typer.Option(help="Apply built-in unit conversion transforms from the contract")] = True,
|
|
8008
8065
|
fail_on_noncompliant: Annotated[bool, typer.Option(help="Exit code 1 when compliance checks fail")] = False,
|
|
8066
|
+
quick: Annotated[bool, typer.Option("--quick", help="Quick scan: only load the first --quick-rows rows for large datasets")] = False,
|
|
8067
|
+
quick_rows: Annotated[int, typer.Option(help="Rows loaded when --quick is enabled")] = 5000,
|
|
8068
|
+
certificate_output: Annotated[Optional[Path], typer.Option(help="Optional MDMP certificate JSON output path")] = None,
|
|
8069
|
+
signing_key: Annotated[Optional[Path], typer.Option(help="Optional Ed25519 private key PEM for externally verifiable certificates")] = None,
|
|
8070
|
+
signing_key_id: Annotated[str, typer.Option(help="Key id written into a signed certificate")] = "iints_local_mdmp_v1",
|
|
8071
|
+
signed_by: Annotated[str, typer.Option(help="Signer label for generated MDMP certificates")] = "IINTS-AF Local MDMP",
|
|
8072
|
+
signing_key_passphrase_env: Annotated[
|
|
8073
|
+
Optional[str],
|
|
8074
|
+
typer.Option(help="Environment variable containing the private-key passphrase, if encrypted"),
|
|
8075
|
+
] = None,
|
|
8009
8076
|
min_mdmp_grade: Annotated[
|
|
8010
8077
|
Optional[str],
|
|
8011
8078
|
typer.Option(help="Optional certification grade gate (draft, research_grade, clinical_grade, ai_ready)"),
|
|
@@ -8018,10 +8085,62 @@ def data_certify(
|
|
|
8018
8085
|
output_json=output_json,
|
|
8019
8086
|
apply_builtin_transforms=apply_builtin_transforms,
|
|
8020
8087
|
fail_on_noncompliant=fail_on_noncompliant,
|
|
8088
|
+
quick=quick,
|
|
8089
|
+
quick_rows=quick_rows,
|
|
8090
|
+
certificate_output=certificate_output,
|
|
8091
|
+
signing_key=signing_key,
|
|
8092
|
+
signing_key_id=signing_key_id,
|
|
8093
|
+
signed_by=signed_by,
|
|
8094
|
+
signing_key_passphrase_env=signing_key_passphrase_env,
|
|
8021
8095
|
min_mdmp_grade=min_mdmp_grade,
|
|
8022
8096
|
)
|
|
8023
8097
|
|
|
8024
8098
|
|
|
8099
|
+
@data_app.command(name="mdmp-keygen")
|
|
8100
|
+
def data_mdmp_keygen(
|
|
8101
|
+
output_dir: Annotated[Path, typer.Option(help="Directory for the MDMP Ed25519 keypair")] = Path("certs/mdmp"),
|
|
8102
|
+
private_name: Annotated[str, typer.Option(help="Private key filename")] = "iints_mdmp_private.pem",
|
|
8103
|
+
public_name: Annotated[str, typer.Option(help="Public key filename")] = "iints_mdmp_public.pem",
|
|
8104
|
+
passphrase: Annotated[Optional[str], typer.Option(help="Optional private-key passphrase (prefer env/file)")] = None,
|
|
8105
|
+
passphrase_env: Annotated[Optional[str], typer.Option(help="Environment variable containing private-key passphrase")] = None,
|
|
8106
|
+
passphrase_file: Annotated[Optional[Path], typer.Option(help="File containing private-key passphrase")] = None,
|
|
8107
|
+
) -> None:
|
|
8108
|
+
"""Generate an Ed25519 keypair for signed MDMP certificates."""
|
|
8109
|
+
|
|
8110
|
+
console = Console()
|
|
8111
|
+
secret = _resolve_secret_option(
|
|
8112
|
+
console=console,
|
|
8113
|
+
label="passphrase",
|
|
8114
|
+
direct_value=passphrase,
|
|
8115
|
+
env_name=passphrase_env,
|
|
8116
|
+
file_path=passphrase_file,
|
|
8117
|
+
)
|
|
8118
|
+
try:
|
|
8119
|
+
from mdmp_core.crypto import generate_keypair
|
|
8120
|
+
|
|
8121
|
+
result = generate_keypair(
|
|
8122
|
+
output_dir=output_dir,
|
|
8123
|
+
private_name=private_name,
|
|
8124
|
+
public_name=public_name,
|
|
8125
|
+
passphrase=secret,
|
|
8126
|
+
)
|
|
8127
|
+
except Exception as exc:
|
|
8128
|
+
console.print(f"[bold red]MDMP key generation failed:[/bold red] {exc}")
|
|
8129
|
+
raise typer.Exit(code=1)
|
|
8130
|
+
|
|
8131
|
+
table = Table(title="MDMP Signing Keypair")
|
|
8132
|
+
table.add_column("Field", style="cyan")
|
|
8133
|
+
table.add_column("Value")
|
|
8134
|
+
table.add_row("Private key", str(result["private_key"]))
|
|
8135
|
+
table.add_row("Public key", str(result["public_key"]))
|
|
8136
|
+
table.add_row("Encrypted", "yes" if result.get("private_key_encrypted") else "no")
|
|
8137
|
+
console.print(table)
|
|
8138
|
+
console.print(
|
|
8139
|
+
"[dim]Use the private key with `iints data certify --signing-key ... --certificate-output ...`. "
|
|
8140
|
+
"Share the public key with reviewers who need to verify certificates.[/dim]"
|
|
8141
|
+
)
|
|
8142
|
+
|
|
8143
|
+
|
|
8025
8144
|
@data_app.command(name="eu-ai-pact-review")
|
|
8026
8145
|
def data_eu_ai_pact_review(
|
|
8027
8146
|
report_json: Annotated[Path, typer.Argument(help="Path to an MDMP/data certification JSON report")],
|
|
@@ -8367,6 +8486,35 @@ def data_synthetic_mirror(
|
|
|
8367
8486
|
if fail_on_noncompliant and not artifact.validation.is_compliant:
|
|
8368
8487
|
raise typer.Exit(code=1)
|
|
8369
8488
|
|
|
8489
|
+
@data_app.command(name="pull-hf")
|
|
8490
|
+
def data_pull_hf(
|
|
8491
|
+
repo: Annotated[str, typer.Option("--repo", help="Hugging Face dataset repository (e.g. MaxPrestige/Synthetic-Diabetes-Dataset)")],
|
|
8492
|
+
output: Annotated[Path, typer.Option("--output", help="Output directory for the pulled dataset")] = Path("data/raw/"),
|
|
8493
|
+
split: Annotated[str, typer.Option("--split", help="Dataset split to download (default: train)")] = "train",
|
|
8494
|
+
):
|
|
8495
|
+
"""Pull a tabular or time-series dataset from Hugging Face and prepare it for MDMP."""
|
|
8496
|
+
console = Console()
|
|
8497
|
+
try:
|
|
8498
|
+
from datasets import load_dataset
|
|
8499
|
+
except ImportError:
|
|
8500
|
+
console.print("[bold red]Please install 'datasets' package first: pip install datasets[/bold red]")
|
|
8501
|
+
raise typer.Exit(code=1)
|
|
8502
|
+
|
|
8503
|
+
console.print(f"Downloading dataset {repo} ({split} split)...")
|
|
8504
|
+
try:
|
|
8505
|
+
dataset = load_dataset(repo, split=split)
|
|
8506
|
+
df = dataset.to_pandas()
|
|
8507
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
8508
|
+
repo_name = repo.split("/")[-1]
|
|
8509
|
+
out_file = output / f"{repo_name}_{split}.csv"
|
|
8510
|
+
df.to_csv(out_file, index=False)
|
|
8511
|
+
console.print(f"[green]Successfully downloaded dataset to {out_file}[/green]")
|
|
8512
|
+
console.print(f"Rows: {len(df)}, Columns: {len(df.columns)}")
|
|
8513
|
+
console.print("[yellow]Next step:[/yellow] Use 'iints data certify' or 'iints data mdmp-visualizer' to map columns to the SDK's schema.")
|
|
8514
|
+
except Exception as e:
|
|
8515
|
+
console.print(f"[bold red]Failed to pull dataset:[/bold red] {e}")
|
|
8516
|
+
raise typer.Exit(code=1)
|
|
8517
|
+
|
|
8370
8518
|
|
|
8371
8519
|
@mdmp_app.command(name="template")
|
|
8372
8520
|
def mdmp_template(
|
|
@@ -9365,6 +9513,63 @@ def manage_results(
|
|
|
9365
9513
|
_print_results_index_bundle(bundle, console)
|
|
9366
9514
|
|
|
9367
9515
|
|
|
9516
|
+
@research_app.command(name="xai-report")
|
|
9517
|
+
def research_xai_report(
|
|
9518
|
+
results_csv: Annotated[Path, typer.Argument(help="Path to the simulation results.csv")],
|
|
9519
|
+
hf_model: Annotated[Optional[str], typer.Option("--hf-model", help="Hugging Face medical LLM to use for generation (e.g. devanshamin/PubMedDiabetes-LLM-Predictions)")] = None,
|
|
9520
|
+
output_json: Annotated[Path, typer.Option("--output", help="Path to write the xai_events.json")] = Path("xai_events.json"),
|
|
9521
|
+
):
|
|
9522
|
+
"""Generate an XAI (Explainable AI) clinical report from a simulation run, optionally using a Hugging Face medical LLM."""
|
|
9523
|
+
console = Console()
|
|
9524
|
+
if not results_csv.is_file():
|
|
9525
|
+
console.print(f"[bold red]Results file not found:[/bold red] {results_csv}")
|
|
9526
|
+
raise typer.Exit(code=1)
|
|
9527
|
+
|
|
9528
|
+
try:
|
|
9529
|
+
df = pd.read_csv(results_csv)
|
|
9530
|
+
hypos = len(df[df.get("glucose_actual_mgdl", pd.Series([100])) < 70])
|
|
9531
|
+
hypers = len(df[df.get("glucose_actual_mgdl", pd.Series([100])) > 180])
|
|
9532
|
+
summary_text = f"Simulation complete. {hypos} hypoglycemic intervals and {hypers} hyperglycemic intervals detected."
|
|
9533
|
+
|
|
9534
|
+
if hf_model:
|
|
9535
|
+
console.print(f"Loading medical LLM from Hugging Face: {hf_model}...")
|
|
9536
|
+
try:
|
|
9537
|
+
from transformers import pipeline
|
|
9538
|
+
generator = pipeline("text-generation", model=hf_model, device="cpu")
|
|
9539
|
+
prompt = f"Patient simulation summary: {summary_text}. Provide a clinical interpretation:"
|
|
9540
|
+
output = generator(prompt, max_new_tokens=50, num_return_sequences=1)
|
|
9541
|
+
clinical_interpretation = output[0]["generated_text"]
|
|
9542
|
+
except ImportError:
|
|
9543
|
+
console.print("[bold red]Please install transformers: pip install transformers torch[/bold red]")
|
|
9544
|
+
raise typer.Exit(code=1)
|
|
9545
|
+
except Exception as e:
|
|
9546
|
+
console.print(f"[bold yellow]Failed to generate with {hf_model}, falling back to basic summary. Error: {e}[/bold yellow]")
|
|
9547
|
+
clinical_interpretation = "Clinical interpretation requires a successful LLM generation."
|
|
9548
|
+
else:
|
|
9549
|
+
clinical_interpretation = "Provide a --hf-model to generate clinical insights."
|
|
9550
|
+
|
|
9551
|
+
xai_events = {
|
|
9552
|
+
"source_file": str(results_csv),
|
|
9553
|
+
"summary": summary_text,
|
|
9554
|
+
"clinical_interpretation": clinical_interpretation,
|
|
9555
|
+
"metrics": {
|
|
9556
|
+
"hypoglycemia_minutes": hypos * 5,
|
|
9557
|
+
"hyperglycemia_minutes": hypers * 5
|
|
9558
|
+
}
|
|
9559
|
+
}
|
|
9560
|
+
|
|
9561
|
+
output_json.parent.mkdir(parents=True, exist_ok=True)
|
|
9562
|
+
output_json.write_text(json.dumps(xai_events, indent=2))
|
|
9563
|
+
console.print(f"[green]XAI Report generated and saved to {output_json}[/green]")
|
|
9564
|
+
console.print(f"Summary: {summary_text}")
|
|
9565
|
+
if hf_model:
|
|
9566
|
+
console.print(f"LLM Interpretation: {clinical_interpretation}")
|
|
9567
|
+
|
|
9568
|
+
except Exception as e:
|
|
9569
|
+
console.print(f"[bold red]Failed to generate XAI report:[/bold red] {e}")
|
|
9570
|
+
raise typer.Exit(code=1)
|
|
9571
|
+
|
|
9572
|
+
|
|
9368
9573
|
@research_app.command(name="results-index")
|
|
9369
9574
|
def research_results_index(
|
|
9370
9575
|
root: Annotated[
|
|
@@ -10013,9 +10218,13 @@ def research_glucose_model_jetson_train_hf(
|
|
|
10013
10218
|
Path,
|
|
10014
10219
|
typer.Option(help="Normalized glucose training dataset CSV/Parquet built by glucose-model build-dataset."),
|
|
10015
10220
|
] = Path("models/iints-glucose-forecast-v0/dataset/glucose_training_dataset.csv"),
|
|
10016
|
-
|
|
10221
|
+
base_hf_repo: Annotated[
|
|
10222
|
+
Optional[str],
|
|
10223
|
+
typer.Option("--base-hf-repo", "--repo-id", help="External Hugging Face model to pull from (e.g. username/GlucoFM). If empty, pulls from target_hf_repo. --repo-id is kept as a compatibility alias."),
|
|
10224
|
+
] = None,
|
|
10225
|
+
target_hf_repo: Annotated[
|
|
10017
10226
|
Optional[str],
|
|
10018
|
-
typer.Option("--repo
|
|
10227
|
+
typer.Option("--target-hf-repo", help="Your Hugging Face model repo id to push to, e.g. username/iints-glucose-forecast-v0."),
|
|
10019
10228
|
] = "IINTS/iints-glucose-forecast-v0",
|
|
10020
10229
|
local_base_dir: Annotated[
|
|
10021
10230
|
Optional[Path],
|
|
@@ -10076,7 +10285,8 @@ def research_glucose_model_jetson_train_hf(
|
|
|
10076
10285
|
from iints.research.jetson_hf_trainer import run_jetson_hf_training
|
|
10077
10286
|
|
|
10078
10287
|
result = run_jetson_hf_training(
|
|
10079
|
-
|
|
10288
|
+
base_repo_id=base_hf_repo or target_hf_repo,
|
|
10289
|
+
target_repo_id=target_hf_repo,
|
|
10080
10290
|
dataset=dataset,
|
|
10081
10291
|
work_dir=work_dir,
|
|
10082
10292
|
local_base_dir=local_base_dir,
|
|
@@ -11459,11 +11669,11 @@ def docs_algo(
|
|
|
11459
11669
|
except Exception as e:
|
|
11460
11670
|
console.print(f"[bold red]Error loading algorithm module {algo_path}: {e}[/bold red]")
|
|
11461
11671
|
raise typer.Exit(code=1)
|
|
11462
|
-
|
|
11672
|
+
|
|
11463
11673
|
if algorithm_instance is None:
|
|
11464
11674
|
console.print(f"[bold red]Error: No subclass of InsulinAlgorithm found in {algo_path}[/bold red]")
|
|
11465
11675
|
raise typer.Exit(code=1)
|
|
11466
|
-
|
|
11676
|
+
|
|
11467
11677
|
# Extract Metadata
|
|
11468
11678
|
metadata = algorithm_instance.get_algorithm_metadata()
|
|
11469
11679
|
|
|
@@ -11531,7 +11741,7 @@ def benchmark(
|
|
|
11531
11741
|
if not scenarios_dir.is_dir():
|
|
11532
11742
|
console.print(f"[bold red]Error: Scenarios directory '{scenarios_dir}' not found.[/bold red]")
|
|
11533
11743
|
raise typer.Exit(code=1)
|
|
11534
|
-
|
|
11744
|
+
|
|
11535
11745
|
# Ensure output directory exists
|
|
11536
11746
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
11537
11747
|
|
|
@@ -11595,7 +11805,7 @@ def benchmark(
|
|
|
11595
11805
|
for scenario_file in scenario_files:
|
|
11596
11806
|
scenario_name = scenario_file.stem
|
|
11597
11807
|
console.print(f" [bold]Scenario: {scenario_name}[/bold]")
|
|
11598
|
-
|
|
11808
|
+
|
|
11599
11809
|
# Load Scenario Data (validated)
|
|
11600
11810
|
try:
|
|
11601
11811
|
scenario_model = load_scenario(scenario_file)
|
|
@@ -11622,7 +11832,7 @@ def benchmark(
|
|
|
11622
11832
|
)
|
|
11623
11833
|
for event in stress_events:
|
|
11624
11834
|
simulator_ai.add_stress_event(event)
|
|
11625
|
-
|
|
11835
|
+
|
|
11626
11836
|
try:
|
|
11627
11837
|
results_df_ai, safety_report_ai = simulator_ai.run_batch(duration)
|
|
11628
11838
|
metrics_ai = iints.generate_benchmark_metrics(results_df_ai)
|
|
@@ -11637,7 +11847,7 @@ def benchmark(
|
|
|
11637
11847
|
console.print(f" Running [yellow]Standard Pump[/yellow]...") # Use name directly
|
|
11638
11848
|
# The standard pump also needs patient-specific parameters for ISF, ICR, basal rate, etc.
|
|
11639
11849
|
# We'll pass the patient_params directly to the StandardPumpAlgorithm constructor.
|
|
11640
|
-
standard_pump_algo_instance = iints.StandardPumpAlgorithm(settings=patient_params)
|
|
11850
|
+
standard_pump_algo_instance = iints.StandardPumpAlgorithm(settings=patient_params)
|
|
11641
11851
|
patient_model_std = iints.PatientModel(**patient_params) # New instance for each run
|
|
11642
11852
|
simulator_std = iints.Simulator(
|
|
11643
11853
|
patient_model=patient_model_std,
|
|
@@ -11647,7 +11857,7 @@ def benchmark(
|
|
|
11647
11857
|
)
|
|
11648
11858
|
for event in stress_events:
|
|
11649
11859
|
simulator_std.add_stress_event(event)
|
|
11650
|
-
|
|
11860
|
+
|
|
11651
11861
|
try:
|
|
11652
11862
|
results_df_std, safety_report_std = simulator_std.run_batch(duration)
|
|
11653
11863
|
metrics_std = iints.generate_benchmark_metrics(results_df_std)
|
|
@@ -11673,19 +11883,19 @@ def benchmark(
|
|
|
11673
11883
|
**{f"Std Safety Violations": safety_report_std.get('total_violations', float('nan'))},
|
|
11674
11884
|
})
|
|
11675
11885
|
run_index += 1
|
|
11676
|
-
|
|
11886
|
+
|
|
11677
11887
|
console.print("\n[bold green]Benchmark Suite Completed![/bold green]")
|
|
11678
11888
|
|
|
11679
11889
|
# Print Comparison Table
|
|
11680
11890
|
if benchmark_results:
|
|
11681
11891
|
results_df = pd.DataFrame(benchmark_results)
|
|
11682
|
-
|
|
11892
|
+
|
|
11683
11893
|
table = Table(title="IINTS-AF Benchmark Results", show_header=True, header_style="bold magenta")
|
|
11684
|
-
|
|
11894
|
+
|
|
11685
11895
|
# Add columns dynamically
|
|
11686
11896
|
table.add_column("Patient", style="cyan", no_wrap=True)
|
|
11687
11897
|
table.add_column("Scenario", style="cyan", no_wrap=True)
|
|
11688
|
-
|
|
11898
|
+
|
|
11689
11899
|
# Assuming AI Algo and Standard Algo names are consistent across results
|
|
11690
11900
|
ai_algo_name = benchmark_results[0]["AI Algo"]
|
|
11691
11901
|
std_algo_name = benchmark_results[0]["Standard Algo"]
|
|
@@ -11697,7 +11907,7 @@ def benchmark(
|
|
|
11697
11907
|
for metric_name_raw in sample_metrics_keys_raw:
|
|
11698
11908
|
table.add_column(f"{ai_algo_name} {metric_name_raw}", style="green")
|
|
11699
11909
|
table.add_column(f"{std_algo_name} {metric_name_raw}", style="yellow")
|
|
11700
|
-
|
|
11910
|
+
|
|
11701
11911
|
table.add_column(f"{ai_algo_name} Safety Violations", style="red")
|
|
11702
11912
|
table.add_column(f"{std_algo_name} Safety Violations", style="red")
|
|
11703
11913
|
|
|
@@ -11706,13 +11916,13 @@ def benchmark(
|
|
|
11706
11916
|
for metric_name_raw in sample_metrics_keys_raw:
|
|
11707
11917
|
ai_val = row[f'AI {metric_name_raw}']
|
|
11708
11918
|
std_val = row[f'Std {metric_name_raw}']
|
|
11709
|
-
|
|
11919
|
+
|
|
11710
11920
|
ai_formatted = f"{ai_val:.2f}%" if "%" in metric_name_raw and not pd.isna(ai_val) else (f"{ai_val:.2f}" if not pd.isna(ai_val) else "N/A")
|
|
11711
11921
|
std_formatted = f"{std_val:.2f}%" if "%" in metric_name_raw and not pd.isna(std_val) else (f"{std_val:.2f}" if not pd.isna(std_val) else "N/A")
|
|
11712
|
-
|
|
11922
|
+
|
|
11713
11923
|
row_data.append(ai_formatted)
|
|
11714
11924
|
row_data.append(std_formatted)
|
|
11715
|
-
|
|
11925
|
+
|
|
11716
11926
|
ai_safety_violations = row['AI Safety Violations']
|
|
11717
11927
|
std_safety_violations = row['Std Safety Violations']
|
|
11718
11928
|
row_data.append(f"{ai_safety_violations:.0f}" if not pd.isna(ai_safety_violations) else "N/A")
|
|
@@ -14034,3 +14244,88 @@ def edge_benchmark_alias(
|
|
|
14034
14244
|
api_port=api_port,
|
|
14035
14245
|
seed=seed,
|
|
14036
14246
|
)
|
|
14247
|
+
|
|
14248
|
+
@app.command(name="render-molecules")
|
|
14249
|
+
def render_molecules(
|
|
14250
|
+
target: Annotated[str, typer.Option("--target", help="The structural target to render (e.g. insulin-mutation, glucagon, glut4, insulin-receptor, or all)")] = "all",
|
|
14251
|
+
) -> None:
|
|
14252
|
+
"""
|
|
14253
|
+
Render explanatory 3D structural-biology targets using AlphaFold and PyMOL.
|
|
14254
|
+
|
|
14255
|
+
The generated structures are research/education artifacts. They help connect
|
|
14256
|
+
SDK model terms such as insulin, glucagon, GLUT4, and receptor biology to
|
|
14257
|
+
public protein-structure data, but they are not simulator inputs or clinical
|
|
14258
|
+
evidence.
|
|
14259
|
+
"""
|
|
14260
|
+
from iints.research.structure import render_target
|
|
14261
|
+
render_target(target)
|
|
14262
|
+
|
|
14263
|
+
@app.command(name="render-pathways")
|
|
14264
|
+
def render_pathways(
|
|
14265
|
+
network: Annotated[str, typer.Option("--network", help="The physiological network to render (e.g. insulin-cascade, glucagon-rescue, or all)")] = "all",
|
|
14266
|
+
) -> None:
|
|
14267
|
+
"""
|
|
14268
|
+
Download explanatory physiological pathway images from the STRING Database.
|
|
14269
|
+
|
|
14270
|
+
The rendered protein-interaction networks help users see how model concepts
|
|
14271
|
+
such as insulin signalling, GLUT4 translocation, and glucagon rescue relate
|
|
14272
|
+
to known biological pathways. They do not calibrate or replace the simulator.
|
|
14273
|
+
"""
|
|
14274
|
+
from iints.research.physiology import render_pathways as fetch_networks
|
|
14275
|
+
fetch_networks(network)
|
|
14276
|
+
|
|
14277
|
+
@app.command(name="render-pae")
|
|
14278
|
+
def render_pae(
|
|
14279
|
+
target: Annotated[str, typer.Option("--target", help="The structural target to render the PAE matrix for (e.g. insulin-mutation, glucagon, glut4, insulin-receptor, or all)")] = "all",
|
|
14280
|
+
) -> None:
|
|
14281
|
+
"""
|
|
14282
|
+
Render interactive Predicted Aligned Error (PAE) matrices from AlphaFold.
|
|
14283
|
+
|
|
14284
|
+
The Plotly heatmaps show AlphaFold's predicted relative-position uncertainty
|
|
14285
|
+
between residues. PAE is useful for structural context only; it is not a
|
|
14286
|
+
glucose, dosing, safety-supervisor, or treatment metric.
|
|
14287
|
+
"""
|
|
14288
|
+
from iints.research.structure import render_pae as fetch_pae
|
|
14289
|
+
fetch_pae(target)
|
|
14290
|
+
|
|
14291
|
+
@app.command(name="simulate-mutation")
|
|
14292
|
+
def simulate_mutation(
|
|
14293
|
+
gene: Annotated[str, typer.Option("--gene", help="The gene to simulate a mutation for (e.g. INSR or INS)")] = "INSR",
|
|
14294
|
+
) -> None:
|
|
14295
|
+
"""
|
|
14296
|
+
Fetch ClinVar variant summaries and show deterministic stress-test mappings.
|
|
14297
|
+
|
|
14298
|
+
The SDK currently includes curated educational mappings for genes such as
|
|
14299
|
+
INSR and INS. These mappings create virtual-patient edge cases for research;
|
|
14300
|
+
they are not diagnostic genetics or patient-specific interpretation.
|
|
14301
|
+
"""
|
|
14302
|
+
from iints.research.genetics import simulate_mutation as run_sim
|
|
14303
|
+
run_sim(gene)
|
|
14304
|
+
|
|
14305
|
+
@app.command(name="analyze-insulin")
|
|
14306
|
+
def analyze_insulin(
|
|
14307
|
+
drug: Annotated[str, typer.Option("--drug", help="The insulin analog to analyze (e.g. lispro, glargine, regular)")] = "lispro",
|
|
14308
|
+
) -> None:
|
|
14309
|
+
"""
|
|
14310
|
+
Fetch ChEMBL molecule context and show deterministic insulin PK mappings.
|
|
14311
|
+
|
|
14312
|
+
ChEMBL provides public pharmacology context. The simulator absorption values
|
|
14313
|
+
are fixed SDK defaults for common insulin classes, not patient-specific
|
|
14314
|
+
dosing guidance and not AI-generated at runtime.
|
|
14315
|
+
"""
|
|
14316
|
+
from iints.research.pharmacology import analyze_insulin as run_analysis
|
|
14317
|
+
run_analysis(drug)
|
|
14318
|
+
|
|
14319
|
+
@app.command(name="render-expression")
|
|
14320
|
+
def render_expression(
|
|
14321
|
+
gene: Annotated[str, typer.Option("--gene", help="The gene to map anatomically (e.g. GLUT4 or GCGR)")] = "GLUT4",
|
|
14322
|
+
) -> None:
|
|
14323
|
+
"""
|
|
14324
|
+
Fetch GTEx tissue expression data and render an interactive anatomy chart.
|
|
14325
|
+
|
|
14326
|
+
The chart helps explain why genes such as SLC2A4/GLUT4 matter for muscle
|
|
14327
|
+
glucose uptake and exercise modelling. It is supporting biology context, not
|
|
14328
|
+
automatic patient calibration.
|
|
14329
|
+
"""
|
|
14330
|
+
from iints.research.anatomy import render_expression as fetch_expression
|
|
14331
|
+
fetch_expression(gene)
|
|
@@ -98,12 +98,18 @@ class HovorkaPatientModel:
|
|
|
98
98
|
dawn_phenomenon_strength: float = 0.0,
|
|
99
99
|
dawn_start_hour: float = 4.0,
|
|
100
100
|
dawn_end_hour: float = 8.0,
|
|
101
|
+
molecular_affinity_scalar: float = 1.0,
|
|
102
|
+
muscle_sensitivity_scalar: float = 1.0,
|
|
103
|
+
liver_sensitivity_scalar: float = 1.0,
|
|
101
104
|
carb_absorption_duration_minutes: float = 240.0,
|
|
102
105
|
max_glucose_rate_mgdl_per_min: float = 3.0,
|
|
103
106
|
hovorka_params: Optional[HovorkaParameters] = None,
|
|
104
107
|
) -> None:
|
|
105
108
|
self.basal_insulin_rate = basal_insulin_rate
|
|
106
|
-
self.
|
|
109
|
+
self.molecular_affinity_scalar = molecular_affinity_scalar
|
|
110
|
+
self.muscle_sensitivity_scalar = muscle_sensitivity_scalar
|
|
111
|
+
self.liver_sensitivity_scalar = liver_sensitivity_scalar
|
|
112
|
+
self.insulin_sensitivity = insulin_sensitivity * molecular_affinity_scalar
|
|
107
113
|
self.carb_factor = carb_factor
|
|
108
114
|
self.initial_glucose = initial_glucose
|
|
109
115
|
self.basal_glucose_target = basal_glucose_target
|
|
@@ -152,8 +158,8 @@ class HovorkaPatientModel:
|
|
|
152
158
|
|
|
153
159
|
I_basal = 10.0 # mU/L approximation.
|
|
154
160
|
x1_init = p.S_IT * I_basal
|
|
155
|
-
x2_init = p.S_ID * I_basal
|
|
156
|
-
x3_init = p.S_IE * I_basal
|
|
161
|
+
x2_init = p.S_ID * self.muscle_sensitivity_scalar * I_basal
|
|
162
|
+
x3_init = p.S_IE * self.liver_sensitivity_scalar * I_basal
|
|
157
163
|
|
|
158
164
|
# State vector: [Q1, Q2, S1, S2, I, x1, x2, x3, D1, D2, D3, H_stress, H_exercise, Y1, Y2, Gamma, x_gluc, HAAF, GLUT4_active]
|
|
159
165
|
return np.array(
|
|
@@ -491,8 +497,8 @@ class HovorkaPatientModel:
|
|
|
491
497
|
overall_sens = stress_sens_multiplier * ex_sens_multiplier
|
|
492
498
|
|
|
493
499
|
k_b1 = p.S_IT * p.k_a1 * overall_sens
|
|
494
|
-
k_b2 = p.S_ID * p.k_a2 * overall_sens
|
|
495
|
-
k_b3 = p.S_IE * p.k_a3 * overall_sens
|
|
500
|
+
k_b2 = p.S_ID * p.k_a2 * overall_sens * self.muscle_sensitivity_scalar
|
|
501
|
+
k_b3 = p.S_IE * p.k_a3 * overall_sens * self.liver_sensitivity_scalar
|
|
496
502
|
|
|
497
503
|
# Insulin action
|
|
498
504
|
dx1_dt = -p.k_a1 * x1 + k_b1 * I
|
iints/core/patient/models.py
CHANGED
|
@@ -24,7 +24,8 @@ class CustomPatientModel:
|
|
|
24
24
|
dawn_start_hour: float = 4.0,
|
|
25
25
|
dawn_end_hour: float = 8.0,
|
|
26
26
|
carb_absorption_duration_minutes: float = 240.0,
|
|
27
|
-
max_glucose_rate_mgdl_per_min: float = 3.0
|
|
27
|
+
max_glucose_rate_mgdl_per_min: float = 3.0,
|
|
28
|
+
molecular_affinity_scalar: float = 1.0):
|
|
28
29
|
"""
|
|
29
30
|
Initializes the patient model with simplified parameters.
|
|
30
31
|
|
|
@@ -39,9 +40,11 @@ class CustomPatientModel:
|
|
|
39
40
|
insulin_peak_time (float): Time to peak insulin activity in minutes.
|
|
40
41
|
meal_mismatch_epsilon (float): The multiplier for carb intake to simulate meal size errors.
|
|
41
42
|
`true_carbs = announced_carbs * meal_mismatch_epsilon`. Defaults to 1.0.
|
|
43
|
+
molecular_affinity_scalar (float): Multiplier representing molecular binding affinity (1.0 = normal, 0.2 = 80% loss of function).
|
|
42
44
|
"""
|
|
43
45
|
self.basal_insulin_rate = basal_insulin_rate
|
|
44
|
-
self.
|
|
46
|
+
self.molecular_affinity_scalar = molecular_affinity_scalar
|
|
47
|
+
self.insulin_sensitivity = insulin_sensitivity * molecular_affinity_scalar
|
|
45
48
|
self.carb_factor = carb_factor
|
|
46
49
|
self.glucose_decay_rate = glucose_decay_rate
|
|
47
50
|
self.glucose_absorption_rate = glucose_absorption_rate
|
iints/core/simulator.py
CHANGED
|
@@ -724,12 +724,13 @@ class Simulator:
|
|
|
724
724
|
"""Alias for run_batch to ensure backward compatibility."""
|
|
725
725
|
return self.run_batch(duration_minutes)
|
|
726
726
|
|
|
727
|
-
def run_batch(self, duration_minutes: int) -> Tuple[pd.DataFrame, Dict[str, Any]]:
|
|
727
|
+
def run_batch(self, duration_minutes: int, step_callback: Optional[Callable[[int, int, float], None]] = None) -> Tuple[pd.DataFrame, Dict[str, Any]]:
|
|
728
728
|
"""
|
|
729
729
|
Runs the entire simulation and returns the results as a single DataFrame.
|
|
730
730
|
|
|
731
731
|
Args:
|
|
732
732
|
duration_minutes (int): Total simulation duration in minutes.
|
|
733
|
+
step_callback (Callable): Optional callback for live telemetry (time, duration, glucose).
|
|
733
734
|
|
|
734
735
|
Returns:
|
|
735
736
|
pd.DataFrame: A DataFrame containing the complete simulation results.
|
|
@@ -740,6 +741,8 @@ class Simulator:
|
|
|
740
741
|
try:
|
|
741
742
|
for record in self.run_live(duration_minutes):
|
|
742
743
|
all_records.append(record)
|
|
744
|
+
if step_callback:
|
|
745
|
+
step_callback(record["time"], duration_minutes, record["glucose"])
|
|
743
746
|
except SimulationLimitError as err:
|
|
744
747
|
logger.error("Simulation terminated early: %s", err)
|
|
745
748
|
self._termination_info = {
|