Vaishnav14220 commited on
Commit
2385d69
Β·
1 Parent(s): e46b082

Add auto-fetch thermodynamic data for search queries and selected reactions, plus animation options for plots

Browse files
Files changed (1) hide show
  1. app.py +262 -50
app.py CHANGED
@@ -250,20 +250,34 @@ def _summaries_to_dropdown(results) -> List[tuple[str, str]]:
250
  return choices
251
 
252
 
253
- def perform_search(query, decomposition_only, category_raw, units_value):
254
  if not query.strip():
255
- return [], "⚠️ Enter a search query.", gr.update(choices=[], value=None, interactive=False), []
256
 
257
- # Create a simple filter for reactants containing the query
258
- filter_obj = SearchFilter(
 
 
 
 
259
  boolean=None,
260
  left_parenthesis="",
261
  field=FieldName.reactants,
262
  relation=Relation.contains,
263
- value=query.strip(),
264
  right_parenthesis="",
265
- )
266
- filters = [filter_obj]
 
 
 
 
 
 
 
 
 
 
267
 
268
  category_raw = category_raw or str(Category.any.value)
269
  units_value = (units_value or "").strip() or None
@@ -278,11 +292,29 @@ def perform_search(query, decomposition_only, category_raw, units_value):
278
  try:
279
  results = client.search(request)
280
  except Exception as exc: # pragma: no cover - network/parsing issues
281
- return [], f"🚨 Search failed: {exc}", gr.update(choices=[], value=None, interactive=False), []
282
 
283
  table_data = _summaries_to_table(results)
284
  dropdown_choices = _summaries_to_dropdown(results)
285
- status = f"βœ… Found {len(results)} matching reactions." if results else "No records matched this query."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  dropdown_update = gr.update(
287
  choices=dropdown_choices,
288
  value=None,
@@ -293,7 +325,13 @@ def perform_search(query, decomposition_only, category_raw, units_value):
293
  {"record_count": summary.record_count, "reaction": summary.reaction, "detail_url": summary.detail_url}
294
  for summary in results
295
  ]
296
- return table_data, status, dropdown_update, state_payload
 
 
 
 
 
 
297
 
298
 
299
  def _format_detail_markdown(detail: ReactionDetail, detail_url: str) -> str:
@@ -494,30 +532,146 @@ def render_reaction_svg(reaction_text: str):
494
  return svg, status
495
 
496
 
497
- def fetch_detail(selected_url: str, manual_url: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  detail_url = (manual_url or "").strip() or (selected_url or "").strip()
499
  if not detail_url:
500
- return "ℹ️ Select a reaction above or paste a detail URL.", [], None, ""
501
 
502
  try:
503
  detail = client.fetch_reaction_detail(detail_url)
504
  except Exception as exc: # pragma: no cover - network/parsing issues
505
- return f"🚨 Could not load detail: {exc}", [], None, ""
506
 
507
  markdown = _format_detail_markdown(detail, detail_url)
508
  table = _datasets_to_table(detail)
509
  if not table:
510
  markdown += "\n\n_No kinetics datasets were returned for this reaction._"
511
- return markdown, table, None, ""
512
 
513
  plot_fig = _build_dataset_plot(detail)
514
-
515
  # Try to render the reaction title as SVG
516
  reaction_svg = ""
517
  if detail.title:
518
  title = detail.title.strip()
519
  smiles_attempt = None
520
-
521
  # Try different reaction format conversions
522
  if " β†’ " in title:
523
  # Format: "A + B β†’ C"
@@ -536,13 +690,33 @@ def fetch_detail(selected_url: str, manual_url: str):
536
  reactants = parts[0].replace(" + ", ".").strip()
537
  products = parts[1].replace(" + ", ".").strip()
538
  smiles_attempt = f"{reactants}>>{products}"
539
-
540
  if smiles_attempt:
541
  svg = _render_smiles_to_svg(smiles_attempt)
542
  if svg:
543
  reaction_svg = svg
544
-
545
- return markdown, table, plot_fig, reaction_svg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
 
548
  def _parse_points(text: str) -> Tuple[List[float], List[float], List[str]]:
@@ -680,9 +854,17 @@ def build_interface() -> gr.Blocks:
680
  results_state = gr.State([])
681
 
682
  with gr.Tabs():
683
- # Tab 1: Search (Original functionality)
684
  with gr.TabItem("Search"):
685
- simple_search = gr.Textbox(label="Search Query", placeholder="Enter reactants, products, or keywords (e.g., CH4 + O2)")
 
 
 
 
 
 
 
 
686
 
687
  with gr.Row():
688
  decomp = gr.Checkbox(label="Only decomposition reactions", value=False)
@@ -692,8 +874,9 @@ def build_interface() -> gr.Blocks:
692
  placeholder="Leave blank to use NIST account defaults",
693
  )
