celltype-cli 0.1.0__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.
Files changed (89) hide show
  1. celltype_cli-0.1.0.dist-info/METADATA +267 -0
  2. celltype_cli-0.1.0.dist-info/RECORD +89 -0
  3. celltype_cli-0.1.0.dist-info/WHEEL +4 -0
  4. celltype_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. celltype_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. ct/__init__.py +3 -0
  7. ct/agent/__init__.py +0 -0
  8. ct/agent/case_studies.py +426 -0
  9. ct/agent/config.py +523 -0
  10. ct/agent/doctor.py +544 -0
  11. ct/agent/knowledge.py +523 -0
  12. ct/agent/loop.py +99 -0
  13. ct/agent/mcp_server.py +478 -0
  14. ct/agent/orchestrator.py +733 -0
  15. ct/agent/runner.py +656 -0
  16. ct/agent/sandbox.py +481 -0
  17. ct/agent/session.py +145 -0
  18. ct/agent/system_prompt.py +186 -0
  19. ct/agent/trace_store.py +228 -0
  20. ct/agent/trajectory.py +169 -0
  21. ct/agent/types.py +182 -0
  22. ct/agent/workflows.py +462 -0
  23. ct/api/__init__.py +1 -0
  24. ct/api/app.py +211 -0
  25. ct/api/config.py +120 -0
  26. ct/api/engine.py +124 -0
  27. ct/cli.py +1448 -0
  28. ct/data/__init__.py +0 -0
  29. ct/data/compute_providers.json +59 -0
  30. ct/data/cro_database.json +395 -0
  31. ct/data/downloader.py +238 -0
  32. ct/data/loaders.py +252 -0
  33. ct/kb/__init__.py +5 -0
  34. ct/kb/benchmarks.py +147 -0
  35. ct/kb/governance.py +106 -0
  36. ct/kb/ingest.py +415 -0
  37. ct/kb/reasoning.py +129 -0
  38. ct/kb/schema_monitor.py +162 -0
  39. ct/kb/substrate.py +387 -0
  40. ct/models/__init__.py +0 -0
  41. ct/models/llm.py +370 -0
  42. ct/tools/__init__.py +195 -0
  43. ct/tools/_compound_resolver.py +297 -0
  44. ct/tools/biomarker.py +368 -0
  45. ct/tools/cellxgene.py +282 -0
  46. ct/tools/chemistry.py +1371 -0
  47. ct/tools/claude.py +390 -0
  48. ct/tools/clinical.py +1153 -0
  49. ct/tools/clue.py +249 -0
  50. ct/tools/code.py +1069 -0
  51. ct/tools/combination.py +397 -0
  52. ct/tools/compute.py +402 -0
  53. ct/tools/cro.py +413 -0
  54. ct/tools/data_api.py +2114 -0
  55. ct/tools/design.py +295 -0
  56. ct/tools/dna.py +575 -0
  57. ct/tools/experiment.py +604 -0
  58. ct/tools/expression.py +655 -0
  59. ct/tools/files.py +957 -0
  60. ct/tools/genomics.py +1387 -0
  61. ct/tools/http_client.py +146 -0
  62. ct/tools/imaging.py +319 -0
  63. ct/tools/intel.py +223 -0
  64. ct/tools/literature.py +743 -0
  65. ct/tools/network.py +422 -0
  66. ct/tools/notification.py +111 -0
  67. ct/tools/omics.py +3330 -0
  68. ct/tools/ops.py +1230 -0
  69. ct/tools/parity.py +649 -0
  70. ct/tools/pk.py +245 -0
  71. ct/tools/protein.py +678 -0
  72. ct/tools/regulatory.py +643 -0
  73. ct/tools/remote_data.py +179 -0
  74. ct/tools/report.py +181 -0
  75. ct/tools/repurposing.py +376 -0
  76. ct/tools/safety.py +1280 -0
  77. ct/tools/shell.py +178 -0
  78. ct/tools/singlecell.py +533 -0
  79. ct/tools/statistics.py +552 -0
  80. ct/tools/structure.py +882 -0
  81. ct/tools/target.py +901 -0
  82. ct/tools/translational.py +123 -0
  83. ct/tools/viability.py +218 -0
  84. ct/ui/__init__.py +0 -0
  85. ct/ui/markdown.py +31 -0
  86. ct/ui/status.py +258 -0
  87. ct/ui/suggestions.py +567 -0
  88. ct/ui/terminal.py +1456 -0
  89. ct/ui/traces.py +112 -0
