import streamlit as st import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap from scipy.ndimage import label as nd_label def run_lab_model( width=30, total_time=500, channel_width=10, avulsion_period=100, lateral_migration_rate=5, switch_step=None, new_channel_width=None, new_avulsion_period=None, new_lateral_migration_rate=None ): """ Simple LAB Model: Uniform aggradation, lateral migration, and periodic avulsion. Args: width (int): Width of the basin cross-section (number of cells). total_time (int): Total simulation time (timesteps). Represents depth (Y-axis). channel_width (int): Width of the channel (sand) in cells. avulsion_period (int): Number of timesteps between avulsion (channel jump). lateral_migration_rate (float): Number of cells the channel moves per timestep (0~1). Returns: np.ndarray: 2D array of size (total_time, width). 0=none, 1=mud (floodplain), 2=sand (channel). """ # Initialize variables stratigraphy = np.zeros((total_time, width), dtype=int) surface_elevation = np.zeros(width) channel_x = float(width) / 2 # Declare as float np.random.seed() # System random migration_dir = np.random.choice([-1, 1]) # Parameter change point (e.g., halfway through the simulation) if switch_step is None: switch_step = total_time // 2 # Example of parameters to change (received as arguments from sliders) if new_avulsion_period is None: new_avulsion_period = avulsion_period if new_channel_width is None: new_channel_width = channel_width if new_lateral_migration_rate is None: new_lateral_migration_rate = lateral_migration_rate for t in range(total_time): # --- Change parameters mid-simulation --- if t == switch_step: avulsion_period = new_avulsion_period channel_width = new_channel_width lateral_migration_rate = new_lateral_migration_rate # --- 1. Uniform aggradation: Increase elevation of all cells by +1 --- surface_elevation += 1 # --- 2. Channel lateral migration --- channel_x += migration_dir * lateral_migration_rate # Reverse direction and move once in the opposite direction if boundary is reached if channel_x <= channel_width / 2: channel_x = channel_width / 2 migration_dir *= -1 channel_x += migration_dir * lateral_migration_rate elif channel_x >= width - channel_width / 2 - 1: channel_x = width - channel_width / 2 - 1 migration_dir *= -1 channel_x += migration_dir * lateral_migration_rate # --- 3. Record stratigraphy --- new_layer_sed_type = np.full(width, 1) # 1 = Mud start_x = max(0, int(np.floor(channel_x - channel_width / 2))) end_x = min(width, int(np.ceil(channel_x + channel_width / 2))) new_layer_sed_type[start_x:end_x] = 2 # 2 = Sand stratigraphy[t, :] = new_layer_sed_type # --- 4. Avulsion: Random channel jump every avulsion period --- if (t + 1) % avulsion_period == 0: channel_x = float(np.random.uniform(channel_width / 2, width - channel_width / 2)) migration_dir = np.random.choice([-1, 1]) return stratigraphy, switch_step def analyze_stratigraphy(stratigraphy): """ Analyze the generated stratigraphy and output simple statistics. """ if stratigraphy.size == 0: print("No data to analyze.") return # N/G Ratio (Net-to-Gross): Proportion of sand (reservoir) in total sediment is_sand = (stratigraphy == 2) net_to_gross = np.sum(is_sand) / stratigraphy.size # Amalgamation Ratio (Vertical connectivity): sand_cells_below_first_row = is_sand[1:, :] if sand_cells_below_first_row.any(): sand_above_sand = is_sand[1:, :] & is_sand[:-1, :] amalgamation_ratio = np.sum(sand_above_sand) / np.sum(sand_cells_below_first_row) else: amalgamation_ratio = 0 labeled_array, num_features = nd_label(is_sand) print(f"--- Analysis Results ---") print(f"Net-to-Gross (N/G) Ratio: {net_to_gross:.2%}") print(f"Amalgamation Ratio (Vertical connectivity): {amalgamation_ratio:.2%}") print(f"Total Sand Bodies (Number of connected sand regions): {num_features}") print(f"------------------") def plot_stratigraphy(stratigraphy, title, switch_step=None): colors = ['#8B4513', '#FFD700'] cmap = ListedColormap(colors) fig = plt.figure(figsize=(10, 6)) plt.imshow(stratigraphy, aspect='auto', cmap=cmap, interpolation='none') plt.title(title) plt.xlabel("Basin Width (cells)") plt.ylabel("Time / Depth (timesteps)") plt.gca().invert_yaxis() if switch_step is not None: plt.axhline(switch_step, color='red', linewidth=2, linestyle='--', label='Scenario Switch') handles = [plt.Rectangle((0,0),1,1, color=colors[0]), plt.Rectangle((0,0),1,1, color=colors[1])] labels = ["Floodplain (Mud)", "Channel (Sand)"] plt.legend(handles, labels, loc='upper right') plt.show() st.title("LAB Model Alluvial Stratigraphy Simulator") # Sidebar: Parameter input width = st.sidebar.slider("Basin Width (cells)", 10, 30, 20) total_time = st.sidebar.slider("Total Time Steps", 100, 200, 100) channel_width = st.sidebar.slider("Channel Width (cells)", 1, width//2, 5) avulsion_period = st.sidebar.slider("Avulsion Period (timesteps)", 1, 10, 7) lateral_migration_rate = st.sidebar.slider("Lateral Migration Rate (cells/timestep)", 0.1, 2.0, 1.0) # Scenario switch parameters switch_step = st.sidebar.slider("Scenario Switch Step", 1, total_time-1, total_time//2) new_channel_width = st.sidebar.slider("New Channel Width", 1, width//2, 2) new_avulsion_period = st.sidebar.slider("New Avulsion Period", 1, 10, 3) new_lateral_migration_rate = st.sidebar.slider("New Lateral Migration Rate", 0.1, 2.0, 2.0) if st.button("Run Simulation"): # Run simulation strat, switch_step_val = run_lab_model( width=width, total_time=total_time, channel_width=channel_width, avulsion_period=avulsion_period, lateral_migration_rate=lateral_migration_rate, switch_step=switch_step, new_channel_width=new_channel_width, new_avulsion_period=new_avulsion_period, new_lateral_migration_rate=new_lateral_migration_rate ) # Apply values after parameter change point # switch_step, new_channel_width, etc., are already handled within run_lab_model # Output analysis results (before/after parameter change) st.subheader("Simulation Results") def analyze_segment(segment, label): is_sand = (segment == 2) net_to_gross = np.sum(is_sand) / segment.size sand_cells_below_first_row = is_sand[1:, :] if sand_cells_below_first_row.any(): sand_above_sand = is_sand[1:, :] & is_sand[:-1, :] amalgamation_ratio = np.sum(sand_above_sand) / np.sum(sand_cells_below_first_row) else: amalgamation_ratio = 0 labeled_array, num_features = nd_label(is_sand) return { "Label": label, "Net-to-Gross (N/G) Ratio": f"{net_to_gross:.2%}", "Amalgamation Ratio": f"{amalgamation_ratio:.2%}", "Total Sand Bodies": num_features } # Analyze results overall_results = analyze_segment(strat, "Overall Results") before_results = analyze_segment(strat[:switch_step_val, :], "Before Parameter Switch") after_results = analyze_segment(strat[switch_step_val:, :], "After Parameter Switch") # Display plot fig, ax = plt.subplots(figsize=(10, 6)) cmap = ListedColormap(['#8B4513', '#FFD700']) ax.imshow(strat, aspect='auto', cmap=cmap, interpolation='none') ax.axhline(switch_step_val, color='red', linewidth=2, linestyle='--', label='Scenario Switch') ax.set_title("LAB Simulation") ax.set_xlabel("Basin Width (cells)") ax.set_ylabel("Time / Depth (timesteps)") ax.invert_yaxis() handles = [plt.Rectangle((0,0),1,1, color='#8B4513'), plt.Rectangle((0,0),1,1, color='#FFD700')] labels = ["Floodplain (Mud)", "Channel (Sand)"] ax.legend(handles, labels, loc='upper right') st.pyplot(fig) # Display results in a table st.table([overall_results, before_results, after_results])