694
 
695
- search_button = gr.Button("Search NIST", variant="primary")
696
  search_status = gr.Markdown()
 
697
  result_table = gr.Dataframe(
698
  headers=["#", "Records", "Reaction", "Detail URL"],
699
  datatype=["number", "number", "str", "str"],
@@ -701,39 +884,68 @@ def build_interface() -> gr.Blocks:
701
  wrap=True,
702
  )
703
 
704
- # Tab 2: Reaction Detail (Original functionality)
 
 
 
 
 
705
  with gr.TabItem("Reaction Detail"):
706
- selection = gr.Dropdown(
707
- label="Select a reaction from the latest search",
708
- choices=[],
709
- interactive=False,
710
- )
711
- manual_url = gr.Textbox(
712
- label="Or paste a NIST detail URL",
713
- placeholder="https://kinetics.nist.gov/kinetics/ReactionSearch?....",
714
- )
715
- detail_button = gr.Button("Fetch Reaction Detail")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
 
717
  # Reaction metadata and details
718
  detail_markdown = gr.Markdown()
719
 
720
- # Kinetics data table
721
- dataset_table = gr.Dataframe(
722
- headers=["Section", "Squib", "Temp [K]", "A", "n", "Ea [J/mole]", "k(298 K)", "Order", "Squib URL"],
723
- datatype=["str"] * 9,
724
- interactive=False,
725
- wrap=True,
726
- )
 
 
 
 
 
 
 
 
727
 
728
  # Reaction SVG visualization
729
  with gr.Row():
730
  gr.Markdown("### Reaction Structure")
731
  reaction_svg = gr.HTML()
732
 
733
- # Arrhenius plot
734
- with gr.Row():
735
- gr.Markdown("### Arrhenius Plot")
736
- reaction_plot = gr.Plot()
 
 
737
 
738
  # Tab 3: Reaction SVG (Original functionality)
739
  with gr.TabItem("Reaction SVG"):
@@ -849,21 +1061,21 @@ def build_interface() -> gr.Blocks:
849
  # Event handlers for original functionality
850
  search_button.click(
851
  fn=perform_search,
852
- inputs=[simple_search, decomp, category, units],
853
- outputs=[result_table, search_status, selection, results_state],
854
  )
855
 
856
  detail_button.click(
857
  fn=fetch_detail,
858
- inputs=[selection, manual_url],
859
- outputs=[detail_markdown, dataset_table, reaction_plot, reaction_svg],
860
  )
861
 
862
  # Auto-render SVG when selection changes
863
  selection.change(
864
  fn=fetch_detail,
865
- inputs=[selection, manual_url],
866
- outputs=[detail_markdown, dataset_table, reaction_plot, reaction_svg],
867
  )
868
 
869
  # Examples (global or per-tab)
 
250
  return choices
251
 
252
 
253
+ def perform_search(query, decomposition_only, category_raw, units_value, auto_search_thermo=True):
254
  if not query.strip():
255
+ return [], "⚠️ Enter a search query.", gr.update(choices=[], value=None, interactive=False), [], {}
256
 
257
+ # Create multiple filters for comprehensive search
258
+ query_term = query.strip()
259
+ filters = []
260
+
261
+ # Search in reactants
262
+ filters.append(SearchFilter(
263
  boolean=None,
264
  left_parenthesis="",
265
  field=FieldName.reactants,
266
  relation=Relation.contains,
267
+ value=query_term,
268
  right_parenthesis="",
269
+ ))
270
+
271
+ # Also search in products if it's a longer query
272
+ if len(query_term) > 2:
273
+ filters.append(SearchFilter(
274
+ boolean=LogicalOperator.or_,
275
+ left_parenthesis="",
276
+ field=FieldName.products,
277
+ relation=Relation.contains,
278
+ value=query_term,
279
+ right_parenthesis="",
280
+ ))
281
 
282
  category_raw = category_raw or str(Category.any.value)
283
  units_value = (units_value or "").strip() or None
 
292
  try:
293
  results = client.search(request)
294
  except Exception as exc: # pragma: no cover - network/parsing issues
295
+ return [], f"🚨 Search failed: {exc}", gr.update(choices=[], value=None, interactive=False), [], {}
296
 
297
  table_data = _summaries_to_table(results)
298
  dropdown_choices = _summaries_to_dropdown(results)
