liuganghuggingface commited on
Commit
991b396
Β·
verified Β·
1 Parent(s): a21b923

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +320 -115
app.py CHANGED
@@ -1,142 +1,347 @@
1
- # import spaces
2
  import gradio as gr
 
3
  import torch
4
- import torch.nn as nn
 
5
  import random
 
 
 
 
 
 
 
 
 
 
6
  from rdkit import Chem
7
  from rdkit.Chem import Draw
8
 
9
- from graph_decoder.diffusion_model import GraphDiT
 
10
 
 
 
 
11
 
12
- ATOM_SYMBOLS = ['C', 'N', 'O', 'H']
13
 
14
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
15
 
16
- path = 'model_labeled'
17
- model = GraphDiT(
18
- model_config_path=f"{path}/config.yaml",
19
- data_info_path=f"{path}/data.meta.json",
20
- model_dtype=torch.float32
21
- )
22
- model.to(device)
23
 
24
- def generate_random_smiles(length=10):
25
- return ''.join(random.choices(ATOM_SYMBOLS, k=length))
26
 
27
- # @spaces.GPU
28
- def generate_polymer(CH4, CO2, H2, N2, O2, guidance_scale):
29
- properties = torch.tensor([CH4, CO2, H2, N2, O2], dtype=torch.float32).unsqueeze(0)
30
-
31
- print('in generate_polymer')
 
 
32
  try:
33
- # Generate a random SMILES string (this is a placeholder)
34
- generated_molecule = generate_random_smiles()
35
- model.generate(properties, device)
36
-
37
- mol = Chem.MolFromSmiles(generated_molecule)
38
- if mol is not None:
39
- standardized_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
40
- img = Draw.MolToImage(mol)
41
- return standardized_smiles, img
42
  except Exception as e:
