iints-sdk-python35 1.5.19__py3-none-any.whl → 1.5.22__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 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.19"
14
+ __version__ = "1.5.22"
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.
@@ -854,9 +854,15 @@ class ClinicalReportGenerator:
854
854
  ax.set_ylabel("Sensor glucose (mg/dL)")
855
855
  ax.grid(True, alpha=0.22)
856
856
  ax.legend(loc="upper left", fontsize=7, frameon=True)
857
- fig.tight_layout()
858
- fig.savefig(output_path, dpi=260, bbox_inches="tight")
859
- plt.close(fig)
857
+ try:
858
+ fig.tight_layout()
859
+ fig.savefig(output_path, dpi=260, bbox_inches="tight")
860
+ except ValueError as exc:
861
+ logger.warning("Clinical validation plot layout failed; saving fallback layout: %s", exc)
862
+ fig.subplots_adjust(left=0.08, right=0.98, top=0.9, bottom=0.12)
863
+ fig.savefig(output_path, dpi=260)
864
+ finally:
865
+ plt.close(fig)
860
866
 
861
867
  @staticmethod
862
868
  def _metric_delta(current: float, target: float, higher_is_better: bool = True) -> str:
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 = {
iints/highlevel.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
4
  from dataclasses import asdict
5
- from typing import Any, Dict, Optional, Union
5
+ from typing import Any, Dict, Optional, Union, Callable
6
6
 
7
7
  import pandas as pd
8
8
  import yaml
@@ -296,6 +296,7 @@ def run_full(
296
296
  enable_profiling: bool = True,
297
297
  safety_config: Optional[SafetyConfig] = None,
298
298
  predictor: Optional[object] = None,
299
+ step_callback: Optional[Callable[[int, int, float], None]] = None,
299
300
  ) -> Dict[str, Any]:
300
301
  """
301
302
  One-line runner that always exports results + audit + PDF + baseline comparison.
@@ -341,7 +342,7 @@ def run_full(
341
342
  for event in build_stress_events(stress_event_payloads):
342
343
  simulator.add_stress_event(event)
343
344
 
344
- results_df, safety_report = simulator.run_batch(duration_minutes)
345
+ results_df, safety_report = simulator.run_batch(duration_minutes, step_callback=step_callback)
345
346
 
346
347
  outputs: Dict[str, Any] = {
347
348
  "results": results_df,
@@ -0,0 +1,91 @@
1
+ import urllib.request
2
+ import json
3
+ from typing import Dict, Any
4
+
5
+ class AlphaFoldGenomicsEngine:
6
+ """
7
+ Bridges deep 3D structural AI (AlphaFold) to physiological equations.
8
+ """
9
+
10
+ @staticmethod
11
+ def evaluate_plddt_impact(uniprot_id: str, residue_index: int) -> Dict[str, Any]:
12
+ """
13
+ Fetches AlphaFold structure, finds the pLDDT for the residue,
14
+ and mathematically translates it into a molecular_affinity_scalar.
15
+ """
16
+ api_url = f"https://alphafold.ebi.ac.uk/api/prediction/{uniprot_id}"
17
+
18
+ try:
19
+ req = urllib.request.Request(api_url, headers={'User-Agent': 'IINTS-AF-SDK/1.0'})
20
+ with urllib.request.urlopen(req) as response:
21
+ if response.status != 200:
22
+ return {"error": f"Failed to query AlphaFold API for {uniprot_id} (Status {response.status})"}
23
+ data = json.loads(response.read().decode())
24
+ except Exception as e:
25
+ return {"error": f"AlphaFold API request failed: {e}"}
26
+
27
+ if not data:
28
+ return {"error": f"No AlphaFold prediction found for {uniprot_id}"}
29
+
30
+ # Find the correct fragment for large proteins
31
+ target_fragment = None
32
+ for frag in data:
33
+ start = frag.get("uniprotStart", 1)
34
+ end = frag.get("uniprotEnd", 999999)
35
+ if start <= residue_index <= end:
36
+ target_fragment = frag
37
+ break
38
+
39
+ if not target_fragment:
40
+ return {"error": f"Residue {residue_index} is out of bounds for {uniprot_id}."}
41
+
42
+ pdb_url = target_fragment.get("pdbUrl")
43
+ if not pdb_url:
44
+ return {"error": "PDB file not available in AlphaFold DB for this protein."}
45
+
46
+ try:
47
+ pdb_req = urllib.request.Request(pdb_url, headers={'User-Agent': 'IINTS-AF-SDK/1.0'})
48
+ with urllib.request.urlopen(pdb_req) as response:
49
+ pdb_text = response.read().decode()
50
+ except Exception as e:
51
+ return {"error": f"Failed to download PDB file: {e}"}
52
+
53
+ # Parse PDB text to find pLDDT (B-factor is in columns 61-66)
54
+ plddt_values = []
55
+ for line in pdb_text.split("\n"):
56
+ if line.startswith("ATOM "):
57
+ try:
58
+ res_num = int(line[22:26].strip())
59
+ if res_num == residue_index:
60
+ b_factor = float(line[60:66].strip())
61
+ plddt_values.append(b_factor)
62
+ except ValueError:
63
+ pass
64
+
65
+ if not plddt_values:
66
+ return {"error": f"Residue {residue_index} not found in AlphaFold structure."}
67
+
68
+ avg_plddt = sum(plddt_values) / len(plddt_values)
69
+
70
+ # Mathematical translation
71
+ if avg_plddt >= 90:
72
+ scalar = 0.15
73
+ conclusion = "Highly structured core domain. Mutation is likely catastrophic."
74
+ elif avg_plddt >= 70:
75
+ scalar = 0.40
76
+ conclusion = "Structured domain. Mutation likely impairs function."
77
+ elif avg_plddt >= 50:
78
+ scalar = 0.75
79
+ conclusion = "Moderate flexibility. Mutation partially tolerated."
80
+ else:
81
+ scalar = 0.95
82
+ conclusion = "Intrinsically disordered/flexible region. Mutation is highly tolerated."
83
+
84
+ return {
85
+ "uniprot_id": uniprot_id,
86
+ "residue_index": residue_index,
87
+ "plddt": round(avg_plddt, 2),
88
+ "scalar": scalar,
89
+ "conclusion": conclusion,
90
+ "pdb_url": pdb_url
91
+ }
@@ -57,14 +57,33 @@ class GenomicsEngine:
57
57
 
58
58
  @staticmethod
59
59
  def evaluate_mutation(gene: str, variant: str) -> dict[str, Any]:
60
- """Translate a variant such as ``INSR V938M`` into a functional scalar."""
60
+ """Translate a variant such as ``INSR V938M`` into a functional scalar using AlphaFold."""
61
+ import re
62
+ from iints.research.alphafold_engine import AlphaFoldGenomicsEngine
61
63
 
62
64
  variant = variant.upper().strip()
63
- if gene.upper() != "INSR":
64
- return {"scalar": 1.0, "desc": f"Gene {gene} not supported yet.", "residue": None}
65
- if variant in KNOWN_MUTATIONS:
65
+
66
+ # Check known mutations first for fast/offline execution
67
+ if gene.upper() == "INSR" and variant in KNOWN_MUTATIONS:
66
68
  return dict(KNOWN_MUTATIONS[variant])
67
- return {"scalar": 0.5, "desc": "Unknown mutation (assumed 50% loss of function)", "residue": None}
69
+
70
+ # Determine UniProt ID (INSR -> P06213)
71
+ uniprot_id = "P06213" if gene.upper() == "INSR" else gene.strip()
72
+
73
+ # Extract residue index from variant (e.g. V938M -> 938)
74
+ match = re.search(r'\d+', variant)
75
+ if not match:
76
+ return {"scalar": 0.5, "desc": "Unknown mutation format (assumed 50% loss of function)", "residue": None}
77
+
78
+ residue_idx = int(match.group())
79
+
80
+ # Query AlphaFold
81
+ af_result = AlphaFoldGenomicsEngine.evaluate_plddt_impact(uniprot_id, residue_idx)
82
+ if "error" in af_result:
83
+ return {"scalar": 0.5, "desc": f"AlphaFold fallback (50% loss): {af_result['error']}", "residue": residue_idx}
84
+
85
+ desc = f"AlphaFold pLDDT: {af_result['plddt']}. {af_result['conclusion']}"
86
+ return {"scalar": af_result['scalar'], "desc": desc, "residue": residue_idx}
68
87
 
69
88
  @staticmethod
70
89
  def run_multi_scale_simulation(
iints/utils/plotting.py CHANGED
@@ -54,6 +54,11 @@ def apply_plot_style(
54
54
  "ytick.labelsize": 10,
55
55
  "axes.spines.top": False,
56
56
  "axes.spines.right": False,
57
+ # Scientific styles can enable mathtext tick formatting. Some
58
+ # Matplotlib/macOS combinations then try to parse empty ticklabels
59
+ # during tight_layout(), which can abort report generation.
60
+ "axes.formatter.use_mathtext": False,
61
+ "text.usetex": False,
57
62
  }
58
63
  )
59
64
  return colors
iints_desktop/engine.py CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  from datetime import datetime, timezone
7
7
  from importlib import metadata
8
8
  from pathlib import Path
9
- from typing import Any
9
+ from typing import Any, Callable
10
10
 
11
11
 
12
12
  @dataclass(frozen=True)
@@ -174,6 +174,7 @@ def run_demo_preset(
174
174
  desktop_preset_key: str = DEFAULT_DESKTOP_PRESET_KEY,
175
175
  preset_name: str | None = None,
176
176
  seed: int = 42,
177
+ step_callback: Callable[[int, int, float], None] | None = None,
177
178
  ) -> DesktopRunResult:
178
179
  """Run one deterministic demo preset through the normal SDK engine.
179
180
 
@@ -205,6 +206,7 @@ def run_demo_preset(
205
206
  time_step=int(preset["time_step_minutes"]),
206
207
  seed=seed,
207
208
  output_dir=target,
209
+ step_callback=step_callback,
208
210
  )
209
211
 
210
212
  results_csv = _optional_path(outputs.get("results_csv"))
@@ -243,6 +245,71 @@ def run_demo_preset(
243
245
  return result
244
246
 
245
247
 
248
+ def run_custom_preset(
249
+ *,
250
+ output_dir: str | Path,
251
+ custom_preset: dict[str, Any],
252
+ seed: int = 42,
253
+ step_callback: Callable[[int, int, float], None] | None = None,
254
+ ) -> DesktopRunResult:
255
+ """Run a dynamically constructed scenario from the UI."""
256
+ base_output = Path(output_dir).expanduser().resolve()
257
+ mpl_cache = base_output / ".cache" / "matplotlib"
258
+ mpl_cache.mkdir(parents=True, exist_ok=True)
259
+ os.environ.setdefault("MPLCONFIGDIR", str(mpl_cache))
260
+
261
+ from iints.core.algorithms.clinical_baseline import ClinicalBaselineAlgorithm
262
+ from iints.highlevel import run_full
263
+
264
+ resolved_preset_name = custom_preset.get("name", "custom_scenario")
265
+ folder_name = _safe_slug(f"custom-{resolved_preset_name}-{seed}")
266
+ target = base_output / folder_name
267
+ outputs: dict[str, Any] = run_full(
268
+ algorithm=ClinicalBaselineAlgorithm(),
269
+ scenario=custom_preset.get("scenario", {}),
270
+ patient_config=custom_preset.get("patient_config", {}),
271
+ duration_minutes=int(custom_preset.get("duration_minutes", 1440)),
272
+ time_step=int(custom_preset.get("time_step_minutes", 5)),
273
+ seed=seed,
274
+ output_dir=target,
275
+ step_callback=step_callback,
276
+ )
277
+
278
+ results_csv = _optional_path(outputs.get("results_csv"))
279
+ report_pdf = _optional_path(outputs.get("report_pdf"))
280
+ config_path = _optional_path(outputs.get("config_path"))
281
+ run_id = str(outputs.get("run_id", "unknown-run"))
282
+ summary = "\n".join(
283
+ line
284
+ for line in [
285
+ "Workflow: Custom Scenario Builder",
286
+ f"SDK preset: {resolved_preset_name}",
287
+ f"Seed: {seed}",
288
+ f"Run completed: {run_id}",
289
+ f"Output folder: {target}",
290
+ f"Results CSV: {results_csv}" if results_csv else "Results CSV: not generated",
291
+ f"Clinical report: {report_pdf}" if report_pdf else "Clinical report: not generated",
292
+ "Research only: not a medical device and not for treatment decisions.",
293
+ ]
294
+ )
295
+ result = DesktopRunResult(
296
+ run_id=run_id,
297
+ workflow_title="Custom Scenario",
298
+ preset_name=resolved_preset_name,
299
+ seed=seed,
300
+ output_dir=target,
301
+ results_csv=results_csv,
302
+ report_pdf=report_pdf,
303
+ config_path=config_path,
304
+ summary=summary,
305
+ )
306
+ try:
307
+ append_run_history(base_output, result)
308
+ except OSError:
309
+ pass
310
+ return result
311
+
312
+
246
313
  def _optional_path(value: object) -> Path | None:
247
314
  if value is None:
248
315
  return None
iints_desktop/local_ai.py CHANGED
@@ -19,6 +19,7 @@ RECOMMENDED_OLLAMA_MODELS = (
19
19
  "llama3.1:8b",
20
20
  "qwen2.5:7b",
21
21
  "gemma3:4b",
22
+ "hf.co/devanshamin/PubMedDiabetes-LLM-Predictions",
22
23
  )
23
24
 
24
25
 
@@ -45,22 +46,19 @@ class LocalAIStartResult:
45
46
  pulled_model: bool = False
46
47
 
47
48
 
48
- SYSTEM_PROMPT = """You are the local IINTS-AF desktop research assistant.
49
-
50
- Rules:
51
- - Research and education only.
52
- - Not a medical device.
53
- - Do not provide diagnosis, insulin dosing, treatment instructions, or real-time patient care advice.
54
- - Be critical about simulation limitations, data quality, uncertainty, and physiology.
55
- - If the user asks for dosing/treatment decisions, refuse briefly and redirect to safe research interpretation.
56
- - Prefer clear, plain-language explanations over hype.
57
- - If result-summary context is provided, explain what it suggests without pretending it is clinically validated.
58
- - Use this exact readable structure:
59
- Summary
60
- Key observations
61
- Limitations
62
- Next checks
63
- - Use short paragraphs or numbered points. Do not use markdown tables, decorative separators, or long raw bullet lists.
49
+ SYSTEM_PROMPT = """You are a highly advanced Medical Data Scientist and Computational Biologist analyzing output from the IINTS-AF closed-loop insulin simulator.
50
+
51
+ CRITICAL INSTRUCTIONS:
52
+ 1. Speak with absolute professional authority and scientific rigor. Do NOT sound like an AI assistant. Do NOT use vague conversational filler.
53
+ 2. Analyze the provided clinical simulation data. Extract meaningful physiological insights, glycemic control patterns (Time in Range, Coefficient of Variation), and algorithmic behaviors (e.g. basal suspension aggressiveness, insulin resistance masking).
54
+ 3. Do not include boilerplate legal disclaimers at the beginning of your text. If necessary, include a single sentence limitation at the very end.
55
+ 4. Structure your response explicitly using these exact headers:
56
+ Clinical Overview
57
+ Biomathematical Observations
58
+ Algorithmic Behavior
59
+ Conclusions
60
+ 5. Use highly specific scientific terminology (e.g., exogenous insulin kinetics, hepatic glucose production, PI3K/AKT pathway attenuation).
61
+ 6. Acknowledge that the IINTS-AF simulator is for research and education only. It is Not a medical device. Do not provide diagnosis, insulin dosing, or treatment advice.
64
62
  """
65
63
 
66
64
 
@@ -280,7 +278,12 @@ def format_ai_answer(text: str) -> str:
280
278
  line = "• " + line[2:].strip()
281
279
  if line.startswith(("---", "___", "***")):
282
280
  continue
283
- if line.lower().rstrip(":") in {"summary", "key observations", "limitations", "next checks"}:
281
+ if line.lower().rstrip(":") in {
282
+ "clinical overview",
283
+ "biomathematical observations",
284
+ "algorithmic behavior",
285
+ "conclusions"
286
+ }:
284
287
  line = line.rstrip(":").title()
285
288
  lines.append(line)
286
289
  return "\n".join(lines).strip()
@@ -309,8 +312,8 @@ def ask_local_ai(
309
312
  user_prompt = (
310
313
  f"Result context:\n{context}\n\n"
311
314
  f"User question:\n{question.strip()}\n\n"
312
- "Answer as a critical SDK research assistant. Mention limitations and avoid treatment advice. "
313
- "Make the response readable in a desktop app text panel."
315
+ "Perform a highly technical, rigorous analysis based purely on the data. "
316
+ "Do not offer generic advice. Structure the response perfectly."
314
317
  )
315
318
  answer = backend.complete(system_prompt=SYSTEM_PROMPT, user_prompt=user_prompt)
316
319
  resolved = backend.resolved_model_name or model