299
+
300
+ # Enhanced status with compound information
301
+ status_parts = [f"βœ… Found {len(results)} matching reactions"]
302
+ if results:
303
+ status_parts.append(f" for query: '{query_term}'")
304
+
305
+ # Extract unique compounds from results for auto-suggestions
306
+ all_compounds = set()
307
+ for result in results[:10]: # Check first 10 results
308
+ compounds = _extract_compounds_from_reaction(result.reaction)
309
+ all_compounds.update(compounds)
310
+
311
+ if all_compounds:
312
+ status_parts.append(f" | Compounds detected: {', '.join(list(all_compounds)[:5])}")
313
+ if len(all_compounds) > 5:
314
+ status_parts.append(f" +{len(all_compounds) - 5} more")
315
+
316
+ status = "".join(status_parts)
317
+
318
  dropdown_update = gr.update(
319
  choices=dropdown_choices,
320
  value=None,
 
325
  {"record_count": summary.record_count, "reaction": summary.reaction, "detail_url": summary.detail_url}
326
  for summary in results
327
  ]
328
+
329
+ # Auto-fetch thermodynamic data for the searched compound
330
+ search_thermo_data = {}
331
+ if auto_search_thermo and query_term:
332
+ search_thermo_data = _fetch_compound_thermo_data([query_term])
333
+
334
+ return table_data, status, dropdown_update, state_payload, search_thermo_data
335
 
336
 
337
  def _format_detail_markdown(detail: ReactionDetail, detail_url: str) -> str:
 
532
  return svg, status
533
 
534
 
535
+ def _extract_compounds_from_reaction(reaction_text: str) -> List[str]:
536
+ """Extract compound names/identifiers from reaction text."""
537
+ compounds = []
538
+
539
+ # Clean the reaction text
540
+ reaction_text = reaction_text.strip()
541
+
542
+ # Handle different reaction formats
543
+ if " β†’ " in reaction_text:
544
+ parts = reaction_text.split(" β†’ ")
545
+ elif "->" in reaction_text:
546
+ parts = reaction_text.split("->")
547
+ elif " ↔ " in reaction_text:
548
+ parts = reaction_text.split(" ↔ ")
549
+ else:
550
+ return compounds
551
+
552
+ # Process each part (reactants and products)
553
+ for part in parts:
554
+ # Split by " + " to get individual compounds
555
+ individual_compounds = [c.strip() for c in part.split(" + ") if c.strip()]
556
+
557
+ # Try to identify chemical formulas or names
558
+ for compound in individual_compounds:
559
+ # Remove coefficients (numbers at start)
560
+ compound = re.sub(r'^\d+\s*', '', compound)
561
+ if compound and len(compound) > 1: # Avoid single letters
562
+ compounds.append(compound)
563
+
564
+ return list(set(compounds)) # Remove duplicates
565
+
566
+
567
+ def _fetch_compound_thermo_data(compounds: List[str]) -> dict:
568
+ """Fetch thermodynamic data for a list of compounds from NIST databases."""
569
+ thermo_data = {}
570
+
571
+ for compound in compounds[:5]: # Limit to 5 compounds to avoid overwhelming
572
+ compound_data = {}
573
+
574
+ # Try different databases
575
+ databases_to_try = [
576
+ "NIST Organic Thermochemistry Archive",
577
+ "Organometallic Thermochemistry Database",
578
+ "Gas-Phase Ion Thermochemistry"
579
+ ]
580
+
581
+ for db_name in databases_to_try:
582
+ try:
583
+ md_content, df, plot = fetch_specific_db(db_name, compound)
584
+ if df is not None and not df.empty:
585
+ compound_data[db_name] = {
586
+ 'markdown': md_content,
587
+ 'dataframe': df,
588
+ 'plot': plot
589
+ }
590
+ break # Stop at first successful fetch
591
+ except Exception:
592
+ continue
593
+
594
+ if compound_data:
595
+ thermo_data[compound] = compound_data
596
+
597
+ return thermo_data
598
+
599
+
600
+ def _create_animated_plot(fig: go.Figure, animate: bool = False) -> go.Figure:
601
+ """Add animation capabilities to plots if requested."""
602
+ if not animate or fig is None:
603
+ return fig
604
+
605
+ # Add animation frames for temperature sweep
606
+ if hasattr(fig, 'data') and len(fig.data) > 0:
607
+ trace = fig.data[0]
608
+
609
+ # Create animation frames
610
+ frames = []
611
+ temps = list(range(300, 2500, 100)) # Temperature range
612
+
613
+ for temp in temps:
614
+ frame_data = []
615
+ for trace in fig.data:
616
+ if hasattr(trace, 'x') and hasattr(trace, 'y'):
617
+ # Simulate temperature-dependent behavior
618
+ animated_trace = go.Scatter(
619
+ x=trace.x,
620
+ y=trace.y,
621
+ mode=trace.mode,
622
+ name=trace.name,
623
+ line=dict(color=trace.line.color if hasattr(trace, 'line') else 'blue')
624
+ )
625
+ frame_data.append(animated_trace)
626
+
627
+ frames.append(go.Frame(data=frame_data, name=str(temp)))
628
+
629
+ fig.frames = frames
630
+
631
+ # Add animation controls
632
+ fig.update_layout(
633
+ updatemenus=[dict(
634
+ type="buttons",
635
+ buttons=[dict(
636
+ label="Play",
637
+ method="animate",
638
+ args=[None, dict(mode="immediate", frame=dict(duration=500, redraw=True), fromcurrent=True)]
639
+ )]
640
+ )],
641
+ sliders=[dict(
642
+ active=0,
643
+ steps=[dict(method="animate", args=[[f.name], dict(mode="immediate", frame=dict(duration=300, redraw=False), transition=dict(duration=0))], label=f.name) for f in frames],
644
+ currentvalue={"prefix": "Temperature: "},
645
+ )]
646
+ )
647
+
648
+ return fig
649
+
650
+
651
+ def fetch_detail(selected_url: str, manual_url: str, auto_fetch_thermo: bool = True, animate_plots: bool = False):
652
  detail_url = (manual_url or "").strip() or (selected_url or "").strip()
