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.
- celltype_cli-0.1.0.dist-info/METADATA +267 -0
- celltype_cli-0.1.0.dist-info/RECORD +89 -0
- celltype_cli-0.1.0.dist-info/WHEEL +4 -0
- celltype_cli-0.1.0.dist-info/entry_points.txt +2 -0
- celltype_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ct/__init__.py +3 -0
- ct/agent/__init__.py +0 -0
- ct/agent/case_studies.py +426 -0
- ct/agent/config.py +523 -0
- ct/agent/doctor.py +544 -0
- ct/agent/knowledge.py +523 -0
- ct/agent/loop.py +99 -0
- ct/agent/mcp_server.py +478 -0
- ct/agent/orchestrator.py +733 -0
- ct/agent/runner.py +656 -0
- ct/agent/sandbox.py +481 -0
- ct/agent/session.py +145 -0
- ct/agent/system_prompt.py +186 -0
- ct/agent/trace_store.py +228 -0
- ct/agent/trajectory.py +169 -0
- ct/agent/types.py +182 -0
- ct/agent/workflows.py +462 -0
- ct/api/__init__.py +1 -0
- ct/api/app.py +211 -0
- ct/api/config.py +120 -0
- ct/api/engine.py +124 -0
- ct/cli.py +1448 -0
- ct/data/__init__.py +0 -0
- ct/data/compute_providers.json +59 -0
- ct/data/cro_database.json +395 -0
- ct/data/downloader.py +238 -0
- ct/data/loaders.py +252 -0
- ct/kb/__init__.py +5 -0
- ct/kb/benchmarks.py +147 -0
- ct/kb/governance.py +106 -0
- ct/kb/ingest.py +415 -0
- ct/kb/reasoning.py +129 -0
- ct/kb/schema_monitor.py +162 -0
- ct/kb/substrate.py +387 -0
- ct/models/__init__.py +0 -0
- ct/models/llm.py +370 -0
- ct/tools/__init__.py +195 -0
- ct/tools/_compound_resolver.py +297 -0
- ct/tools/biomarker.py +368 -0
- ct/tools/cellxgene.py +282 -0
- ct/tools/chemistry.py +1371 -0
- ct/tools/claude.py +390 -0
- ct/tools/clinical.py +1153 -0
- ct/tools/clue.py +249 -0
- ct/tools/code.py +1069 -0
- ct/tools/combination.py +397 -0
- ct/tools/compute.py +402 -0
- ct/tools/cro.py +413 -0
- ct/tools/data_api.py +2114 -0
- ct/tools/design.py +295 -0
- ct/tools/dna.py +575 -0
- ct/tools/experiment.py +604 -0
- ct/tools/expression.py +655 -0
- ct/tools/files.py +957 -0
- ct/tools/genomics.py +1387 -0
- ct/tools/http_client.py +146 -0
- ct/tools/imaging.py +319 -0
- ct/tools/intel.py +223 -0
- ct/tools/literature.py +743 -0
- ct/tools/network.py +422 -0
- ct/tools/notification.py +111 -0
- ct/tools/omics.py +3330 -0
- ct/tools/ops.py +1230 -0
- ct/tools/parity.py +649 -0
- ct/tools/pk.py +245 -0
- ct/tools/protein.py +678 -0
- ct/tools/regulatory.py +643 -0
- ct/tools/remote_data.py +179 -0
- ct/tools/report.py +181 -0
- ct/tools/repurposing.py +376 -0
- ct/tools/safety.py +1280 -0
- ct/tools/shell.py +178 -0
- ct/tools/singlecell.py +533 -0
- ct/tools/statistics.py +552 -0
- ct/tools/structure.py +882 -0
- ct/tools/target.py +901 -0
- ct/tools/translational.py +123 -0
- ct/tools/viability.py +218 -0
- ct/ui/__init__.py +0 -0
- ct/ui/markdown.py +31 -0
- ct/ui/status.py +258 -0
- ct/ui/suggestions.py +567 -0
- ct/ui/terminal.py +1456 -0
- 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
|
+
}
|