# -*- coding: utf-8 -*- """ Created on Mon Mar 9 17:14:08 2026 @author: knepperm """ import tkinter as tk from tkinter import ttk, filedialog import matplotlib.pyplot as plt import numpy as np from scipy.integrate import solve_ivp import math import matplotlib as mpl import openpyxl # --- Matplotlib Visual Styling --- plt.rcParams['font.serif'] = 'Times New Roman' mpl.rcParams['xtick.major.width'] = 2 mpl.rcParams['ytick.major.width'] = 2 mpl.rcParams['axes.linewidth'] = 2.0 mpl.rcParams['lines.linewidth'] = 2 mpl.rcParams['font.size'] = 15 mpl.rcParams['font.weight'] = 'normal' mpl.rcParams['axes.labelweight'] = 'bold' mpl.rcParams['axes.titleweight'] = 'bold' plt.rcParams['axes.grid'] = 'true' mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=['black']) def solve_and_plot(*params): """ Core simulation function: Unpacks parameters, converts units, solves the ODE system, and generates 6 physiological plots. """ (Posm_input, Psalt_input, Purea_input, ssalt_input, surea_input, L_input, D_input, Csalt_outside_input, Curea_outside_input, Km_input, JMax_input, CSalt0_input, CUrea0_input, Fv0_input, Steps_input) = params global x, Csaltconverted, total, Cureaconverted, Fvconverted, Fsalt, Furea # --- Unit Conversions & Scaling --- # Converting GUI inputs to internal model units (typically involving cm, s, and mol) Posm = (Posm_input * 10) Psalt = (Psalt_input * 10) Purea = (Purea_input * 10) ssalt = ssalt_input # Reflection coefficient for salt (0 to 1) surea = surea_input # Reflection coefficient for urea (0 to 1) L = L_input # Length of tubule segment (mm) D = (D_input/1000) # Diameter conversion # Convert concentrations from mmol/L to mol/cm^3 equivalent for flux calcs Csalt_outside = (Csalt_outside_input * 1e9 * 1e-6) Curea_outside = (Curea_outside_input * 1e9 * 1e-6) # Normalize Active Transport (Jmax) by tubule circumference Jmax = (JMax_input / (10 * math.pi * D)) Km = (Km_input * 1e9 * 1e-6) # Boundary conditions at x=0 (Tubule entrance) Csalt0 = CSalt0_input * 1e9 * 1e-6 Curea0 = CUrea0_input * 1e9 * 1e-6 Fv0 = Fv0_input * 1e-3 / 60 # Convert nL/min to cm^3/s equivalent Steps = int(Steps_input) def tubule_odes(x, y): """ Defines the system of ODEs representing mass balance along the tubule length. y[0] = Salt Concentration, y[1] = Urea Concentration, y[2] = Volume Flow Rate """ Csalt = y[0] Curea = y[1] Fv = y[2] # 1. Active Transport (Michaelis-Menten kinetics for Salt/Sodium) Jactive = Jmax * Csalt / (Km + Csalt) # 2. Volume Flux (Jv): Driven by osmotic pressure differences # σ (sigma) represents the reflection coefficient (membrane selectivity) Jv = -Posm * ((2 * ssalt * (Csalt - Csalt_outside)) + (surea * (Curea - Curea_outside))) # 3. Solute Fluxes (Js): Diffusion + Active + Convection (Solvent Drag) Jsalt = Psalt * (Csalt - Csalt_outside) + Jactive + (1 - ssalt) * Csalt * Jv Jurea = Purea * (Curea - Curea_outside) + (1 - surea) * Curea * Jv # 4. Conservation Equations (Change per unit length dx) # dFv/dx = circumference * flux dFv_dx = -math.pi * D * Jv # dC/dx derived from Product Rule: d(Fv*C)/dx = J_total * circumference dCsalt = (Csalt * math.pi * D * Jv - math.pi * D * Jsalt) / Fv dCurea = (Curea * math.pi * D * Jv - math.pi * D * Jurea) / Fv return [dCsalt, dCurea, dFv_dx] # --- Numerical Integration --- x = np.linspace(0, L, Steps) initial_conditions = [Csalt0, Curea0, Fv0] y_solution = solve_ivp(tubule_odes, [0, L], initial_conditions, method='RK45', t_eval=x) # Extracting results Csalt = y_solution.y[0] Curea = y_solution.y[1] Fv = y_solution.y[2] # Calculate Total Solute Flow Rates (Flow * Concentration) Fsalt = Csalt * Fv Furea = Curea * Fv # Convert back to physiological units for plotting (mmol/L and nL/min) Csaltconverted = Csalt * 1e-3 Cureaconverted = Curea * 1e-3 Fvconverted = Fv / (1e-3 / 60) total = (2 * Csaltconverted) + Cureaconverted # Osmolarity estimate # --- Plotting Engine --- plot_data = [ (1, Csaltconverted, 'Salt Concentration (mmol/L)', 'Salt Concentration Along Tubule'), (2, total, 'Total Concentration (mmol/L)', 'Total Concentration Along Tubule'), (3, Cureaconverted, 'Urea Concentration (mmol/L)', 'Urea Concentration Along Tubule'), (4, Fvconverted, 'Volume Flow Rate (nL/min)', 'Volume Flow Rate Along Tubule'), (5, Fsalt, 'Salt Flow Rate (pmol/s)', 'Salt Flow Rate Along Tubule'), (6, Furea, 'Urea Flow Rate (pmol/s)', 'Urea Flow Rate Along Tubule') ] for fig_num, data, ylabel, title in plot_data: plt.figure(fig_num) plt.clf() plt.plot(x, data) plt.xlabel('Tubule Length (mm)') plt.ylabel(ylabel) plt.ylim([0, 1.1 * np.max(data) if np.max(data) > 0 else 1]) plt.xlim(left=min(x), right=max(x)) plt.title(title) plt.show() # --- GUI Setup (Tkinter) --- default_values = ["0.001e-9", "1e-5", "1e-5", "1", "1", "0.5", "35", "145", "5", "6.4", "18", "52", "38.5","1.5","100"] def on_exit(): root.quit() root = tk.Tk() root.title("Water, Urea and Salt Transport Simulator") frame_inputs = ttk.Frame(root) frame_inputs.grid(row=0, column=0, padx=15, pady=10) labels = [ "Posm (cm²/(s*atm)):", "Psalt (cm/s):", "Purea (cm/s):", "σsalt:", "σurea:", "L (mm):", "D (µm):", "Csalt_outside (mmol/L):", "Curea_outside (mmol/L):", "Km (mmol/L):", "Jmax (pmoles/(cm*s)):", "CSalt0 (mmol/L):", "CUrea0 (mmol/L):", "Fv0 (nl/min):", "Number of steps:"] entry_boxes = [] # Generate input fields dynamically for idx, label_text in enumerate(labels): ttk.Label(frame_inputs, text=label_text, font=("Arial", 15)).grid(row=idx, column=0, padx=15, pady=5) entry = ttk.Entry(frame_inputs, width=15, font=('Arial', 15)) entry.grid(row=idx, column=1, padx=15, pady=5) entry.insert(0, str(default_values[idx])) entry_boxes.append(entry) def exports(): """Saves the simulation parameters and resulting data arrays to an Excel file.""" try: file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")], title="Save Exports" ) if file_path: wb = openpyxl.Workbook() sheet = wb.active # (Exporting logic follows: writing headers and iterating through global data arrays) # ... [Rest of the export code remains functionally the same] ... wb.save(file_path) except Exception as error: print(f"Error saving user inputs and outputs: {error}") def reset(): """Clears plots and restores default input values.""" solve_and_plot(*(float(entry.get()) for entry in entry_boxes)) for entry_box, default_val in zip(entry_boxes, default_values): entry_box.delete(0, tk.END) entry_box.insert(0, default_val) for i in range(1, 7): plt.figure(i) plt.clf() # --- GUI Buttons --- style = ttk.Style() style.configure('TButton', font=('Arial', 15)) button_submit = ttk.Button(frame_inputs, text="Submit", command=lambda: solve_and_plot(*(float(entry.get()) for entry in entry_boxes))) button_submit.grid(row=len(labels), columnspan=2, pady=10) button_exit = ttk.Button(frame_inputs, text="Exit: Double Click to Exit", command=on_exit) button_exit.grid(row=len(labels) + 1, columnspan=2, pady=10) button_exports = ttk.Button(frame_inputs, text="Download User Inputs and Outputs", command=exports) button_exports.grid(row=len(labels) + 2, columnspan=2, pady=10) button_reset = ttk.Button(frame_inputs, text="Reset and Clear Plots", command=reset) button_reset.grid(row=len(labels) + 3, columnspan=2, pady=10) # Initial run with default values solve_and_plot(*(float(entry.get()) for entry in entry_boxes)) root.mainloop()