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/design.py ADDED
@@ -0,0 +1,295 @@
1
+ """
2
+ Design tools: compound modification suggestions, med-chem optimization.
3
+
4
+ Provides AI-guided medicinal chemistry recommendations using RDKit-based
5
+ property calculations, bioisosteric replacement rules, and Lipinski/Veber
6
+ scoring.
7
+ """
8
+
9
+ from ct.tools import registry
10
+
11
+
12
+ # Common medicinal chemistry transformations: (SMARTS_pattern, SMARTS_replacement, description)
13
+ # Each entry: (name, smarts_from, list_of_replacements)
14
+ _MEDCHEM_TRANSFORMS = [
15
+ # Halogen walks
16
+ ("F_to_Cl", "[cH0:1][F:2]", "[cH0:1][Cl]", "F->Cl: increase lipophilicity and steric bulk"),
17
+ ("F_to_H", "[cH0:1][F:2]", "[cH:1]", "F->H: remove halogen, reduce MW"),
18
+ ("Cl_to_F", "[cH0:1][Cl:2]", "[cH0:1][F]", "Cl->F: reduce lipophilicity, metabolic block"),
19
+ # Alkyl modifications
20
+ ("Me_to_Et", "[CH3:1]([#6:2])", "[CH2:1]([#6:2])C", "Me->Et: increase steric bulk, explore SAR"),
21
+ ("OH_to_OMe", "[c:1][OH:2]", "[c:1]OC", "OH->OMe: cap phenol, block glucuronidation"),
22
+ ("OMe_to_OH", "[c:1][O:2][CH3]", "[c:1][OH]", "OMe->OH: add H-bond donor, improve solubility"),
23
+ # N-modifications
24
+ ("NH_to_NMe", "[NH2:1]", "[NH:1]C", "NH2->NHMe: reduce basicity, improve metabolic stability"),
25
+ ("NMe_to_NH", "[NH:1]([CH3])", "[NH2:1]", "NHMe->NH2: simplify, add H-bond donor"),
26
+ # Ring modifications
27
+ ("phenyl_to_pyridine", "[c:1]1[c:2][c:3][c:4][c:5][cH:6]1", "[c:1]1[c:2][c:3][c:4][c:5][n:6]1",
28
+ "phenyl->pyridyl: improve solubility, add H-bond acceptor"),
29
+ ]
30
+
31
+
32
+ def _compute_properties(mol) -> dict:
33
+ """Compute drug-relevant molecular properties."""
34
+ from rdkit.Chem import Descriptors, rdMolDescriptors
35
+
36
+ return {
37
+ "mw": round(Descriptors.MolWt(mol), 1),
38
+ "logp": round(Descriptors.MolLogP(mol), 2),
39
+ "hbd": Descriptors.NumHDonors(mol),
40
+ "hba": Descriptors.NumHAcceptors(mol),
41
+ "tpsa": round(Descriptors.TPSA(mol), 1),
42
+ "rotatable_bonds": Descriptors.NumRotatableBonds(mol),
43
+ "rings": Descriptors.RingCount(mol),
44
+ "aromatic_rings": Descriptors.NumAromaticRings(mol),
45
+ "fsp3": round(rdMolDescriptors.CalcFractionCSP3(mol), 3),
46
+ "heavy_atoms": mol.GetNumHeavyAtoms(),
47
+ }
48
+
49
+
50
+ def _lipinski_violations(props: dict) -> int:
51
+ """Count Lipinski Rule-of-5 violations."""
52
+ return sum([
53
+ props["mw"] > 500,
54
+ props["logp"] > 5,
55
+ props["hbd"] > 5,
56
+ props["hba"] > 10,
57
+ ])
58
+
59
+
60
+ def _veber_violations(props: dict) -> int:
61
+ """Count Veber oral bioavailability rule violations."""
62
+ return sum([
63
+ props["tpsa"] > 140,
64
+ props["rotatable_bonds"] > 10,
65
+ ])
66
+
67
+
68
+ def _score_for_objective(parent_props: dict, child_props: dict,
69
+ objective: str) -> float:
70
+ """Score a modification relative to the parent, for a given objective.
71
+
72
+ Returns a float where higher is better (range roughly -1 to +1).
73
+ """
74
+ if objective == "potency":
75
+ # Favour: lower MW, moderate LogP (2-4), more H-bond interactions
76
+ logp_score = 1.0 - abs(child_props["logp"] - 3.0) / 5.0
77
+ mw_score = (parent_props["mw"] - child_props["mw"]) / 100.0
78
+ return round(logp_score * 0.5 + mw_score * 0.3 + 0.2, 3)
79
+
80
+ elif objective == "selectivity":
81
+ # Favour: more specific interactions (more HBD/HBA), higher TPSA
82
+ hb_delta = (child_props["hbd"] + child_props["hba"]) - (parent_props["hbd"] + parent_props["hba"])
83
+ tpsa_delta = (child_props["tpsa"] - parent_props["tpsa"]) / 50.0
84
+ return round(hb_delta * 0.3 + tpsa_delta * 0.4 + 0.3, 3)
85
+
86
+ elif objective == "admet":
87
+ # Favour: fewer Lipinski/Veber violations, moderate LogP
88
+ lip = _lipinski_violations(child_props)
89
+ veb = _veber_violations(child_props)
90
+ penalty = lip * 0.25 + veb * 0.25
91
+ logp_bonus = 1.0 - abs(child_props["logp"] - 2.5) / 5.0
92
+ return round(max(0, 1.0 - penalty) * 0.6 + logp_bonus * 0.4, 3)
93
+
94
+ elif objective == "solubility":
95
+ # Favour: lower LogP, higher TPSA, higher Fsp3
96
+ logp_score = max(0, (parent_props["logp"] - child_props["logp"]) / 3.0)
97
+ tpsa_score = max(0, (child_props["tpsa"] - parent_props["tpsa"]) / 40.0)
98
+ fsp3_score = max(0, (child_props["fsp3"] - parent_props["fsp3"]))
99
+ return round(logp_score * 0.4 + tpsa_score * 0.3 + fsp3_score * 0.3, 3)
100
+
101
+ elif objective == "metabolic_stability":
102
+ # Favour: fewer metabolic soft spots (reduce ArOH, add F-blocks)
103
+ logp_score = 1.0 - abs(child_props["logp"] - 2.0) / 5.0
104
+ mw_penalty = max(0, (child_props["mw"] - 500) / 200.0)
105
+ rotbond_penalty = max(0, (child_props["rotatable_bonds"] - 7) / 5.0)
106
+ return round(logp_score * 0.4 - mw_penalty * 0.3 - rotbond_penalty * 0.3, 3)
107
+
108
+ # Default: balanced score
109
+ return 0.5
110
+
111
+
112
+ @registry.register(
113
+ name="design.suggest_modifications",
114
+ description="Suggest medicinal chemistry modifications to improve a compound's properties",
115
+ category="design",
116
+ parameters={
117
+ "smiles": "Input compound SMILES string",
118
+ "objective": "Optimization goal: potency, selectivity, admet, solubility, metabolic_stability (default: potency)",
119
+ "n_suggestions": "Number of suggestions to return (default 5)",
120
+ },
121
+ usage_guide=(
122
+ "You have a hit or lead compound and want to generate ideas for "
123
+ "structural modifications to improve potency, selectivity, ADMET, "
124
+ "solubility, or metabolic stability. Returns modified SMILES with "
125
+ "property comparisons and medicinal chemistry rationale."
126
+ ),
127
+ )
128
+ def suggest_modifications(smiles: str, objective: str = "potency",
129
+ n_suggestions: int = 5, **kwargs) -> dict:
130
+ """Suggest medicinal chemistry modifications for a compound.
131
+
132
+ Applies bioisosteric replacements and common med-chem transforms using
133
+ RDKit reaction SMARTS, then scores each modification against the
134
+ specified objective.
135
+ """
136
+ try:
137
+ from rdkit import Chem
138
+ from rdkit.Chem import AllChem, Descriptors
139
+ except ImportError:
140
+ return {"error": "RDKit is required for compound modification suggestions (pip install rdkit)", "summary": "RDKit is required for compound modification suggestions (pip install rdkit)"}
141
+ valid_objectives = ("potency", "selectivity", "admet", "solubility", "metabolic_stability")
142
+ if objective not in valid_objectives:
143
+ return {"error": f"Unknown objective '{objective}'. Choose from: {', '.join(valid_objectives)}", "summary": f"Unknown objective '{objective}'. Choose from: {', '.join(valid_objectives)}"}
144
+ mol = Chem.MolFromSmiles(smiles)
145
+ if mol is None:
146
+ return {"error": f"Invalid SMILES: {smiles}", "summary": f"Could not parse SMILES: {smiles}"}
147
+
148
+ parent_props = _compute_properties(mol)
149
+ parent_violations = _lipinski_violations(parent_props)
150
+ canonical = Chem.MolToSmiles(mol)
151
+
152
+ suggestions = []
153
+ seen_smiles = {canonical}
154
+
155
+ # Apply each transformation
156
+ for name, smarts_from, smarts_to, description in _MEDCHEM_TRANSFORMS:
157
+ try:
158
+ rxn_smarts = f"[{smarts_from}]>>[{smarts_to}]"
159
+ # Use ReplaceSubstructs instead of reaction for reliability
160
+ pattern = Chem.MolFromSmarts(smarts_from)
161
+ if pattern is None:
162
+ continue
163
+ if not mol.HasSubstructMatch(pattern):
164
+ continue
165
+
166
+ # Attempt reaction-based transform
167
+ rxn = AllChem.ReactionFromSmarts(f"{smarts_from}>>{smarts_to}")
168
+ if rxn is None:
169
+ continue
170
+
171
+ products = rxn.RunReactants((mol,))
172
+ for product_set in products:
173
+ for product in product_set:
174
+ try:
175
+ Chem.SanitizeMol(product)
176
+ product_smi = Chem.MolToSmiles(product)
177
+ if product_smi in seen_smiles:
178
+ continue
179
+ seen_smiles.add(product_smi)
180
+
181
+ child_props = _compute_properties(product)
182
+ child_violations = _lipinski_violations(child_props)
183
+
184
+ score = _score_for_objective(parent_props, child_props, objective)
185
+
186
+ # Property deltas
187
+ deltas = {
188
+ "mw": round(child_props["mw"] - parent_props["mw"], 1),
189
+ "logp": round(child_props["logp"] - parent_props["logp"], 2),
190
+ "hbd": child_props["hbd"] - parent_props["hbd"],
191
+ "hba": child_props["hba"] - parent_props["hba"],
192
+ "tpsa": round(child_props["tpsa"] - parent_props["tpsa"], 1),
193
+ }
194
+
195
+ suggestions.append({
196
+ "smiles": product_smi,
197
+ "transform": name,
198
+ "rationale": description,
199
+ "score": score,
200
+ "properties": child_props,
201
+ "property_deltas": deltas,
202
+ "lipinski_violations": child_violations,
203
+ })
204
+ except Exception:
205
+ continue
206
+ except Exception:
207
+ continue
208
+
209
+ # Also add simple functional group additions if we have few suggestions
210
+ _simple_additions = [
211
+ ("add_F", "F", "Add fluorine — metabolic blocker, minimal size increase"),
212
+ ("add_OH", "O", "Add hydroxyl — improve solubility, add H-bond donor"),
213
+ ("add_NH2", "N", "Add amine — add H-bond donor, potential salt formation"),
214
+ ("add_Me", "C", "Add methyl — explore steric effects, fill hydrophobic pocket"),
215
+ ]
216
+
217
+ for add_name, add_atom, add_desc in _simple_additions:
218
+ if len(suggestions) >= n_suggestions * 3:
219
+ break
220
+ try:
221
+ # Find an aromatic carbon that could accept a substituent
222
+ pattern = Chem.MolFromSmarts("[cH]")
223
+ if pattern and mol.HasSubstructMatch(pattern):
224
+ matches = mol.GetSubstructMatches(pattern)
225
+ if matches:
226
+ from rdkit.Chem import RWMol
227
+ for match in matches[:1]: # just first match
228
+ rw = RWMol(mol)
229
+ new_idx = rw.AddAtom(Chem.Atom(add_atom))
230
+ rw.AddBond(match[0], new_idx, Chem.BondType.SINGLE)
231
+ try:
232
+ Chem.SanitizeMol(rw)
233
+ new_smi = Chem.MolToSmiles(rw)
234
+ if new_smi in seen_smiles:
235
+ continue
236
+ seen_smiles.add(new_smi)
237
+
238
+ new_mol = Chem.MolFromSmiles(new_smi)
239
+ if new_mol is None:
240
+ continue
241
+
242
+ child_props = _compute_properties(new_mol)
243
+ child_violations = _lipinski_violations(child_props)
244
+ score = _score_for_objective(parent_props, child_props, objective)
245
+
246
+ deltas = {
247
+ "mw": round(child_props["mw"] - parent_props["mw"], 1),
248
+ "logp": round(child_props["logp"] - parent_props["logp"], 2),
249
+ "hbd": child_props["hbd"] - parent_props["hbd"],
250
+ "hba": child_props["hba"] - parent_props["hba"],
251
+ "tpsa": round(child_props["tpsa"] - parent_props["tpsa"], 1),
252
+ }
253
+
254
+ suggestions.append({
255
+ "smiles": new_smi,
256
+ "transform": add_name,
257
+ "rationale": add_desc,
258
+ "score": score,
259
+ "properties": child_props,
260
+ "property_deltas": deltas,
261
+ "lipinski_violations": child_violations,
262
+ })
263
+ except Exception:
264
+ continue
265
+ except Exception:
266
+ continue
267
+
268
+ # Sort by score descending, take top n
269
+ suggestions.sort(key=lambda s: s["score"], reverse=True)
270
+ top = suggestions[:n_suggestions]
271
+
272
+ if not top:
273
+ return {
274
+ "summary": f"No modifications found for {smiles} (no applicable transforms matched)",
275
+ "parent_smiles": canonical,
276
+ "parent_properties": parent_props,
277
+ "suggestions": [],
278
+ }
279
+
280
+ # Build summary
281
+ best = top[0]
282
+ logp_delta = best["property_deltas"]["logp"]
283
+ delta_str = f"LogP {logp_delta:+.2f}" if logp_delta != 0 else "similar LogP"
284
+
285
+ return {
286
+ "summary": (
287
+ f"{len(top)} modification(s) suggested for {canonical[:40]} "
288
+ f"(objective={objective}): top suggestion {best['transform']} ({delta_str})"
289
+ ),
290
+ "parent_smiles": canonical,
291
+ "parent_properties": parent_props,
292
+ "parent_lipinski_violations": parent_violations,
293
+ "objective": objective,
294
+ "suggestions": top,
295
+ }