.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/app_geophysics_workflow.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_app_geophysics_workflow.py: PyHydroGeophysX Streamlit Web Application Natural-language interface for geophysical workflows. Usage: streamlit run app_geophysics_workflow.py .. GENERATED FROM PYTHON SOURCE LINES 7-2410 .. code-block:: default import os import sys import tempfile from pathlib import Path from typing import Dict, List, Optional import streamlit as st import streamlit.components.v1 as components # Add parent directory to path so local package can be imported when run from examples/ CURRENT_DIR = Path(__file__).parent PARENT_DIR = CURRENT_DIR.parent if str(PARENT_DIR) not in sys.path: sys.path.insert(0, str(PARENT_DIR)) IMPORT_ERROR = "" try: from PyHydroGeophysX.agents import BaseAgent, ContextInputAgent AGENTS_AVAILABLE = True except ImportError as e: AGENTS_AVAILABLE = False IMPORT_ERROR = str(e) BaseAgent = None ContextInputAgent = None # Check for pygimli availability try: import pygimli PYGIMLI_AVAILABLE = True except ImportError: PYGIMLI_AVAILABLE = False st.set_page_config( page_title="PyHydroGeophysX - Geophysical Workflows", page_icon="PHGX", layout="wide", initial_sidebar_state="expanded", ) CUSTOM_CSS = """ :root { --phgx-blue: #0f4c75; --phgx-green: #2d9c5b; --phgx-gray: #f5f7fb; --phgx-dark: #1b262c; --phgx-accent: #3d6cb9; } section.main > div { padding-top: 1rem; } .phgx-header { font-size: 2.4rem; font-weight: 700; color: var(--phgx-dark); letter-spacing: 0.04em; } .phgx-subtitle-main { background: linear-gradient(90deg, var(--phgx-blue) 0%, var(--phgx-accent) 50%, var(--phgx-green) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-size: 1.2rem; font-weight: 700; letter-spacing: 0.02em; margin-top: 0.1rem; margin-bottom: 0.3rem; } .phgx-author-line { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.8rem; flex-wrap: wrap; } .phgx-version-badge { display: inline-flex; align-items: center; padding: 0.2rem 0.6rem; border-radius: 0.4rem; background: linear-gradient(135deg, #e8f4f8 0%, #f0f7ff 100%); border: 1px solid #c8dce8; font-size: 0.8rem; font-weight: 600; color: var(--phgx-blue); } .phgx-author-text { color: #5a6a7a; font-size: 0.9rem; font-weight: 500; letter-spacing: 0.01em; } .phgx-author-text a { color: var(--phgx-accent); text-decoration: none; border-bottom: 1px dotted var(--phgx-accent); } .phgx-author-text a:hover { color: var(--phgx-blue); border-bottom-style: solid; } .phgx-subtitle { color: #2f3b4a; font-size: 1.35rem; font-weight: 700; letter-spacing: 0.01em; margin-top: -0.05rem; margin-bottom: 0.75rem; } .phgx-card { padding: 1.1rem 1.2rem; border-radius: 0.6rem; background: var(--phgx-gray); border: 1px solid #e1e5ec; } .phgx-pill { display: inline-block; padding: 0.15rem 0.55rem; border-radius: 999px; background: #e2f0ff; color: #174ea6; font-weight: 600; font-size: 0.85rem; margin-right: 0.35rem; margin-bottom: 0.3rem; } .phgx-mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.9rem; } .phgx-support-card { background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border: 1px solid #cbd5e1; border-radius: 0.8rem; padding: 1.2rem 1.5rem; margin-top: 1rem; text-align: center; } .phgx-support-title { font-size: 1.1rem; font-weight: 600; color: #334155; margin-bottom: 0.5rem; } .phgx-support-text { font-size: 0.9rem; color: #64748b; margin-bottom: 0.8rem; line-height: 1.5; } .phgx-venmo-btn { display: inline-block; background: linear-gradient(135deg, #008cff 0%, #0066cc 100%); color: white !important; padding: 0.5rem 1.2rem; border-radius: 2rem; font-weight: 600; font-size: 0.9rem; text-decoration: none; margin: 0.3rem; transition: all 0.2s ease; } .phgx-venmo-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 140, 255, 0.3); } .phgx-email-link { color: #0f4c75; text-decoration: none; font-weight: 500; border-bottom: 1px dotted #0f4c75; } .phgx-email-link:hover { color: #3d6cb9; border-bottom-style: solid; } .phgx-free-badge { display: inline-block; background: #dcfce7; color: #166534; padding: 0.2rem 0.6rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 600; margin-bottom: 0.5rem; } /* Make tabs larger and more prominent */ .stTabs [data-baseweb="tab-list"] { gap: 8px; background-color: #f0f4f8; padding: 0.5rem; border-radius: 0.6rem; } .stTabs [data-baseweb="tab"] { height: 60px; padding: 0 24px; font-size: 1.1rem; font-weight: 600; color: var(--phgx-dark); background-color: white; border-radius: 0.5rem; border: 1px solid #e1e5ec; white-space: pre-wrap; } .stTabs [data-baseweb="tab"]:hover { background-color: #e8f4f8; border-color: var(--phgx-accent); } .stTabs [aria-selected="true"] { background: linear-gradient(135deg, var(--phgx-blue) 0%, var(--phgx-accent) 100%) !important; color: white !important; border-color: var(--phgx-blue) !important; } .stTabs [data-baseweb="tab-panel"] { padding-top: 1.5rem; } """ st.markdown(f"", unsafe_allow_html=True) EXAMPLE_REQUESTS: Dict[str, str] = { "Standard ERT": """Run a standard ERT inversion using the DAS-1 instrument. Data file: 20171105_1418.Data Electrode file: electrodes.dat Petrophysics: rho_sat=541, porosity=0.37, n=1.24 Regularization lambda: 15""", "Time-Lapse ERT": """Run a time-lapse ERT inversion on four E4D files: - 2022-03-26_0030.ohm (baseline) - 2022-04-26_0030.ohm - 2022-05-26_0030.ohm - 2022-06-26_0030.ohm Temporal regularization: 10 Include climate data for Mt. Snodgrass at 38.92584N, -106.97998W""", "Data Fusion": """Perform structure-constrained inversion using seismic + ERT. Seismic: srtfieldline2.dat with velocity threshold 1000 m/s ERT: fielddataline2.dat Petrophysics: - Regolith: rho_sat 50-250, n 1.3-2.2, porosity 0.25-0.50 - Fractured bedrock: rho_sat 165-350, n 2.0-2.2, porosity 0.2-0.3 Monte Carlo realizations: 100""", "Seismic Refraction": """Run a seismic refraction tomography (SRT) inversion. Data file: synthetic_seismic_data_long.dat Regularization lambda: 50 Vertical weight: 0.2 Velocity constraints: 500-5000 m/s Parametric depth: 60 m Extract velocity interfaces at: 1200 m/s (regolith-bedrock), 5000 m/s (fractured-fresh)""", "TDEM Inversion": """Run a TDEM (Time-Domain Electromagnetic) inversion. Data file: tdem_synthetic_data.txt Loop source radius: 10 meters Number of inversion layers: 20 Use sparse regularization (IRLS): yes Maximum iterations: 50""" } DATA_LINKS: Dict[str, str] = { "Example data folder (all)": "https://github.com/geohang/PyHydroGeophysX/tree/main/examples/data", } # Example-specific data links organized by workflow type EXAMPLE_DATA_LINKS: Dict[str, Dict[str, str]] = { "ERT Example (Ex1)": { "description": "Standard ERT inversion with DAS-1 instrument data from Snowy Range, Wyoming", "notebook": "https://github.com/geohang/PyHydroGeophysX/blob/main/examples/Ex_Unified_Workflow_ex1.ipynb", "ert_data": "https://github.com/geohang/PyHydroGeophysX/tree/main/examples/data/ERT/DAS", "files": ["20171105_1418.Data", "electrodes.dat"], }, "Time-Lapse Example (Ex2)": { "description": "Time-lapse ERT monitoring with climate integration from Mt. Snodgrass, Colorado", "notebook": "https://github.com/geohang/PyHydroGeophysX/blob/main/examples/Ex_Unified_Workflow_ex2.ipynb", "ert_data": "https://github.com/geohang/PyHydroGeophysX/tree/main/examples/data/ERT/E4D", "files": ["2022-03-26_0030.ohm", "2022-04-26_0030.ohm", "2022-05-26_0030.ohm", "2022-06-26_0030.ohm"], }, "Data Fusion Example (Ex3)": { "description": "Multi-method integration: Seismic + ERT with structure constraints", "notebook": "https://github.com/geohang/PyHydroGeophysX/blob/main/examples/Ex_Unified_Workflow_ex3.ipynb", "seismic_data": "https://github.com/geohang/PyHydroGeophysX/tree/main/examples/data/Seismic", "ert_data": "https://github.com/geohang/PyHydroGeophysX/tree/main/examples/data/ERT/Bert", "files": ["srtfieldline2.dat (seismic)", "fielddataline2.dat (ERT)"], }, } AUTHOR_LINK = "https://sites.google.com/view/hangchen" STANDARD_ERT_TUTORIAL_IMAGES = [ ("Step 1", "step1.png"), ("Step 2", "Step2.png"), ("Final result 1", "Final_result_1.png"), ("Final result 2", "Final_result_2.png"), ("Resistivity model", "resistivity_model (5).png"), ("Water content", "water_content.png"), ] def init_session_state() -> None: defaults = { "context_agent": None, "workflow_result": None, "api_key": "", "llm_model": "", "llm_provider": "openai", "output_dir": "results/streamlit_workflow", "user_request": "", "upload_dir": None, "workflow_config": None, } for key, value in defaults.items(): if key not in st.session_state: st.session_state[key] = value def render_header() -> None: st.markdown('
PyHydroGeophysX Workflows
', unsafe_allow_html=True) st.markdown( '
AQUAH: Autonomous Query-driven Understanding Agent for Hydrogeophysics
', unsafe_allow_html=True, ) st.markdown( '
' 'v1.0' 'Developed by Hang Chen ยท University of Iowa' 'โ–ถ Video Tutorial' '
', unsafe_allow_html=True, ) st.markdown( '
Unified Workflow
' '
ERT
' '
TDEM
' '
Seismic
' '
Data Fusion
' '
Climate
', unsafe_allow_html=True, ) def render_example_buttons() -> None: st.subheader("Example workflows") cols = st.columns(len(EXAMPLE_REQUESTS)) for idx, (label, text) in enumerate(EXAMPLE_REQUESTS.items()): if cols[idx].button(label): st.session_state.user_request = text st.rerun() st.caption("Click any example to auto-fill the request box.") def render_tutorial_tab() -> None: st.subheader("Tutorial") # Video Tutorial st.markdown("### Video Tutorial") st.video("https://www.youtube.com/watch?v=d4lgs_hQqDo") st.markdown("---") st.markdown( """
Run a workflow in six steps
  1. Initialize the context agent in the sidebar (provider, model, API key).
  2. Pick sample files from GitHub or upload your own measurements.
  3. Describe the workflow in plain language with file names and parameters.
  4. Use the example buttons to auto-fill, then edit the request to match your data.
  5. Click "Run workflow" and watch the progress and execution plan.
  6. Download the report files and review the interpretation summary.