653
  if not detail_url:
654
+ return "ℹ️ Select a reaction above or paste a detail URL.", [], None, "", {}, ""
655
 
656
  try:
657
  detail = client.fetch_reaction_detail(detail_url)
658
  except Exception as exc: # pragma: no cover - network/parsing issues
659
+ return f"🚨 Could not load detail: {exc}", [], None, "", {}, ""
660
 
661
  markdown = _format_detail_markdown(detail, detail_url)
662
  table = _datasets_to_table(detail)
663
  if not table:
664
  markdown += "\n\n_No kinetics datasets were returned for this reaction._"
665
+ return markdown, table, None, "", {}, ""
666
 
667
  plot_fig = _build_dataset_plot(detail)
668
+
669
  # Try to render the reaction title as SVG
670
  reaction_svg = ""
671
  if detail.title:
672
  title = detail.title.strip()
673
  smiles_attempt = None
674
+
675
  # Try different reaction format conversions
676
  if " β†’ " in title:
677
  # Format: "A + B β†’ C"
 
690
  reactants = parts[0].replace(" + ", ".").strip()
691
  products = parts[1].replace(" + ", ".").strip()
692
  smiles_attempt = f"{reactants}>>{products}"
693
+
694
  if smiles_attempt:
695
  svg = _render_smiles_to_svg(smiles_attempt)
696
  if svg:
697
  reaction_svg = svg
698
+
699
+ # Auto-fetch thermodynamic data for compounds in the reaction
700
+ thermo_data = {}
701
+ thermo_summary = ""
702
+
703
+ if auto_fetch_thermo and detail.title:
704
+ compounds = _extract_compounds_from_reaction(detail.title)
705
+ if compounds:
706
+ thermo_data = _fetch_compound_thermo_data(compounds)
707
+ if thermo_data:
708
+ thermo_summary = f"### πŸ”¬ Auto-fetched Thermodynamic Data\nFound data for {len(thermo_data)} compound(s): {', '.join(thermo_data.keys())}\n\n"
709
+ for compound, data in thermo_data.items():
710
+ thermo_summary += f"**{compound}:**\n"
711
+ for db_name, db_data in data.items():
712
+ thermo_summary += f"- {db_name}: Data available\n"
713
+ thermo_summary += "\n"
714
+
715
+ # Add animation to plots if requested
716
+ if animate_plots:
717
+ plot_fig = _create_animated_plot(plot_fig, True)
718
+
719
+ return markdown, table, plot_fig, reaction_svg, thermo_data, thermo_summary
720
 
721
 
722
  def _parse_points(text: str) -> Tuple[List[float], List[float], List[str]]:
 
854
  results_state = gr.State([])
855
 
856
  with gr.Tabs():
857
+ # Tab 1: Search (Enhanced functionality)
858
  with gr.TabItem("Search"):
859
+ with gr.Row():
860
+ with gr.Column(scale=2):
861
+ simple_search = gr.Textbox(label="Search Query", placeholder="Enter reactants, products, or compound (e.g., CH4 + O2, CH3, benzene)")
862
+ with gr.Column(scale=1):
863
+ auto_search_thermo = gr.Checkbox(
864
+ label="πŸ”¬ Auto-fetch thermo data",
865
+ value=True,
866
+ info="Automatically fetch thermodynamic data for searched compounds"
867
+ )
868
 
