-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.py
476 lines (381 loc) · 22.3 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
import pandas as pd
from pathlib import Path
# Shiny imports
from shiny.express import input, render, ui
import shinyswatch
from shiny import reactive
# RDKit imports
from rdkit import Chem
from rdkit.Chem.Draw import rdMolDraw2D
from rdkit.Chem import rdDepictor
# Favicon generation
from favicon_utils import generate_favicon
favicon_data_uri = generate_favicon()
# Favicon generation
favicon_data_uri = generate_favicon()
# Sets Schrodinger's CoordGen algorithm for generating drawing coordinates.
rdDepictor.SetPreferCoordGen(True)
# Import CSV
abxData = pd.read_csv(Path(__file__).parent / "antibiotics_data.csv")
# Page level options
ui.page_opts(title="PenicillinX",
window_title="PenicillinX",
#fillable=True,
theme=shinyswatch.theme.superhero,
style="text-align: center;"
)
with ui.tags.head():
if favicon_data_uri:
ui.tags.link(rel="icon", href=favicon_data_uri, type="image/png")
ui.tags.link(rel="stylesheet",
href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700&display=swap")
ui.tags.link(rel="stylesheet", href="style.css")
#=================================================== Modal =============================================
welcomeMessage = ui.markdown("""
This is a development preview for PenicillinX (Penicillin *Cross* reactivity). This program is designed to demonstrate the similar molecular substructures on various beta-lactam antibiotics.
#### Use case
When a patient with a history of hypersensitivity to more than one beta-lactam antibiotic, there is an opportunity to examine shared molecular structures between them to determine the causative substructure.
This can aid specialist clinicians in selecting beta-lactam antibiotics that are less likely to result in hypersensitivity reactions for potential challenge.
#### Warning
PenicillinX is still under development. This is a preview for testing purposes. The results presented are currently not validated and should not be used to assist in **any** kind of clinical decision making.
PenicillinX is provided free under the terms of the [GPL v3.0 licence](https://github.com/liamlah/PenicillinX/blob/main/LICENSE)
By using this software you agree to the terms of the licence and agree <u>to not use any results output by this program to **any** extent in **any** clinical decision making</u>.
""")
welcome_modal = ui.modal(
welcomeMessage,
ui.div(
{"style": "text-align: center; gap: 10px; display: flex; justify-content: center;"},
ui.modal_button("Accept"),
ui.tags.a(
ui.input_action_button("decline", "Decline", class_="btn btn-default"),
href="https://github.com/liamlah/PenicillinX"
)
),
title=ui.div(
{"style": "display: flex; align-items: center; gap: 10px;"},
ui.img(src=favicon_data_uri, style="width: 48px; height: 48px;"),
"Welcome"
),
footer=None,
size='l',
easy_close=False,
fade=True,
align="left"
)
ui.modal_show(welcome_modal)
#================================================== SIDEBAR ==================================================
with (ui.sidebar(open="always")):
ui.input_selectize(
id="abx1",
label="Select Antibiotic 1:",
selected="",
choices=[""] + abxData["Antibiotic"].tolist(),
multiple=False,
options={"Placeholder": "CHECK12", "allowEmptyOption": True, "plugins":["clear_button"]})
ui.input_selectize(
id="abx2",
label="Select Antibiotic 2:",
selected="Select Antibiotic",
choices=[""] + abxData["Antibiotic"].tolist(),
options={"Placeholder": "Select Please", "allowEmptyOption": True, "plugins":["clear_button"]})
ui.input_selectize(
id="abx3",
label="Select Antibiotic 3:",
selected="Select Antibiotic",
choices=[""] + abxData["Antibiotic"].tolist(),
options={"Placeholder": "Select Please", "allowEmptyOption": True, "plugins":["clear_button"]})
ui.input_action_button("show", "Info")
@reactive.effect
@reactive.event(input.show)
def modalOpen():
ui.modal_show(welcome_modal)
# @render.ui ----delete if remains unused
# @reactive.event(input.Info)
# def show_modal():
# welcome_modal.show()
#
#
# ui.input_action_button("Info", "Show Info")
# with ui.tooltip(id="btn_tooltip", placement="bottom"):
# ui.input_action_button("btn", "Info")
# ui.HTML(
# "This application allows you to compare the chemical structures of various antibiotics to . "
# "Select up to three antibiotics from the dropdown. The application will then analyze the selected antibiotics and display any matching features they share. Eventually, this app will allow clinicians "
# "to identify the common structures behind a patient's antibiotic allergy, and allow them to safely prescribe alternatives. "
# "<strong>NOTE: App currently in development. This should absolutely not be used in any clinical setting.</strong> <a href='https://github.com/liamlah/penicillinX' target='_blank' style='color: #ffffff;'>More information can be found on GitHub</a>")
#
@reactive.effect # Ensures antibiotic can't be selected twice - remove during RDKIT debugging ********************************************
def update_dropdown_choices():
# Grabs the antibiotics selected in each dropdown
selected_abx1 = input.abx1()
selected_abx2 = input.abx2()
selected_abx3 = input.abx3()
# Get the full list of antibiotics
all_choices = abxData["Antibiotic"].tolist()
# Exclude selected antibiotics for each dropdown, ensures that current selection is not removed
choices_for_abx1 = (
[selected_abx1] + [abx for abx in all_choices if abx != selected_abx2 and abx != selected_abx3]
if selected_abx1 else
[""] + [abx for abx in all_choices if abx != selected_abx2 and abx != selected_abx3]
)
choices_for_abx2 = (
[selected_abx2] + [abx for abx in all_choices if abx != selected_abx1 and abx != selected_abx3]
if selected_abx2 else
[""] + [abx for abx in all_choices if abx != selected_abx1 and abx != selected_abx3]
)
choices_for_abx3 = (
[selected_abx3] + [abx for abx in all_choices if abx != selected_abx1 and abx != selected_abx2]
if selected_abx3 else
[""] + [abx for abx in all_choices if abx != selected_abx1 and abx != selected_abx2]
)
# Dynamically update the dropdown choices while maintaining the current selection
ui.update_selectize("abx1", choices=choices_for_abx1, selected=selected_abx1)
ui.update_selectize("abx2", choices=choices_for_abx2, selected=selected_abx2)
ui.update_selectize("abx3", choices=choices_for_abx3, selected=selected_abx3)
#================================================== MAIN PANEL ==================================================
with ui.tags.div(class_="main-content"):
with ui.layout_column_wrap(fixed_width=True, width="330px", class_="ubuntu-regular"):
with ui.card(fill=True, min_height="250px"):
@render.ui
def selectedlist1():
# Get the selected antibiotic from the input
selected_abx = input.abx1()
selected_abx2 = input.abx2()
selected_abx3 = input.abx3()
if not selected_abx:
return ui.HTML("<p>Please select an antibiotic.</p>")
# Get the SMILES string corresponding to the selected antibiotic
smiles = abxData.loc[abxData["Antibiotic"] == selected_abx, "SMILESFull"].values
if len(smiles) == 0:
return ui.HTML("<p>SMILES not found for the selected antibiotic.</p>")
# Turns the SMILES into a MOL with coordinates
mol = Chem.MolFromSmiles(smiles[0])
if not mol:
return ui.HTML("<p>Invalid SMILES format for the selected antibiotic.</p>")
# Beta-Lactam structure, not explicitly defined in antibiotics_data.csv
lactamRing = Chem.MolFromSmarts("O=[#6]-1-[#6]-[#6]-[#7]-1")
lactam_atoms = list(mol.GetSubstructMatch(lactamRing)) # Lactam atom indices
# RDKit commands to generate image
drawer = rdMolDraw2D.MolDraw2DSVG(300, 300) # Set canvas size
opts = drawer.drawOptions()
opts.setBackgroundColour((1, 1, 1, 0)) # Transparent background (RGBA)
# Makes the molecule white to match the dark theme
atom_palette = {atom.GetIdx(): (1.0, 1.0, 1.0) for atom in mol.GetAtoms()}
opts.setAtomPalette(atom_palette)
opts.fontFile = "www/fonts/Ubuntu-Regular.ttf"
opts.setLegendColour((1, 1, 1)) # White legend color
opts.legendFontSize = 20
# Highlight dictionaries
highlight_colors = {} # Atom highlight colors
highlight_atoms = [] # List of atoms to highlight
messages = [] # Collects the messages
# For blue Beta-Lactam ring
if lactam_atoms:
highlight_atoms.extend(lactam_atoms)
for atom_idx in lactam_atoms:
highlight_colors[atom_idx] = (0.4, 0.4, 1, 0.5) # Blue RGBA
messages.append(f"• {selected_abx} contains a <mark style='background-color:#454eb4; color: white;'>Beta-Lactam ring</mark>.")
else:
messages.append(f"• {selected_abx} Is not a Beta Lactam Antibiotic")
# 2. Substructure comparison with other abx
for other_abx in [selected_abx2, selected_abx3]:
if other_abx:
substructure_smiles = abxData.loc[
abxData["Antibiotic"] == other_abx,
["SMILESR1", "SMILESR2", "SMILESR3", "SMILESRing"]
]
for col_name, sub_smiles in substructure_smiles.iloc[0].items():
# Skip invalid entries where side chains don't exist
if pd.isna(sub_smiles):
continue
substructure = Chem.MolFromSmiles(sub_smiles)
if substructure:
matching_atoms = list(mol.GetSubstructMatch(substructure))
if matching_atoms:
highlight_atoms.extend(matching_atoms)
if col_name == "SMILESRing": # Specifically ring match
for atom_idx in matching_atoms:
highlight_colors[atom_idx] = (0.0, 0.8, 0.0, 0.5) # Green RGBA
messages.append(f"• {selected_abx} shares a similar <mark style='background-color:#08791c; color: white;'>core</mark> with {other_abx}.")
else: # R1, R2, or R3 match
for atom_idx in matching_atoms:
highlight_colors[atom_idx] = (0.9, 0.0, 0.0, 0.5) # Red RGBA
messages.append(f"• {selected_abx} shares a similar <mark style='background-color:#940e15; color: white;'>side chain</mark> with {other_abx}.")
# Puts the highlights on the molecule
drawer.DrawMolecule(
mol,
highlightAtoms=highlight_atoms,
highlightAtomColors=highlight_colors,
legend=selected_abx
)
drawer.FinishDrawing()
# Extract and clean up the SVG
svg = drawer.GetDrawingText()
# Combine the SVG and messages
message_html = "".join(f"<p>{msg}</p>" for msg in messages)
return ui.HTML(svg + message_html)
with ui.card(fill=True, min_height="250px"):
@render.ui
def selectedlist2():
# Get the selected antibiotic from the input
selected_abx = input.abx2()
selected_abx1 = input.abx1()
selected_abx3 = input.abx3()
if not selected_abx:
return ui.HTML("<p>""</p>") # No output if selection empty
# Get the SMILES string corresponding to the selected antibiotic
smiles = abxData.loc[abxData["Antibiotic"] == selected_abx, "SMILESFull"].values
if len(smiles) == 0:
return ui.HTML("<p>SMILES not found for the selected antibiotic.</p>")
# Turns the SMILES into a MOL with coordinates
mol = Chem.MolFromSmiles(smiles[0])
if not mol:
return ui.HTML("<p>Invalid SMILES format for the selected antibiotic.</p>")
# Beta-lactam structure, not explicitly defined in antibiotics_data.csv
lactamRing = Chem.MolFromSmarts("O=[#6]-1-[#6]-[#6]-[#7]-1")
lactam_atoms = list(mol.GetSubstructMatch(lactamRing)) # Lactam atom indices
# RDKit commands to generate image
drawer = rdMolDraw2D.MolDraw2DSVG(300, 300) # Set canvas size
opts = drawer.drawOptions()
opts.setBackgroundColour((1, 1, 1, 0)) # Transparent background (RGBA)
# Makes the molecule white to match the dark theme
atom_palette = {atom.GetIdx(): (1.0, 1.0, 1.0) for atom in mol.GetAtoms()}
opts.setAtomPalette(atom_palette)
opts.fontFile = "www/fonts/Ubuntu-Regular.ttf"
opts.setLegendColour((1, 1, 1)) # White legend color
opts.legendFontSize = 20
# Highlight dictionaries
highlight_colors = {} # Atom highlight colors
highlight_atoms = [] # List of atoms to highlight
messages = [] # Collects the messages
# For blue Beta-Lactam ring
if lactam_atoms:
highlight_atoms.extend(lactam_atoms)
for atom_idx in lactam_atoms:
highlight_colors[atom_idx] = (0.4, 0.4, 1, 0.5) # Blue RGBA
messages.append(f"• {selected_abx} contains a <mark style='background-color:#454eb4; color: white;'>Beta-Lactam ring</mark>.")
else:
messages.append(f"• {selected_abx} Is not a Beta Lactam Antibiotic")
# 2. Substructure comparison with other abx
for other_abx in [selected_abx1, selected_abx3]:
if other_abx:
substructure_smiles = abxData.loc[
abxData["Antibiotic"] == other_abx,
["SMILESR1", "SMILESR2", "SMILESR3", "SMILESRing"]
]
for col_name, sub_smiles in substructure_smiles.iloc[0].items():
# Skip invalid entries where side chains don't exist
if pd.isna(sub_smiles):
continue
substructure = Chem.MolFromSmiles(sub_smiles)
if substructure:
matching_atoms = list(mol.GetSubstructMatch(substructure))
if matching_atoms:
highlight_atoms.extend(matching_atoms)
if col_name == "SMILESRing": # Specifically ring match
for atom_idx in matching_atoms:
highlight_colors[atom_idx] = (0.0, 0.8, 0.0, 0.5) # Green RGBA
messages.append(f"• {selected_abx} shares a similar <mark style='background-color:#08791c; color: white;'>core</mark> with {other_abx}.")
else: # R1, R2, or R3 match
for atom_idx in matching_atoms:
highlight_colors[atom_idx] = (0.9, 0.0, 0.0, 0.5) # Red RGBA
messages.append(f"• {selected_abx} shares a similar <mark style='background-color:#940e15; color: white;'>side chain</mark> with {other_abx}.")
# Puts the highlights on the molecule
drawer.DrawMolecule(
mol,
highlightAtoms=highlight_atoms,
highlightAtomColors=highlight_colors,
legend=selected_abx
)
drawer.FinishDrawing()
# Extract and clean up the SVG
svg = drawer.GetDrawingText()
# Combine the SVG and messages
message_html = "".join(f"<p>{msg}</p>" for msg in messages)
return ui.HTML(svg + message_html)
with ui.card(fill=True, min_height="250px"):
@render.ui
def selectedlist3():
# Get the selected antibiotic from the input
selected_abx = input.abx3()
selected_abx1 = input.abx1()
selected_abx2 = input.abx2()
if not selected_abx:
return ui.HTML("<p>""</p>") # No output if selection empty
# Get the SMILES string corresponding to the selected antibiotic
smiles = abxData.loc[abxData["Antibiotic"] == selected_abx, "SMILESFull"].values
if len(smiles) == 0:
return ui.HTML("<p>SMILES not found for the selected antibiotic.</p>")
# Turns the SMILES into a MOL with coordinates
mol = Chem.MolFromSmiles(smiles[0])
if not mol:
return ui.HTML("<p>Invalid SMILES format for the selected antibiotic.</p>")
# Beta lactam structure, not explicitly defined in antibiotics_data.csv
lactamRing = Chem.MolFromSmarts("O=[#6]-1-[#6]-[#6]-[#7]-1")
lactam_atoms = list(mol.GetSubstructMatch(lactamRing)) # Lactam atom indices
# RDKit commands to generate image
drawer = rdMolDraw2D.MolDraw2DSVG(300, 300) # Set canvas size
opts = drawer.drawOptions()
opts.setBackgroundColour((1, 1, 1, 0)) # Transparent background (RGBA)
opts.fontFile= "www/fonts/Ubuntu-Regular.ttf"
# Makes the molecule white to match the dark theme
atom_palette = {atom.GetIdx(): (1.0, 1.0, 1.0) for atom in mol.GetAtoms()}
opts.setAtomPalette(atom_palette)
opts.setLegendColour((1, 1, 1)) # White legend color
opts.legendFontSize = 20
# Highlight dictionaries
highlight_colors = {} # Atom highlight colors
highlight_atoms = [] # List of atoms to highlight
messages = [] # Collects the messages
# For blue Beta-Lactam ring
if lactam_atoms:
highlight_atoms.extend(lactam_atoms)
for atom_idx in lactam_atoms:
highlight_colors[atom_idx] = (0.4, 0.4, 1, 0.5) # Blue RGBA
messages.append(f"• {selected_abx} contains a <mark style='background-color:#454eb4; color: white;'>Beta-Lactam ring</mark>.")
else:
messages.append(f"• {selected_abx} Is not a Beta Lactam Antibiotic")
# 2. Substructure comparison with other abx
for other_abx in [selected_abx1, selected_abx2]:
if other_abx:
substructure_smiles = abxData.loc[
abxData["Antibiotic"] == other_abx,
["SMILESR1", "SMILESR2", "SMILESR3", "SMILESRing"]
]
for col_name, sub_smiles in substructure_smiles.iloc[0].items():
# Skip invalid entries where side chains don't exist
if pd.isna(sub_smiles):
continue
substructure = Chem.MolFromSmiles(sub_smiles)
if substructure:
matching_atoms = list(mol.GetSubstructMatch(substructure))
if matching_atoms:
highlight_atoms.extend(matching_atoms)
if col_name == "SMILESRing": # Specifically ring match
for atom_idx in matching_atoms:
highlight_colors[atom_idx] = (0.0, 0.8, 0.0, 0.5) # Green RGBA
messages.append(f"• {selected_abx} shares a similar <mark style='background-color:#08791c; color: white;'>core</mark> with {other_abx}.")
else: # R1, R2, or R3 match
for atom_idx in matching_atoms:
highlight_colors[atom_idx] = (0.9, 0.0, 0.0, 0.5) # Red RGBA
messages.append(f"• {selected_abx} shares a similar <mark style='background-color:#940e15; color: white;'>side chain</mark> with {other_abx}.")
# Puts the highlights on the molecule
drawer.DrawMolecule(
mol,
highlightAtoms=highlight_atoms,
highlightAtomColors=highlight_colors,
legend=selected_abx
)
drawer.FinishDrawing()
# Extract and cleans up the SVG
svg = drawer.GetDrawingText()
# Combines SVG and text matches
message_html = "".join(f"<p>{msg}</p>" for msg in messages)
return ui.HTML(svg + message_html)
# todo
# Improve small screen and mobile layout: sidebar layout - on top in mobile
# New text box at bottom with summary of matches
# refactor 3 functions to one if possible
# Bugs to fix
# Fix one way matches. e.g ampicillin penicillinG
# Fix repeating match output for different molecules e.g amox + cefalex + ampicillin