""", unsafe_allow_html=True, ) # API Key Setup Section st.markdown("### How to Get an API Key") with st.expander("Step-by-step guide to obtain LLM API keys", expanded=False): st.markdown(""" PyHydroGeophysX requires an LLM (Large Language Model) API key to power its natural language processing capabilities. You can use any of the following providers: #### Option 1: OpenAI (Recommended for beginners) 1. Go to [OpenAI Platform](https://platform.openai.com/signup) 2. Create an account or sign in with Google/Microsoft 3. Navigate to **API Keys** in the left sidebar (or go to [API Keys page](https://platform.openai.com/api-keys)) 4. Click **"Create new secret key"** 5. Give it a name (e.g., "PyHydroGeophysX") and click **Create** 6. **Copy the key immediately** - you won't be able to see it again! 7. Add billing information at [Billing](https://platform.openai.com/account/billing) (required for API access) **Recommended models:** `gpt-4o-mini` (fast & cheap), `gpt-4o` (more capable) #### Option 2: Anthropic (Claude) 1. Go to [Anthropic Console](https://console.anthropic.com/) 2. Create an account and verify your email 3. Navigate to **API Keys** in the settings 4. Click **"Create Key"** 5. Copy and save your API key securely 6. Add billing information in the Billing section **Recommended models:** `claude-3-5-sonnet-20241022`, `claude-3-haiku-20240307` (faster) #### Option 3: Google (Gemini) 1. Go to [Google AI Studio](https://aistudio.google.com/) 2. Sign in with your Google account 3. Click **"Get API Key"** in the top right 4. Select or create a Google Cloud project 5. Copy your API key **Recommended models:** `gemini-1.5-flash` (fast), `gemini-1.5-pro` (more capable) --- **Important Tips:** - Keep your API key **secret** - never share it publicly or commit it to GitHub - API usage is **pay-per-use** - typical workflow costs $0.01-0.10 per run - Start with cheaper models (`gpt-4o-mini`, `claude-3-haiku`, `gemini-1.5-flash`) for testing - Set up **usage limits** in your provider's dashboard to avoid unexpected charges """) st.info("๐Ÿ’ก **Tip:** OpenAI's `gpt-4o-mini` offers the best balance of cost and performance for most hydrogeophysics workflows.") st.markdown("---") st.markdown("### Example Data from GitHub") for label, link in DATA_LINKS.items(): st.markdown(f"- [{label}]({link})") st.markdown("---") # ERT Example Tutorial st.markdown("### Example 1: Standard ERT Inversion") with st.expander("Step-by-step tutorial for ERT workflow", expanded=False): ex1 = EXAMPLE_DATA_LINKS["ERT Example (Ex1)"] st.markdown(f"**Description:** {ex1['description']}") st.markdown(f"**Jupyter Notebook:** [Ex_Unified_Workflow_ex1.ipynb]({ex1['notebook']})") st.markdown(f"**Data Files:** [ERT/DAS folder]({ex1['ert_data']})") st.markdown(f"- Files needed: `{', '.join(ex1['files'])}`") st.markdown("#### Step-by-Step Instructions") st.markdown(""" 1. **Download the data files** from the GitHub link above or upload your own ERT data 2. **Initialize the system** in the sidebar with your LLM API key 3. **Describe your workflow** in the text area. Example request: """) st.code("""We have ERT data from DAS-1 instrument at examples/data/ERT/DAS/20171105_1418.Data and electrode file in examples/data/ERT/DAS/electrodes.dat in the Snowy Range in southeastern Wyoming. The bedrock consists of foliated gneiss in the Cheyenne Belt. Use specific petrophysical parameters: rho_sat = 541, porosity = 0.37, n = 1.24""", language="text") st.markdown(""" 4. **Click "Run workflow"** - the system will: - Parse your natural language request - Load ERT data and electrode positions - Run resistivity inversion - Convert to water content using petrophysical parameters 5. **Review results** - download the generated report with resistivity and water content models """) st.markdown("#### Standard ERT Inversion Screenshots") image_dir = CURRENT_DIR / "images" for caption, filename in STANDARD_ERT_TUTORIAL_IMAGES: image_path = image_dir / filename if image_path.exists(): st.image(str(image_path), caption=caption, width="stretch") else: st.warning(f"Missing tutorial image: {image_path}") # Time-Lapse Example Tutorial st.markdown("### Example 2: Time-Lapse ERT with Climate Integration") with st.expander("Step-by-step tutorial for Time-Lapse workflow", expanded=False): ex2 = EXAMPLE_DATA_LINKS["Time-Lapse Example (Ex2)"] st.markdown(f"**Description:** {ex2['description']}") st.markdown(f"**Jupyter Notebook:** [Ex_Unified_Workflow_ex2.ipynb]({ex2['notebook']})") st.markdown(f"**Data Files:** [ERT/E4D folder]({ex2['ert_data']})") st.markdown(f"- Files needed: `{', '.join(ex2['files'])}`") st.markdown("#### Step-by-Step Instructions") st.markdown(""" 1. **Download all 4 time-lapse files** from the GitHub link above 2. **Initialize the system** in the sidebar with your LLM API key 3. **Describe your workflow** including all timestep files and climate parameters: """) st.code("""I need to run a TIME-LAPSE ERT inversion to monitor moisture infiltration. DATA FILES FOR TIME-LAPSE INVERSION: Please use these 4 E4D format data files located in folder data/ERT/E4D: - 2022-03-26_0030.ohm (BASELINE) - 2022-04-26_0030.ohm - 2022-05-26_0030.ohm - 2022-06-26_0030.ohm INVERSION SETTINGS: - Temporal Regularization Parameter: 10 - Spatial Regularization (lambda): 15 CLIMATE DATA INTEGRATION: - Site Coordinates: 38.92584ยฐN, -106.97998ยฐW - Date Range: March 2022 to June 2022 - Variables: precipitation, temperature, solar radiation""", language="text") st.markdown(""" 4. **Click "Run workflow"** - the system will: - Detect time-lapse mode from multiple files - Run temporal inversion with regularization - Fetch climate data from DayMet API - Correlate resistivity changes with precipitation and temperature 5. **Review temporal results** - see how subsurface moisture responds to climate events """) # Data Fusion Example Tutorial st.markdown("### Example 3: Data Fusion (Seismic + ERT)") with st.expander("Step-by-step tutorial for Data Fusion workflow", expanded=False): ex3 = EXAMPLE_DATA_LINKS["Data Fusion Example (Ex3)"] st.markdown(f"**Description:** {ex3['description']}") st.markdown(f"**Jupyter Notebook:** [Ex_Unified_Workflow_ex3.ipynb]({ex3['notebook']})") st.markdown(f"**Data Files:**") st.markdown(f"- Seismic: [Seismic folder]({ex3['seismic_data']})") st.markdown(f"- ERT: [ERT/Bert folder]({ex3['ert_data']})") st.markdown(f"- Files needed: `{', '.join(ex3['files'])}`") st.markdown("#### Step-by-Step Instructions") st.markdown(""" 1. **Download both seismic and ERT data files** from the GitHub links above 2. **Initialize the system** in the sidebar with your LLM API key 3. **Describe your multi-method workflow** with layer-specific parameters: """) st.code("""I need to characterize subsurface water content using a multi-method approach: 1. First, use field seismic refraction data to identify the boundary between regolith and fractured bedrock. The seismic data is in 'data/Seismic/srtfieldline2.dat' (BERT format) Use a velocity threshold of 1000 m/s to extract the interface. 2. Then, use this seismic structure to constrain ERT inversion. The ERT data is in 'data/ERT/Bert/fielddataline2.dat' (BERT format). Apply moderate regularization (lambda=20). 3. Finally, convert to water content using layer-specific petrophysical parameters. Use Monte Carlo uncertainty analysis with 100 realizations. - Regolith layer: rho_sat (50-250 ฮฉm), n (1.3-2.2), porosity (0.25-0.5) - Fractured bedrock layer: rho_sat (165-350 ฮฉm), n (2.0-2.2), porosity (0.2-0.3)""", language="text") st.markdown(""" 4. **Click "Run workflow"** - the system will: - Run seismic velocity inversion - Extract layer interface at velocity threshold - Use seismic structure to constrain ERT inversion - Apply layer-specific petrophysics with uncertainty quantification 5. **Review integrated results** - get water content with Monte Carlo uncertainty bounds """) st.markdown("---") st.markdown("### Request Template (Quick Reference)") st.code( """Run a standard ERT inversion using the DAS-1 instrument. Data file: 20171105_1418.Data Electrode file: electrodes.dat Petrophysics: rho_sat=541, porosity=0.37, n=1.24 Regularization lambda: 15""", language="text", ) st.markdown( """ **Tips** - Use the upload area if your local filenames differ from the examples. - Time-lapse data can be listed as multiple files or uploaded together. - Add geology hints, water content targets, or climate context for richer interpretations. """ ) def render_concepts_tab() -> None: st.subheader("Hydrogeophysics Concepts") st.markdown( """ Hydrogeophysics links geophysical measurements to subsurface water, structure, and flow. The workflows in this app focus on the methods below. """ ) col_a, col_b = st.columns([3, 2]) with col_a: st.markdown( """
Electrical Resistivity Tomography (ERT)
Seismic Refraction Tomography (SRT)
Time-Domain Electromagnetics (TDEM)
Data Fusion + Petrophysics
""", unsafe_allow_html=True, ) with col_b: st.markdown("#### Interactive Survey Visualization") html_sim = """
Geophysical Survey Simulator
""" components.html(html_sim, height=450) st.caption("Interactive visualization showing geophysical survey physics. Click tabs to explore different methods.") # LLM-powered explanation section st.markdown("---") st.markdown("### ๐Ÿค– Ask AI About Hydrogeophysics") st.markdown( """
Get AI-Powered Explanations