43
- print(f"Error in generation: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- return "Generation failed", None
 
 
 
 
 
 
 
46
 
47
- # Create the Gradio interface
48
- with gr.Blocks(title="Simplified Polymer Design") as iface:
49
- gr.Markdown("## Polymer Design with Random Neural Network")
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  with gr.Row():
52
- CH4_input = gr.Slider(0, 100, value=2.5, label="CHβ‚„ (Barrier)")
53
- CO2_input = gr.Slider(0, 100, value=15.4, label="COβ‚‚ (Barrier)")
54
- H2_input = gr.Slider(0, 100, value=21.0, label="Hβ‚‚ (Barrier)")
55
- N2_input = gr.Slider(0, 100, value=1.5, label="Nβ‚‚ (Barrier)")
56
- O2_input = gr.Slider(0, 100, value=2.8, label="Oβ‚‚ (Barrier)")
57
- guidance_scale = gr.Slider(1, 3, value=2, label="Guidance Scale")
58
 
59
- generate_btn = gr.Button("Generate Polymer")
 
 
 
 
 
60
 
61
  with gr.Row():
62
- result_smiles = gr.Textbox(label="Generated SMILES")
63
- result_image = gr.Image(label="Molecule Visualization", type="pil")
64
 
65
- generate_btn.click(
66
- generate_polymer,
67
- inputs=[CH4_input, CO2_input, H2_input, N2_input, O2_input, guidance_scale],
68
- outputs=[result_smiles, result_image]
69
- )
70
 
71
- if __name__ == "__main__":
72
- iface.launch()
73
-
74
- # import spaces
75
- # import gradio as gr
76
- # import torch
77
- # from rdkit import Chem
78
- # from rdkit.Chem import Draw
79
- # # from graph_decoder.diffusion_model import GraphDiT
80
-
81
- # # Load the model
82
- # def load_graph_decoder(path='model_labeled'):
83
- # model = GraphDiT(
84
- # model_config_path=f"{path}/config.yaml",
85
- # data_info_path=f"{path}/data.meta.json",
86
- # model_dtype=torch.float32,
87
- # )
88
- # model.init_model(path)
89
- # model.disable_grads()
90
- # return model
91
-
92
- # # model = load_graph_decoder()
93
- # # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
94
-
95
- # @spaces.GPU
96
- # def generate_polymer(CH4, CO2, H2, N2, O2, guidance_scale):
97
- # properties = [CH4, CO2, H2, N2, O2]
98
 
99
- # try:
100
- # model = load_graph_decoder()
101
- # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
102
- # model.to(device)
103
- # print('enter function')
104
- # generated_molecule, _ = model.generate(properties, device=device, guide_scale=guidance_scale)
105
-
106
- # if generated_molecule is not None:
107
- # mol = Chem.MolFromSmiles(generated_molecule)
108
- # if mol is not None:
109
- # standardized_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
110
- # img = Draw.MolToImage(mol)
111
- # return standardized_smiles, img
112
- # except Exception as e:
113
- # print(f"Error in generation: {e}")
114
 
115
- # return "Generation failed", None
 
 
 
 
116
 
117
- # # Create the Gradio interface
118
- # with gr.Blocks(title="Simplified Polymer Design") as iface:
119
- # gr.Markdown("## Polymer Design with GraphDiT")
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- # with gr.Row():
122
- # CH4_input = gr.Slider(0, 100, value=2.5, label="CHβ‚„ (Barrier)")
123
- # CO2_input = gr.Slider(0, 100, value=15.4, label="COβ‚‚ (Barrier)")
124
- # H2_input = gr.Slider(0, 100, value=21.0, label="Hβ‚‚ (Barrier)")
125
- # N2_input = gr.Slider(0, 100, value=1.5, label="Nβ‚‚ (Barrier)")
126
- # O2_input = gr.Slider(0, 100, value=2.8, label="Oβ‚‚ (Barrier)")
127
- # guidance_scale = gr.Slider(1, 3, value=2, label="Guidance Scale")
128
-
129
- # generate_btn = gr.Button("Generate Polymer")
130
-
131
- # with gr.Row():
132
- # result_smiles = gr.Textbox(label="Generated SMILES")
133
- # result_image = gr.Image(label="Molecule Visualization", type="pil")
134
-
135
- # generate_btn.click(
136
- # generate_polymer,
137
- # inputs=[CH4_input, CO2_input, H2_input, N2_input, O2_input, guidance_scale],
138
- # outputs=[result_smiles, result_image]
139
- # )
140
-
141
- # if __name__ == "__main__":
142
- # iface.launch()
 
1
+ import spaces
2
  import gradio as gr
3
+
4
  import torch
5
+ import numpy as np
6
+ import pandas as pd
7
  import random
8
+ import io
9
+ import imageio
10
+ import os
11
+ import tempfile
12
+ import atexit
13
+ import glob
14
+ import csv
15
+ from datetime import datetime
16
+ import json
17
+
18
  from rdkit import Chem
19
  from rdkit.Chem import Draw
20
 
21
+ from evaluator import Evaluator
22
+ from loader import load_graph_decoder
23
 
24
+ # Load the CSV data
25
+ known_labels = pd.read_csv('data/known_labels.csv')
26
+ knwon_smiles = pd.read_csv('data/known_polymers.csv')
27
 
28
+ all_properties = ['CH4', 'CO2', 'H2', 'N2', 'O2']
29
 
30
+ # Initialize evaluators
31
+ evaluators = {prop: Evaluator(f'evaluators/{prop}.joblib', prop) for prop in all_properties}
32
 
33
+ # Get min and max values for each property
34
+ property_ranges = {prop: (known_labels[prop].min(), known_labels[prop].max()) for prop in all_properties}
 
 
 
 
 
35
 
36
+ # Create a temporary directory for GIFs
37
+ temp_dir = tempfile.mkdtemp(prefix="polymer_gifs_")
38
 
39
+ def cleanup_temp_files():
40
+ """Clean up temporary GIF files on exit."""
41
+ for file in glob.glob(os.path.join(temp_dir, "*.gif")):
42
+ try:
43
+ os.remove(file)
44
+ except Exception as e:
45
+ print(f"Error deleting {file}: {e}")
46
  try:
47
+ os.rmdir(temp_dir)
 
 
 
 
 
 
 
 
48
  except Exception as e:
49
+ print(f"Error deleting temporary directory {temp_dir}: {e}")
50
+
51
+ # Register the cleanup function to be called on exit
52
+ atexit.register(cleanup_temp_files)
53
+
54
+ def random_properties():
55
+ return known_labels[all_properties].sample(1).values.tolist()[0]
56
+
57
+ def load_model(model_choice):
58
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
59
+ model = load_graph_decoder(path=model_choice)
60
+ return (model, device)
61
+
62
+ # Create a flagged folder if it doesn't exist
63
+ flagged_folder = "flagged"
64
+ os.makedirs(flagged_folder, exist_ok=True)
65
+
66
+ def save_interesting_log(smiles, properties, suggested_properties):
67
+ """Save interesting polymer data to a CSV file."""
68
+ log_file = os.path.join(flagged_folder, "log.csv")
69
+ file_exists = os.path.isfile(log_file)
70
+
71
+ with open(log_file, 'a', newline='') as csvfile:
72
+ fieldnames = ['timestamp', 'smiles'] + all_properties + [f'suggested_{prop}' for prop in all_properties]
73
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
74
+
75
+ if not file_exists:
76
+ writer.writeheader()
77
+
78
+ log_data = {
79
+ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
80
+ 'smiles': smiles,
81
+ **{prop: value for prop, value in zip(all_properties, properties)},
82
+ **{f'suggested_{prop}': value for prop, value in suggested_properties.items()}
83
+ }
84
+ writer.writerow(log_data)
85
+
86
+ @spaces.GPU
87
+ def generate_graph(CH4, CO2, H2, N2, O2, guidance_scale, num_nodes, repeating_time, model_state, num_chain_steps, fps):
88
+ model, device = model_state
89
+
90
+ properties = [CH4, CO2, H2, N2, O2]
91
 
92
+ def is_nan_like(x):
93
+ return x == 0 or x == '' or (isinstance(x, float) and np.isnan(x))
94
+
95
+ properties = [None if is_nan_like(prop) else prop for prop in properties]
96
+
97
+ nan_message = "The following gas properties were treated as NaN: "
98
+ nan_gases = [gas for gas, prop in zip(all_properties, properties) if prop is None]
99
+ nan_message += ", ".join(nan_gases) if nan_gases else "None"
100
 
101
+ num_nodes = None if num_nodes == 0 else num_nodes
 
 
102
 
103
+ for _ in range(repeating_time):
104
+ # try:
105
+ model.to(device)
106
+ generated_molecule, img_list = model.generate(properties, device=device, guide_scale=guidance_scale, num_nodes=num_nodes, number_chain_steps=num_chain_steps)
107
+
108
+ # Create GIF if img_list is available
109
+ gif_path = None
110
+ if img_list and len(img_list) > 0:
111
+ imgs = [np.array(pil_img) for pil_img in img_list]
112
+ imgs.extend([imgs[-1]] * 10)
113
+ gif_path = os.path.join(temp_dir, f"polymer_gen_{random.randint(0, 999999)}.gif")
114
+ imageio.mimsave(gif_path, imgs, format='GIF', fps=fps, loop=0)
115
+
116
+ if generated_molecule is not None:
117
+ mol = Chem.MolFromSmiles(generated_molecule)
118
+ if mol is not None:
119
+ standardized_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
120
+ is_novel = standardized_smiles not in knwon_smiles['SMILES'].values
121
+ novelty_status = "Novel (Not in Labeled Set)" if is_novel else "Not Novel (Exists in Labeled Set)"
122
+ img = Draw.MolToImage(mol)
123
+
124
+ # Evaluate the generated molecule
125
+ suggested_properties = {}
126
+ for prop, evaluator in evaluators.items():
127
+ suggested_properties[prop] = evaluator([standardized_smiles])[0]
128
+
129
+ suggested_properties_text = "\n".join([f"**Suggested {prop}:** {value:.2f}" for prop, value in suggested_properties.items()])
130
+
131
+ return (
132
+ f"**Generated polymer SMILES:** `{standardized_smiles}`\n\n"
133
+ f"**{nan_message}**\n\n"
134
+ f"**{novelty_status}**\n\n"
135
+ f"**Suggested Properties:**\n{suggested_properties_text}",
136
+ img,
137
+ gif_path,
138
+ properties, # Add this
139
+ suggested_properties # Add this
140
+ )
141
+ else:
142
+ return (
143
+ f"**Generation failed:** Could not generate a valid molecule.\n\n**{nan_message}**",
144
+ None,
145
+ gif_path,
146
+ properties,
147
+ None,
148
+ )
149
+ # except Exception as e:
150
+ # print(f"Error in generation: {e}")
151
+ # continue
152
+
153
+ return f"**Generation failed:** Could not generate a valid molecule after {repeating_time} attempts.\n\n**{nan_message}**", None, None
154
+
155
+ def set_random_properties():
156
+ return random_properties()
157
+
158
+ # Create a mapping of internal names to display names
159
+ model_name_mapping = {
160
+ "model_all": "Graph DiT (trained on labeled + unlabeled)",
161
+ "model_labeled": "Graph DiT (trained on labeled)"
162
+ }
163
+
164
+ def numpy_to_python(obj):
165
+ if isinstance(obj, np.integer):
166
+ return int(obj)
167
+ elif isinstance(obj, np.floating):
168
+ return float(obj)
169
+ elif isinstance(obj, np.ndarray):
170
+ return obj.tolist()
171
+ elif isinstance(obj, list):
172
+ return [numpy_to_python(item) for item in obj]
173
+ elif isinstance(obj, dict):
174
+ return {k: numpy_to_python(v) for k, v in obj.items()}
175
+ else:
176
+ return obj
177
+
178
+ def on_generate(CH4, CO2, H2, N2, O2, guidance_scale, num_nodes, repeating_time, model_state, num_chain_steps, fps):
179
+ result = generate_graph(CH4, CO2, H2, N2, O2, guidance_scale, num_nodes, repeating_time, model_state, num_chain_steps, fps)
180
+ # Check if the generation was successful
181
+ if result[0].startswith("**Generated polymer SMILES:**"):
182
+ smiles = result[0].split("**Generated polymer SMILES:** `")[1].split("`")[0]
183
+ properties = json.dumps(numpy_to_python(result[3]))
184
+ suggested_properties = json.dumps(numpy_to_python(result[4]))
185
+ # Return the result with an enabled feedback button
186
+ return [*result[:3], smiles, properties, suggested_properties, gr.Button(interactive=True)]
187
+ else:
188
+ # Return the result with a disabled feedback button
189
+ return [*result[:3], "", "[]", "[]", gr.Button(interactive=False)]
190
+
191
+ def process_feedback(checkbox_value, smiles, properties, suggested_properties):
192
+ if checkbox_value:
193
+ # Check if properties and suggested_properties are already Python objects
194
+ if isinstance(properties, str):
195
+ properties = json.loads(properties)
196
+ if isinstance(suggested_properties, str):
197
+ suggested_properties = json.loads(suggested_properties)
198
+
199
+ save_interesting_log(smiles, properties, suggested_properties)
200
+ return gr.Textbox(value="Thank you for your feedback! This polymer has been saved to our interesting polymers log.", visible=True)
201
+ else:
202
+ return gr.Textbox(value="Thank you for your feedback!", visible=True)
203
+
204
+ # ADD THIS FUNCTION
205
+ def reset_feedback_button():
206
+ return gr.Button(interactive=False)
207
+
208
+ # Create the Gradio interface using Blocks
209
+ with gr.Blocks(title="Polymer Design with GraphDiT") as iface:
210
+ # Navigation Bar
211
+ with gr.Row(elem_id="navbar"):
212
+ gr.Markdown("""
213
+ <div style="text-align: center;">
214
+ <h1>πŸ”—πŸ”¬ Polymer Design with GraphDiT</h1>
215
+ <div style="display: flex; gap: 20px; justify-content: center; align-items: center; margin-top: 10px;">
216
+ <a href="https://github.com/liugangcode/Graph-DiT" target="_blank" style="display: flex; align-items: center; gap: 5px; text-decoration: none; color: inherit;">
217
+ <img src="https://img.icons8.com/ios-glyphs/30/000000/github.png" alt="GitHub" />
218
+ <span>View Code</span>
219
+ </a>
220
+ <a href="https://arxiv.org/abs/2401.13858" target="_blank" style="text-decoration: none; color: inherit;">
221
+ πŸ“„ View Paper
222
+ </a>
223
+ </div>
224
+ </div>
225
+ """)
226
+
227
+ # Main Description
228
+ gr.Markdown("""
229
+ ## Introduction
230
+
231
+ Input the desired gas barrier properties for CHβ‚„, COβ‚‚, Hβ‚‚, Nβ‚‚, and Oβ‚‚ to generate novel polymer structures. The results are visualized as molecular graphs and represented by SMILES strings if they are successfully generated. Note: Gas barrier values set to 0 will be treated as `NaN` (unconditionally). If the generation fails, please retry or increase the number of repetition attempts.
232
+ """)
233
+
234
+ # Model Selection
235
+ model_choice = gr.Radio(
236
+ choices=list(model_name_mapping.values()),
237
+ label="Model Zoo",
238
+ # value="Graph DiT (trained on labeled + unlabeled)"
239
+ value="Graph DiT (trained on labeled)"
240
+ )
241
+
242
+ # Model Description Accordion
243
+ with gr.Accordion("πŸ” Model Description", open=False):
244
+ gr.Markdown("""
245
+ ### GraphDiT: Graph Diffusion Transformer
246
+
247
+ GraphDiT is a graph diffusion model designed for targeted molecular generation. It employs a conditional diffusion process to iteratively refine molecular structures based on user-specified properties.
248
+
249
+ We have collected a labeled polymer database for gas permeability from [Membrane Database](https://research.csiro.au/virtualscreening/membrane-database-polymer-gas-separation-membranes/). Additionally, we utilize unlabeled polymer structures from [PolyInfo](https://polymer.nims.go.jp/).
250
+
251
+ The gas permeability ranges from 0 to over ten thousand, with only hundreds of labeled data points, making this task particularly challenging.
252
+
253
+ We are actively working on improving the model. We welcome any feedback regarding model usage or suggestions for improvement.
254
+
255
+ #### Currently, we have two variants of Graph DiT:
256
+ - **Graph DiT (trained on labeled + unlabeled)**: This model uses both labeled and unlabeled data for training, potentially leading to more diverse/novel polymer generation.
257
+ - **Graph DiT (trained on labeled)**: This model is trained exclusively on labeled data, which may result in higher validity but potentially less diverse/novel outputs.
258
+ """)
259
+
260
+ # Citation Accordion
261
+ with gr.Accordion("πŸ“„ Citation", open=False):
262
+ gr.Markdown("""
263
+ If you use this model or interface useful, please cite the following paper:
264
+ ```bibtex
265
+ @article{graphdit2024,
266
+ title={Graph Diffusion Transformers for Multi-Conditional Molecular Generation},
267
+ author={Liu, Gang and Xu, Jiaxin and Luo, Tengfei and Jiang, Meng},
268
+ journal={NeurIPS},
269
+ year={2024},
270
+ }
271
+ ```
272
+ """)
273
+
274
+ model_state = gr.State(lambda: load_model("model_labeled"))
275
+
276
  with gr.Row():
277
+ CH4_input = gr.Slider(0, property_ranges['CH4'][1], value=2.5, label=f"CHβ‚„ (Barrier) [0-{property_ranges['CH4'][1]:.1f}]")
278
+ CO2_input = gr.Slider(0, property_ranges['CO2'][1], value=15.4, label=f"COβ‚‚ (Barrier) [0-{property_ranges['CO2'][1]:.1f}]")
279
+ H2_input = gr.Slider(0, property_ranges['H2'][1], value=21.0, label=f"Hβ‚‚ (Barrier) [0-{property_ranges['H2'][1]:.1f}]")
280
+ N2_input = gr.Slider(0, property_ranges['N2'][1], value=1.5, label=f"Nβ‚‚ (Barrier) [0-{property_ranges['N2'][1]:.1f}]")
281
+ O2_input = gr.Slider(0, property_ranges['O2'][1], value=2.8, label=f"Oβ‚‚ (Barrier) [0-{property_ranges['O2'][1]:.1f}]")
 
282
 
283
+ with gr.Row():
284
+ guidance_scale = gr.Slider(1, 3, value=2, label="Guidance Scale from Properties")
285
+ num_nodes = gr.Slider(0, 50, step=1, value=0, label="Number of Nodes (0 for Random, Larger Graphs Take More Time)")
286
+ repeating_time = gr.Slider(1, 10, step=1, value=3, label="Repetition Until Success")
287
+ num_chain_steps = gr.Slider(0, 499, step=1, value=50, label="Number of Diffusion Steps to Visualize (Larger Numbers Take More Time)")
288
+ fps = gr.Slider(0.25, 10, step=0.25, value=5, label="Frames Per Second")
289
 
290
  with gr.Row():
291
+ random_btn = gr.Button("πŸ”€ Randomize Properties (from Labeled Data)")
292
+ generate_btn = gr.Button("πŸš€ Generate Polymer")
293
 
294
+ with gr.Row():
295
+ result_text = gr.Textbox(label="πŸ“ Generation Result")
296
+ result_image = gr.Image(label="Final Molecule Visualization", type="pil")
297
+ result_gif = gr.Image(label="Generation Process Visualization", type="filepath", format="gif")
 
298
 
299
+ with gr.Row() as feedback_row:
300
+ feedback_btn = gr.Button("🌟 I think this polymer is interesting!", visible=True, interactive=False)
301
+ feedback_result = gr.Textbox(label="Feedback Result", visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ # Add model switching functionality
304
+ def switch_model(choice):
305
+ # Convert display name back to internal name
306
+ internal_name = next(key for key, value in model_name_mapping.items() if value == choice)
307
+ return load_model(internal_name)
308
+
309
+ model_choice.change(switch_model, inputs=[model_choice], outputs=[model_state])
310
+
311
+ # Hidden components to store generation data
312
+ hidden_smiles = gr.Textbox(visible=False)
313
+ hidden_properties = gr.JSON(visible=False)
314
+ hidden_suggested_properties = gr.JSON(visible=False)
 
 
 
315
 
316
+ # Set up event handlers
317
+ random_btn.click(
318
+ set_random_properties,
319
+ outputs=[CH4_input, CO2_input, H2_input, N2_input, O2_input]
320
+ )
321
 
322
+ generate_btn.click(
323
+ on_generate,
324
+ inputs=[CH4_input, CO2_input, H2_input, N2_input, O2_input, guidance_scale, num_nodes, repeating_time, model_state, num_chain_steps, fps],
325
+ outputs=[result_text, result_image, result_gif, hidden_smiles, hidden_properties, hidden_suggested_properties, feedback_btn]
326
+ )
327
+
328
+ feedback_btn.click(
329
+ process_feedback,
330
+ inputs=[gr.Checkbox(value=True, visible=False), hidden_smiles, hidden_properties, hidden_suggested_properties],
331
+ outputs=[feedback_result]
332
+ ).then(
333
+ lambda: gr.Button(interactive=False),
334
+ outputs=[feedback_btn]
335
+ )
336
 
337
+ CH4_input.change(reset_feedback_button, outputs=[feedback_btn])
338
+ CO2_input.change(reset_feedback_button, outputs=[feedback_btn])
339
+ H2_input.change(reset_feedback_button, outputs=[feedback_btn])
340
+ N2_input.change(reset_feedback_button, outputs=[feedback_btn])
341
+ O2_input.change(reset_feedback_button, outputs=[feedback_btn])
342
+ random_btn.click(reset_feedback_button, outputs=[feedback_btn])
343
+
344
+ # Launch the interface
345
+ if __name__ == "__main__":
346
+ # iface.launch(share=True)
347
+ iface.launch(share=False)