869
  with gr.Row():
870
  decomp = gr.Checkbox(label="Only decomposition reactions", value=False)
 
874
  placeholder="Leave blank to use NIST account defaults",
875
  )
876
 
877
+ search_button = gr.Button("πŸ” Search NIST", variant="primary")
878
  search_status = gr.Markdown()
879
+
880
  result_table = gr.Dataframe(
881
  headers=["#", "Records", "Reaction", "Detail URL"],
882
  datatype=["number", "number", "str", "str"],
 
884
  wrap=True,
885
  )
886
 
887
+ # Search results thermodynamic data
888
+ search_thermo_accordion = gr.Accordion(label="πŸ”¬ Search Query Thermodynamic Data", open=False)
889
+ with search_thermo_accordion:
890
+ search_thermo_display = gr.JSON(label="Thermodynamic Data for Search Query")
891
+
892
+ # Tab 2: Reaction Detail (Enhanced functionality)
893
  with gr.TabItem("Reaction Detail"):
894
+ with gr.Row():
895
+ with gr.Column(scale=2):
896
+ selection = gr.Dropdown(
897
+ label="Select a reaction from the latest search",
898
+ choices=[],
899
+ interactive=False,
900
+ )
901
+ manual_url = gr.Textbox(
902
+ label="Or paste a NIST detail URL",
903
+ placeholder="https://kinetics.nist.gov/kinetics/ReactionSearch?....",
904
+ )
905
+ with gr.Column(scale=1):
906
+ auto_fetch_thermo = gr.Checkbox(
907
+ label="πŸ”¬ Auto-fetch thermodynamics",
908
+ value=True,
909
+ info="Automatically fetch thermodynamic data for compounds in the reaction"
910
+ )
911
+ animate_plots = gr.Checkbox(
912
+ label="🎬 Animate plots",
913
+ value=False,
914
+ info="Add animation controls to plots"
915
+ )
916
+
917
+ detail_button = gr.Button("Fetch Reaction Detail", variant="primary")
918
 
919
  # Reaction metadata and details
920
  detail_markdown = gr.Markdown()
921
 
922
+ with gr.Row():
923
+ # Kinetics data table
924
+ with gr.Column():
925
+ gr.Markdown("### Kinetics Data")
926
+ dataset_table = gr.Dataframe(
927
+ headers=["Section", "Squib", "Temp [K]", "A", "n", "Ea [J/mole]", "k(298 K)", "Order", "Squib URL"],
928
+ datatype=["str"] * 9,
929
+ interactive=False,
930
+ wrap=True,
931
+ )
932
+
933
+ # Arrhenius plot
934
+ with gr.Column():
935
+ gr.Markdown("### Arrhenius Plot")
936
+ reaction_plot = gr.Plot()
937
 
938
  # Reaction SVG visualization
939
  with gr.Row():
940
  gr.Markdown("### Reaction Structure")
941
  reaction_svg = gr.HTML()
942
 
943
+ # Auto-fetched thermodynamic data
944
+ thermo_summary = gr.Markdown()
945
+ thermo_accordion = gr.Accordion(label="πŸ”¬ Thermodynamic Data", open=False)
946
+
947
+ with thermo_accordion:
948
+ thermo_data_display = gr.JSON(label="Raw Thermodynamic Data")
949
 
950
  # Tab 3: Reaction SVG (Original functionality)
951
  with gr.TabItem("Reaction SVG"):
 
1061
  # Event handlers for original functionality
1062
  search_button.click(
1063
  fn=perform_search,
1064
+ inputs=[simple_search, decomp, category, units, auto_search_thermo],
1065
+ outputs=[result_table, search_status, selection, results_state, search_thermo_display],
1066
  )
1067
 
1068
  detail_button.click(
1069
  fn=fetch_detail,
1070
+ inputs=[selection, manual_url, auto_fetch_thermo, animate_plots],
1071
+ outputs=[detail_markdown, dataset_table, reaction_plot, reaction_svg, thermo_data_display, thermo_summary],
1072
  )
1073
 
1074
  # Auto-render SVG when selection changes
1075
  selection.change(
1076
  fn=fetch_detail,
1077
+ inputs=[selection, manual_url, auto_fetch_thermo, animate_plots],
1078
+ outputs=[detail_markdown, dataset_table, reaction_plot, reaction_svg, thermo_data_display, thermo_summary],
1079
  )
1080
 
1081
  # Examples (global or per-tab)