Use the initialized LLM to ask questions about hydrogeophysics concepts, get help with Python code, or understand your geophysical data.

""", unsafe_allow_html=True, ) # Initialize session state for AI chat if "concept_chat_history" not in st.session_state: st.session_state.concept_chat_history = [] if "pending_question" not in st.session_state: st.session_state.pending_question = "" # Check if there's a pending question from button click initial_value = "" if st.session_state.pending_question: initial_value = st.session_state.pending_question st.session_state.pending_question = "" # Clear it after using # Example questions as buttons st.markdown("**Quick questions:**") example_questions = [ "What is Archie's Law and how is it used in hydrogeophysics?", "How do I choose regularization parameters for ERT inversion?", "What is the difference between Wenner and Dipole-Dipole arrays?", "Show me Python code with PyHydroGeophysX to run ERT inversion and plot results", ] cols = st.columns(2) for idx, question in enumerate(example_questions): if cols[idx % 2].button(question, key=f"example_q_{idx}", width="stretch"): st.session_state.pending_question = question st.rerun() # Text input for custom questions user_question = st.text_area( "Or type your own question:", value=initial_value, height=100, placeholder="Ask about ERT, seismic, petrophysics, Python code, or any hydrogeophysics concept...", ) col_ask, col_clear = st.columns([3, 1]) ask_clicked = col_ask.button("๐Ÿ” Ask AI", type="primary", width="stretch") clear_clicked = col_clear.button("Clear History", width="stretch") if clear_clicked: st.session_state.concept_chat_history = [] st.session_state.pending_question = "" st.rerun() if ask_clicked and user_question.strip(): if not st.session_state.context_agent: st.warning("Please initialize the system in the sidebar first (set your API key).") else: with st.spinner("Thinking..."): try: # Build context-aware prompt for hydrogeophysics system_context = """You are a helpful hydrogeophysics expert assistant for PyHydroGeophysX. You help users understand geophysical concepts, Python code for geophysical analysis, and best practices for ERT, seismic, TDEM, and petrophysical workflows. ## PyHydroGeophysX Library Overview PyHydroGeophysX is an AI-powered hydrogeophysics workflow system. When users ask for code examples, ALWAYS show how to use PyHydroGeophysX agents first, then optionally show lower-level PyGIMLi code. ### Key PyHydroGeophysX Components: 1. **ContextInputAgent** - Parses natural language requests into workflow configurations 2. **BaseAgent.run_unified_agent_workflow()** - Main entry point for all workflows 3. **ERTAgent** - Handles ERT inversion using ResIPy/PyGIMLi 4. **SeismicAgent** - Handles seismic refraction tomography 5. **PetrophysicsAgent** - Converts resistivity to water content using Archie's Law 6. **TimeLapseAgent** - Handles multi-timestep ERT with temporal regularization 7. **DataFusionAgent** - Integrates seismic + ERT with structure constraints 8. **ClimateAgent** - Fetches and integrates meteorological data from DayMet ### Example PyHydroGeophysX Usage Patterns: **Standard ERT Workflow:** ```python from PyHydroGeophysX.agents import BaseAgent, ContextInputAgent # Initialize context agent context_agent = ContextInputAgent(api_key=api_key, model='gpt-4o-mini', llm_provider='openai') # Define workflow in natural language user_request = '''Run ERT inversion on data.ohm with electrode file electrodes.dat. Use regularization lambda=20 and convert to water content with rho_sat=500, porosity=0.35, n=1.5''' # Parse and execute config = context_agent.parse_request(user_request) results, plan, interpretation, files = BaseAgent.run_unified_agent_workflow( config, api_key, 'gpt-4o-mini', 'openai', output_dir ) ``` **Time-Lapse ERT:** ```python user_request = '''Run time-lapse ERT on files: baseline.ohm, time1.ohm, time2.ohm Temporal regularization: 10, Spatial lambda: 15 Fetch climate data for coordinates 38.9N, -107.0W from March to June 2022''' ``` **Data Fusion (Seismic + ERT):** ```python user_request = '''Use seismic data srt_data.dat with velocity threshold 1000 m/s to constrain ERT inversion of ert_data.dat. Layer petrophysics: regolith (rho_sat 50-250), bedrock (rho_sat 200-500)''' ``` ### Key Parameters: - **lambda (regularization)**: Controls smoothness (typical: 10-50, higher=smoother) - **rho_sat**: Saturated resistivity in Archie's Law (ฮฉm) - **porosity**: Rock/soil porosity (0-1) - **n**: Archie's saturation exponent (typically 1.3-2.5) - **velocity_threshold**: For seismic layer extraction (m/s) When providing code examples: 1. FIRST show PyHydroGeophysX natural language approach 2. THEN optionally show equivalent PyGIMLi/low-level code if relevant 3. Use NumPy and matplotlib for data manipulation and plotting 4. Be concise but thorough. Use bullet points for clarity when appropriate. 5. If asked about specific parameters, provide typical ranges and explain the physical meaning.""" full_prompt = f"{system_context}\n\nUser question: {user_question}" # Use the context agent's LLM to get a response response = st.session_state.context_agent.query_llm(full_prompt) # Add to chat history st.session_state.concept_chat_history.append({ "question": user_question, "answer": response }) # Rerun to show the response (text area will be empty on next run) st.rerun() except Exception as e: st.error(f"Error getting AI response: {e}") # Display chat history if st.session_state.concept_chat_history: st.markdown("---") st.markdown("### Conversation History") for i, chat in enumerate(reversed(st.session_state.concept_chat_history)): with st.expander(f"Q: {chat['question'][:60]}...", expanded=(i == 0)): st.markdown(f"**Question:** {chat['question']}") st.markdown("---") st.markdown(f"**Answer:**\n\n{chat['answer']}") def render_author_tab() -> None: # Custom CSS for author page st.markdown(""" """, unsafe_allow_html=True) # SHIP Lab Header st.markdown("""
SHIP Lab
Sustainability, Hydrogeophysics, Imaging, & Prediction
Advancing Earth systems understanding through integrated research approaches
""", unsafe_allow_html=True) # PI Profile Card col_profile, col_contact = st.columns([2, 1]) with col_profile: st.markdown("""
Hang Chen, Ph.D.
Assistant Professor, School of Earth, Environment, and Sustainability
University of Iowa | Affiliated Faculty, Lawrence Berkeley National Laboratory
๐Ÿ“ง hchen117@uiowa.edu  |  ๐Ÿ“ 23 Trowbridge Hall, Iowa City, IA
""", unsafe_allow_html=True) with col_contact: st.markdown("[๐ŸŒ **Visit Full Website**](https://sites.google.com/view/hangchen)") st.markdown("[๐Ÿ’ป **GitHub**](https://github.com/geohang)") st.markdown("---") # Sub-tabs inside the Author tab sub_tab3, sub_tab1, sub_tab4, sub_tab5, sub_tab6 = st.tabs([ "๐Ÿ”ฌ Research", "๐Ÿ  Lab & People", "๐Ÿ“„ Publications", "๐Ÿ“š Teaching", "๐Ÿ’ป Open Source" ]) # --- Lab & People Tab --- with sub_tab1: st.markdown("### SHIP Lab Members") st.markdown("*For full details, visit [SHIP Lab & People](https://sites.google.com/view/hangchen)*") col1, col2 = st.columns(2) with col1: st.markdown("""
Principal Investigator
Hang Chen
Assistant Professor, University of Iowa
""", unsafe_allow_html=True) st.markdown("""
PhD Student
Chen Xiong
Hydrogeophysics Research
""", unsafe_allow_html=True) st.markdown("""
Undergraduate
Cameron Roach
Gravity and Magnetic data joint inversion for geological hydrogen exploration
""", unsafe_allow_html=True) with col2: st.markdown("""
Master's Student
Weiyu Guo
Geophysical Modeling
""", unsafe_allow_html=True) st.markdown("""
Undergraduate
Jax Waller
Processing airborne electromagnetic data
""", unsafe_allow_html=True) st.markdown("""
Open Position
Postdoc Opening
Contact for opportunities!
""", unsafe_allow_html=True) st.info("๐ŸŽ“ **Interested in joining?** Visit [Opportunities](https://sites.google.com/view/hangchen/opportunities) for current openings.") # --- Research Tab --- with sub_tab3: st.markdown("### Research") st.markdown("*For detailed descriptions, visit [Research](https://sites.google.com/view/hangchen/research_1)*") # Research Methods st.markdown("#### Research Methods") st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) # Research Applications st.markdown("#### Research Applications") st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) # --- Publications Tab --- with sub_tab4: st.markdown("### Selected Publications") st.markdown("*For complete list, visit [Publications](https://sites.google.com/view/hangchen/publications)*") pubs_preview = [ ("2025", "Development of an ERT-based framework for bentonite buffers monitoring - Part I & II", "JGR: Solid Earth"), ("2024", "Electrical resistivity changes during heating experiments in salt formations", "Geophysical Research Letters"), ("2024", "Influence of subsurface critical zone structure on hydrological partitioning", "Geophysical Research Letters"), ("2023", "Geophysics-informed hydrologic modeling of a mountain headwater catchment", "Water Resources Research"), ] for year, title, journal in pubs_preview: st.markdown(f"**{year}** | {title} - *{journal}*") st.markdown("---") st.markdown("[๐Ÿ“„ **View All Publications โ†’**](https://sites.google.com/view/hangchen/publications)") # --- Teaching Tab --- with sub_tab5: st.markdown("### Teaching") st.markdown("*For course materials, visit [Teaching](https://sites.google.com/view/hangchen/teaching)*") st.markdown(""" """, unsafe_allow_html=True) st.markdown(""" """, unsafe_allow_html=True) st.markdown("[๐Ÿ“š **View Teaching Page โ†’**](https://sites.google.com/view/hangchen/teaching)") # --- Open Source Tab --- with sub_tab6: st.markdown("### Open Source Projects") st.markdown("*For all codes, visit [Open Source Codes](https://sites.google.com/view/hangchen/open-source-codes)*") st.markdown(""" """, unsafe_allow_html=True) st.markdown("[๐Ÿ’ป **View All Open Source Projects โ†’**](https://sites.google.com/view/hangchen/open-source-codes)") st.markdown("---") st.markdown("### Acknowledgments") st.markdown(""" PyHydroGeophysX development is supported by: - University of Iowa - Lawrence Berkeley National Laboratory Special thanks to **ResIPy**, **PyGIMLi**, and **SimPEG** for their excellent geophysical libraries. """) def render_local_deployment_tab() -> None: st.subheader("Local Deployment") st.markdown( """ # PyHydroGeophysX - Quick Start Guide ## ๐Ÿš€ Get Started in 3 Steps ### Step 0: Download the GitHub Repository ```bash git clone https://github.com/geohang/PyHydroGeophysX.git cd PyHydroGeophysX ``` ### Step 1: Launch the Web App ```bash cd examples streamlit run app_geophysics_workflow.py ``` Or use the launcher scripts: - **Windows**: `start_webapp.bat` - **Linux/Mac**: `./start_webapp.sh` ### Step 2: Configure API Key In the sidebar: 1. Select LLM provider (OpenAI recommended) 2. Enter your API key 3. Click "๐Ÿš€ Initialize System" ### Step 3: Run Your First Workflow 1. Choose a workflow example or describe your data 2. Upload files if needed 3. Click **Run workflow** and review the report outputs """ ) def render_workflow_tab(sidebar_state: Dict[str, str]) -> None: st.markdown("---") st.info( "Cloud resources are limited. For big datasets, use the Local Deployment tab so you can run the same web interface with local compute." ) st.subheader("Describe your workflow") request_text = st.text_area( "Describe what you want to do (files, parameters, outputs)", value=st.session_state.user_request, height=180, placeholder="Example: Run a time-lapse ERT inversion on four surveys...", ) st.session_state.user_request = request_text render_example_buttons() if not st.session_state.context_agent: st.warning("Initialize the system from the sidebar before running. You can still draft your request below.") st.markdown("---") st.subheader("Upload data (optional)") st.caption("Upload any data files here; the app will map them by filename. Otherwise, just reference paths in your description.") uploaded_files = st.file_uploader( "Data files", accept_multiple_files=True, type=["ohm", "dat", "data", "txt", "sgy", "segy"], help="Single upload area for ERT, seismic, electrodes, etc.", ) st.markdown("---") run_clicked = st.button("Run workflow", type="primary", width="stretch") if run_clicked: if not request_text.strip(): st.error("Please describe your workflow.") return if not st.session_state.context_agent: st.error("Initialize the system in the sidebar before running.") return output_path = Path(sidebar_state["output_dir"]).expanduser() output_path.mkdir(parents=True, exist_ok=True) # Parse uploads upload_overrides: Dict[str, str] = {} saved_paths = handle_uploads(output_path, uploaded_files, upload_overrides) # Merge uploaded workflow overrides into the text-derived config during run_workflow st.session_state.workflow_config = upload_overrides run_workflow(request_text, upload_overrides, saved_paths, output_path) if st.session_state.workflow_result: st.markdown("---") render_results() def render_sidebar() -> Dict[str, str]: st.sidebar.header("Configuration") provider = st.sidebar.selectbox( "LLM provider", options=["openai", "gemini", "claude"], index=["openai", "gemini", "claude"].index(st.session_state.llm_provider) if st.session_state.llm_provider in ["openai", "gemini", "claude"] else 0, help="Used by the context agent to parse your natural-language request.", ) default_models = {"openai": "gpt-4o-mini", "gemini": "gemini-pro", "claude": "claude-3-opus-20240229"} model_default = st.session_state.llm_model or default_models.get(provider, "gpt-4o-mini") model = st.sidebar.text_input("Model name", value=model_default) env_map = {"openai": "OPENAI_API_KEY", "gemini": "GEMINI_API_KEY", "claude": "ANTHROPIC_API_KEY"} preset_key = st.session_state.api_key or os.getenv(env_map[provider], "") api_key = st.sidebar.text_input( "API key", type="password", value=preset_key, help=f"Read from environment if set: {env_map[provider]}", ) output_dir = st.sidebar.text_input("Output directory", value=st.session_state.output_dir) col_a, col_b = st.sidebar.columns(2) init_clicked = col_a.button("Initialize", type="primary", width="stretch") reset_clicked = col_b.button("Reset state", width="stretch") if reset_clicked: for key in ["context_agent", "workflow_result", "workflow_config", "upload_dir"]: st.session_state[key] = None st.session_state.user_request = "" st.sidebar.info("Session cleared.") if init_clicked: if not api_key.strip(): st.sidebar.error("Please provide an API key or set the environment variable first.") else: try: st.session_state.context_agent = ContextInputAgent( api_key=api_key.strip(), model=model.strip(), llm_provider=provider ) st.session_state.api_key = api_key.strip() st.session_state.llm_model = model.strip() st.session_state.llm_provider = provider st.sidebar.success("Context agent ready.") except Exception as exc: # noqa: BLE001 st.sidebar.error(f"Initialization failed: {exc}") st.sidebar.exception(exc) st.sidebar.markdown("---") if st.session_state.context_agent: st.sidebar.success("System status: ready") else: st.sidebar.warning("System status: not initialized") return {"provider": provider, "model": model, "api_key": api_key, "output_dir": output_dir} def save_upload(file_obj, target_dir: Path) -> Path: target_dir.mkdir(parents=True, exist_ok=True) dest = target_dir / file_obj.name dest.write_bytes(file_obj.read()) return dest def handle_uploads( output_dir: Path, uploaded_files: Optional[List], workflow_config: Dict[str, str], ) -> Dict[str, str]: """ Single upload entrypoint. Saves all files and applies lightweight heuristics: - Electrode files detected by name (electrode/elec) kept as electrode_file - First .ohm/.data/.dat (excluding electrode files) becomes ert_file - If multiple data files remain, treat as time-lapse list - First file with 'seis' in name or '.seis/.sgy/.segy' becomes seismic_file - All files exposed in uploaded_files map """ saved_paths: Dict[str, str] = {} if not uploaded_files: return saved_paths temp_dir = Path(tempfile.mkdtemp(prefix="phgx_uploads_")) st.session_state.upload_dir = str(temp_dir) st.info(f"Uploaded files stored in: {temp_dir}") all_paths = [] for f in uploaded_files: dest = save_upload(f, temp_dir) all_paths.append(dest) saved_paths[f.name] = str(dest) # Heuristics for convenience def is_electrode(p: Path) -> bool: name = p.name.lower() return ("electrode" in name or "electrodes" in name or "elec" in name) and p.suffix.lower() in [".dat", ".txt", ".csv"] electrode_files = [p for p in all_paths if is_electrode(p)] # Detect TDEM files (typically contain 'tdem', 'tem', or 'electromagnetic' in name) def is_tdem(p: Path) -> bool: name = p.name.lower() return ("tdem" in name or "tem_" in name or "electromagnetic" in name) and p.suffix.lower() in [".txt", ".dat", ".csv"] tdem_candidates = [p for p in all_paths if is_tdem(p)] # Data candidates exclude electrode files, seismic files, and TDEM files data_candidates = [ p for p in all_paths if p.suffix.lower() in [".ohm", ".data", ".dat", ".txt"] and not is_electrode(p) and "seis" not in p.name.lower() and not is_tdem(p) ] seismic_candidates = [p for p in all_paths if "seis" in p.name.lower() or p.suffix.lower() in [".sgy", ".segy"]] if electrode_files: workflow_config["electrode_file"] = str(electrode_files[0]) if len(data_candidates) == 1: workflow_config["data_file"] = str(data_candidates[0]) workflow_config["ert_file"] = str(data_candidates[0]) elif len(data_candidates) > 1: workflow_config["time_lapse_files"] = [str(p) for p in data_candidates] workflow_config["timelapse_files"] = [str(p) for p in data_candidates] if seismic_candidates: workflow_config["seismic_file"] = str(seismic_candidates[0]) if tdem_candidates: workflow_config["tdem_file"] = str(tdem_candidates[0]) # Expose all uploads for downstream agents workflow_config["uploaded_files"] = {p.name: str(p) for p in all_paths} workflow_config["output_dir"] = str(output_dir) return saved_paths def run_workflow( user_request: str, upload_overrides: Dict[str, str], saved_paths: Dict[str, str], output_dir: Path, ) -> None: # Create progress container for real-time updates progress_container = st.container() with progress_container: progress_bar = st.progress(0.0, text="Initializing workflow...") status_text = st.empty() step_expander = st.expander("Workflow Steps", expanded=True) def update_progress(step: str, progress: float, details: str = ""): """Callback to update progress in the UI.""" progress_bar.progress(progress, text=step) if details: status_text.info(details) try: update_progress("Parsing request with LLM...", 0.05, "Analyzing your natural language request") workflow_config = st.session_state.context_agent.parse_request(user_request.strip()) # Merge upload-driven overrides on top of parsed configuration workflow_config.update(upload_overrides) workflow_config["user_request"] = user_request.strip() workflow_config["output_dir"] = str(output_dir) st.session_state.workflow_config = workflow_config update_progress("Request parsed successfully", 0.10, f"Detected workflow type: {_detect_workflow_type(workflow_config)}") except Exception as exc: # noqa: BLE001 st.error(f"Failed to parse request: {exc}") st.exception(exc) return try: # Show execution plan before running with step_expander: st.markdown("**Execution Plan:**") update_progress("Starting workflow execution...", 0.15, "Loading data and preparing inversion") # Run workflow with progress callback results, execution_plan, interpretation, report_files = BaseAgent.run_unified_agent_workflow( workflow_config, st.session_state.api_key, st.session_state.llm_model, st.session_state.llm_provider, output_dir, progress_callback=update_progress, ) # Display execution steps if execution_plan: with step_expander: for i, step in enumerate(execution_plan, 1): st.markdown(f"{i}. **{step.get('step', '')}** - {step.get('agent', '')}") update_progress("Workflow complete!", 1.0, "All steps completed successfully") st.session_state.workflow_result = { "results": results, "execution_plan": execution_plan, "interpretation": interpretation, "report_files": report_files, "workflow_config": workflow_config, "uploads": saved_paths, } except Exception as exc: # noqa: BLE001 update_progress("Workflow failed", 1.0) st.error(f"Workflow failed: {exc}") # Try LLM to suggest root cause if context agent available suggestion = None if st.session_state.context_agent: try: prompt = ( "You are debugging a geophysics workflow error. " f"User request: {user_request}\n" f"Workflow config: {workflow_config}\n" f"Error: {exc}\n" "Suggest concise steps the user should check (file paths, electrode files, instrument type). " "Keep it under 5 bullets." ) suggestion = st.session_state.context_agent.query_llm(prompt) except Exception: suggestion = None if suggestion: st.info(f"Suggested checks:\n{suggestion}") st.exception(exc) def _detect_workflow_type(config: Dict) -> str: """Detect workflow type from configuration.""" config_keys = set(config.keys()) user_request = config.get('user_request', '').lower() # TDEM detection if (config.get('tdem_file') or config.get('tdem_mode') or 'tdem' in user_request or 'tem ' in user_request or 'electromagnetic' in user_request): return "TDEM Inversion" # Seismic-only detection elif (config.get('seismic_file') and not config.get('ert_file') or config.get('seismic_only') or 'seismic refraction' in user_request or 'srt inversion' in user_request): return "Seismic Refraction Tomography" elif 'timelapse_files' in config_keys or 'time_lapse_files' in config_keys: return "Time-Lapse ERT" elif config.get('velocity_threshold') or (config.get('ert_file') and config.get('seismic_file')): return "Data Fusion (Seismic + ERT)" elif config.get('ert_file') or config.get('data_file'): # Check if water content is requested if 'water content' in user_request or 'petrophysic' in user_request or 'moisture' in user_request: return "ERT Inversion + Petrophysics" return "Direct ERT Inversion" return "Unknown" def render_results() -> None: data = st.session_state.workflow_result if not data: return st.success("Workflow complete.") interpretation = data.get("interpretation") if interpretation: st.markdown("### Interpretation") st.info(interpretation) execution_plan = data.get("execution_plan") or [] if execution_plan: st.markdown("### Execution plan") for idx, step in enumerate(execution_plan, 1): st.markdown(f"{idx}. **{step.get('step','')}** - {step.get('agent','')}") results = data.get("results") or {} if results.get("status") == "success": st.markdown("### Results summary") stats = results.get("statistics", {}) col1, col2, col3 = st.columns(3) with col1: if stats.get("resistivity_range"): rng = stats["resistivity_range"] st.metric("Resistivity range (ohm-m)", f"{rng[0]:.1f} to {rng[1]:.1f}") with col2: if stats.get("wc_range"): rng = stats["wc_range"] st.metric("Water content range", f"{rng[0]:.4f} to {rng[1]:.4f}") with col3: if stats.get("num_cells"): st.metric("Mesh cells", stats["num_cells"]) elif stats.get("n_timesteps"): st.metric("Time steps", stats["n_timesteps"]) elif results: st.error(f"Workflow reported an error: {results.get('error','Unknown error')}") report_files = data.get("report_files") or {} if report_files: st.markdown("### Generated files") for file_type, file_path in report_files.items(): path_obj = Path(str(file_path)) label_map = { "report_markdown": "Download report (Markdown)", "report_html": "Download report (HTML)", "report_pdf": "Download report (PDF)", } default_label = f"Download {file_type.replace('_', ' ').title()}" label = label_map.get(file_type, default_label) if path_obj.exists(): with open(path_obj, "rb") as f: st.download_button( label=label, data=f, file_name=path_obj.name, mime="application/octet-stream", ) else: st.markdown(f"- {file_type}: {path_obj}") if data.get("workflow_config"): with st.expander("View workflow configuration"): st.json(data["workflow_config"]) if data.get("uploads"): with st.expander("Uploaded file locations"): st.json(data["uploads"]) def render_cloud_tips() -> None: st.markdown("---") st.markdown("### Run in the cloud") st.markdown( """ - Use `streamlit run examples/app_geophysics_workflow.py` inside a container or VM with Python 3.10+ and required libs installed (`pip install -r requirements.txt streamlit`). - Set API keys as environment variables (`OPENAI_API_KEY`, `GEMINI_API_KEY`, or `ANTHROPIC_API_KEY`) in your cloud platform secrets. - Persist `results/` by mounting a volume or cloud storage (e.g., S3, Azure Files, GCS) to avoid losing generated reports. - On Streamlit Community Cloud, add `requirements.txt` and set the working directory to `examples/`; entry point: `streamlit run app_geophysics_workflow.py`. """ ) def render_support_section() -> None: """Render the support/donate section.""" st.markdown( """
๐ŸŽ‰ FREE & OPEN SOURCE
Support PyHydroGeophysX Development
This app is developed for free usage by the research community.
If you find it useful, consider supporting better Cloud Services!
๐Ÿ’™ Donate via Venmo @Hang-Chen-35
Need a GPT API key to try?
Email me at hang-chen-1@uiowa.edu
""", unsafe_allow_html=True, ) def main() -> None: init_session_state() render_header() # Check for missing dependencies if not AGENTS_AVAILABLE: st.error(f""" โš ๏ธ **Missing Dependencies** Some required packages are not installed: `{IMPORT_ERROR}` Please install the required dependencies: ```bash pip install pygimli SimPEG openai ``` Or use conda for pygimli: ```bash conda install -c gimli pygimli ``` """) st.stop() if not PYGIMLI_AVAILABLE: st.warning(""" โš ๏ธ **PyGIMLi Not Available** ERT inversion and some geophysical functions require PyGIMLi. Install with: `conda install -c gimli pygimli` or `pip install pygimli` You can still use TDEM workflows with SimPEG if available. """) sidebar_state = render_sidebar() tab_workflow, tab_tutorial, tab_concepts, tab_local, tab_author = st.tabs([ "๐Ÿš€ Run Workflow", "๐Ÿ“– Step-by-Step Tutorials", "๐Ÿ”ฌ Learn Hydrogeophysics & Ask AI", "๐Ÿ’ป Local Deployment", "๐Ÿ‘ค About Author", ]) with tab_workflow: render_workflow_tab(sidebar_state) with tab_tutorial: render_tutorial_tab() with tab_concepts: render_concepts_tab() with tab_local: render_local_deployment_tab() with tab_author: render_author_tab() # Render support section in sidebar with st.sidebar: render_support_section() if __name__ == "__main__": main() .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.000 seconds) .. _sphx_glr_download_auto_examples_app_geophysics_workflow.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: app_geophysics_workflow.py ` .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: app_geophysics_workflow.ipynb `