ct/tools/pk.py ADDED
@@ -0,0 +1,245 @@
1
+ """Pharmacokinetics tools for quick noncompartmental analysis (NCA)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+
9
+ from ct.tools import registry
10
+
11
+
12
+ def _to_float_list(values: list | None, field_name: str) -> tuple[list[float] | None, str | None]:
13
+ if values is None:
14
+ return None, f"'{field_name}' is required"
15
+ out: list[float] = []
16
+ try:
17
+ for value in values:
18
+ out.append(float(value))
19
+ except Exception:
20
+ return None, f"'{field_name}' must be a list of numeric values"
21
+ return out, None
22
+
23
+
24
+ def _safe_round(value: float | None, ndigits: int = 6) -> float | None:
25
+ if value is None:
26
+ return None
27
+ return round(float(value), ndigits)
28
+
29
+
30
+ @registry.register(
31
+ name="pk.nca_basic",
32
+ description="Run basic noncompartmental PK analysis (Cmax/Tmax/AUC/lambda_z/t1/2; CL if dose provided)",
33
+ category="pk",
34
+ parameters={
35
+ "times": "Sampling times (e.g., [0, 0.5, 1, 2, 4, 8, 24])",
36
+ "concentrations": "Observed concentrations aligned with times",
37
+ "dose": "Optional administered dose for CL/F and Vz/F",
38
+ "route": "Route type: 'iv' or 'extravascular' (default extravascular)",
39
+ "n_terminal_points": "Number of terminal points for lambda_z fit (default 3)",
40
+ "lloq": "Optional lower limit of quantification; values below are set to 0",
41
+ "subject_id": "Optional subject identifier for report labeling",
42
+ },
43
+ usage_guide=(
44
+ "Use when you have concentration-time data and need rapid PK triage metrics. "
45
+ "Returns noncompartmental metrics plus terminal-phase fit diagnostics."
46
+ ),
47
+ )
48
+ def nca_basic(
49
+ times: list | None = None,
50
+ concentrations: list | None = None,
51
+ dose: float | None = None,
52
+ route: str = "extravascular",
53
+ n_terminal_points: int = 3,
54
+ lloq: float | None = None,
55
+ subject_id: str = "",
56
+ **kwargs,
57
+ ) -> dict:
58
+ """Perform a basic NCA workflow from concentration-time observations."""
59
+ del kwargs
60
+
61
+ t_list, t_error = _to_float_list(times, "times")
62
+ if t_error:
63
+ return {"summary": t_error, "error": "invalid_times"}
64
+ c_list, c_error = _to_float_list(concentrations, "concentrations")
65
+ if c_error:
66
+ return {"summary": c_error, "error": "invalid_concentrations"}
67
+
68
+ assert t_list is not None and c_list is not None
69
+
70
+ if len(t_list) != len(c_list):
71
+ return {
72
+ "summary": f"Length mismatch: {len(t_list)} times vs {len(c_list)} concentrations",
73
+ "error": "length_mismatch",
74
+ }
75
+ if len(t_list) < 3:
76
+ return {
77
+ "summary": f"Need at least 3 observations for NCA, got {len(t_list)}",
78
+ "error": "insufficient_points",
79
+ }
80
+
81
+ warnings: list[str] = []
82
+ pairs = []
83
+ for time_value, conc_value in zip(t_list, c_list):
84
+ if not np.isfinite(time_value) or not np.isfinite(conc_value):
85
+ continue
86
+ pairs.append((float(time_value), float(conc_value)))
87
+ if len(pairs) < 3:
88
+ return {
89
+ "summary": "Need at least 3 finite concentration-time points after filtering.",
90
+ "error": "insufficient_finite_points",
91
+ }
92
+
93
+ pairs.sort(key=lambda x: x[0])
94
+ if pairs[0][0] < 0:
95
+ return {"summary": "Negative sampling times are not allowed.", "error": "negative_time"}
96
+
97
+ # Handle duplicate time points by averaging concentrations at each time.
98
+ dedup: dict[float, list[float]] = {}
99
+ for t_val, c_val in pairs:
100
+ dedup.setdefault(t_val, []).append(c_val)
101
+ if len(dedup) < len(pairs):
102
+ warnings.append("Duplicate time points detected; concentrations were averaged per unique time.")
103
+
104
+ times_sorted = np.array(sorted(dedup.keys()), dtype=float)
105
+ conc_sorted = np.array([float(np.mean(dedup[t])) for t in times_sorted], dtype=float)
106
+ if len(times_sorted) < 3:
107
+ return {
108
+ "summary": "Need at least 3 unique time points after deduplication.",
109
+ "error": "insufficient_unique_times",
110
+ }
111
+
112
+ if lloq is not None:
113
+ try:
114
+ lloq_value = float(lloq)
115
+ except Exception:
116
+ return {"summary": "lloq must be numeric.", "error": "invalid_lloq"}
117
+ if lloq_value < 0:
118
+ return {"summary": "lloq cannot be negative.", "error": "invalid_lloq"}
119
+ below = int(np.sum(conc_sorted < lloq_value))
120
+ if below > 0:
121
+ warnings.append(f"{below} concentration values below LLOQ were set to 0.")
122
+ conc_sorted = np.where(conc_sorted < lloq_value, 0.0, conc_sorted)
123
+
124
+ cmax_idx = int(np.argmax(conc_sorted))
125
+ cmax = float(conc_sorted[cmax_idx])
126
+ tmax = float(times_sorted[cmax_idx])
127
+
128
+ auc_last = 0.0
129
+ for i in range(1, len(times_sorted)):
130
+ dt = float(times_sorted[i] - times_sorted[i - 1])
131
+ if dt <= 0:
132
+ return {
133
+ "summary": "Sampling times must be strictly increasing after deduplication.",
134
+ "error": "non_increasing_time",
135
+ }
136
+ auc_last += 0.5 * float(conc_sorted[i] + conc_sorted[i - 1]) * dt
137
+
138
+ # Terminal elimination estimate (lambda_z) from last positive points.
139
+ n_terminal_points = int(n_terminal_points or 3)
140
+ if n_terminal_points < 3:
141
+ n_terminal_points = 3
142
+ warnings.append("n_terminal_points < 3 is not robust; using 3.")
143
+
144
+ positive_idx = np.where(conc_sorted > 0)[0]
145
+ lambda_z = None
146
+ terminal_r2 = None
147
+ half_life = None
148
+ terminal_points_used = 0
149
+ auc_extrap = None
150
+ auc_inf = None
151
+ extrapolated_fraction = None
152
+
153
+ if len(positive_idx) >= 3:
154
+ choose = min(n_terminal_points, len(positive_idx))
155
+ terminal_slice = positive_idx[-choose:]
156
+ t_term = times_sorted[terminal_slice]
157
+ c_term = conc_sorted[terminal_slice]
158
+ if len(c_term) >= 3 and np.all(c_term > 0):
159
+ slope, intercept = np.polyfit(t_term, np.log(c_term), 1)
160
+ terminal_points_used = len(c_term)
161
+ pred = slope * t_term + intercept
162
+ ss_res = float(np.sum((np.log(c_term) - pred) ** 2))
163
+ ss_tot = float(np.sum((np.log(c_term) - np.mean(np.log(c_term))) ** 2))
164
+ terminal_r2 = 1.0 - (ss_res / ss_tot) if ss_tot > 0 else None
165
+ if slope < 0:
166
+ lambda_z = float(-slope)
167
+ half_life = float(math.log(2.0) / lambda_z) if lambda_z > 0 else None
168
+ else:
169
+ warnings.append(
170
+ "Terminal slope is non-negative; lambda_z and half-life are not reliable."
171
+ )
172
+ else:
173
+ warnings.append("Insufficient positive terminal points for lambda_z estimation.")
174
+ else:
175
+ warnings.append("Fewer than 3 positive concentration points; cannot estimate lambda_z.")
176
+
177
+ clast = float(conc_sorted[-1])
178
+ if lambda_z and lambda_z > 0 and clast > 0:
179
+ auc_extrap = float(clast / lambda_z)
180
+ auc_inf = float(auc_last + auc_extrap)
181
+ if auc_inf > 0:
182
+ extrapolated_fraction = float(auc_extrap / auc_inf)
183
+ if extrapolated_fraction > 0.2:
184
+ warnings.append(
185
+ "Extrapolated AUC fraction > 20%; terminal sampling may be insufficient."
186
+ )
187
+
188
+ route_norm = str(route or "extravascular").strip().lower()
189
+ if route_norm not in {"iv", "extravascular"}:
190
+ return {"summary": "route must be 'iv' or 'extravascular'.", "error": "invalid_route"}
191
+
192
+ dose_value = None
193
+ if dose is not None:
194
+ try:
195
+ dose_value = float(dose)
196
+ except Exception:
197
+ return {"summary": "dose must be numeric when provided.", "error": "invalid_dose"}
198
+ if dose_value <= 0:
199
+ return {"summary": "dose must be > 0 when provided.", "error": "invalid_dose"}
200
+
201
+ clearance = None
202
+ apparent_clearance = None
203
+ volume = None
204
+ apparent_volume = None
205
+
206
+ if dose_value is not None and auc_inf and auc_inf > 0:
207
+ if route_norm == "iv":
208
+ clearance = float(dose_value / auc_inf)
209
+ if lambda_z and lambda_z > 0:
210
+ volume = float(clearance / lambda_z)
211
+ else:
212
+ apparent_clearance = float(dose_value / auc_inf)
213
+ if lambda_z and lambda_z > 0:
214
+ apparent_volume = float(apparent_clearance / lambda_z)
215
+
216
+ label = subject_id.strip() if subject_id else "sample"
217
+ hl_text = f"{half_life:.3g}" if half_life is not None else "NA"
218
+ summary = (
219
+ f"Basic NCA for {label}: Cmax={cmax:.4g} at Tmax={tmax:.4g}, "
220
+ f"AUC_last={auc_last:.4g}, t1/2={hl_text}."
221
+ )
222
+
223
+ return {
224
+ "summary": summary,
225
+ "subject_id": subject_id.strip() or None,
226
+ "n_points": int(len(times_sorted)),
227
+ "terminal_points_used": int(terminal_points_used),
228
+ "route": route_norm,
229
+ "dose": _safe_round(dose_value, 6),
230
+ "cmax": _safe_round(cmax, 6),
231
+ "tmax": _safe_round(tmax, 6),
232
+ "auc_last": _safe_round(auc_last, 6),
233
+ "clast": _safe_round(clast, 6),
234
+ "lambda_z": _safe_round(lambda_z, 6),
235
+ "terminal_r_squared": _safe_round(terminal_r2, 6),
236
+ "half_life": _safe_round(half_life, 6),
237
+ "auc_extrapolated": _safe_round(auc_extrap, 6),
238
+ "auc_inf": _safe_round(auc_inf, 6),
239
+ "extrapolated_fraction": _safe_round(extrapolated_fraction, 6),
240
+ "clearance": _safe_round(clearance, 6),
241
+ "volume_distribution": _safe_round(volume, 6),
242
+ "apparent_clearance": _safe_round(apparent_clearance, 6),
243
+ "apparent_volume_distribution": _safe_round(apparent_volume, 6),
244
+ "warnings": warnings,
245
+ }