Spaces:
Running
Running
| import marimo | |
| __generated_with = "0.12.5" | |
| app = marimo.App(width="medium") | |
| def _(): | |
| import marimo as mo | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from typing import Dict | |
| from numpy.typing import NDArray | |
| conc_a = mo.ui.text(value="0.2",label="$[\mathrm{A}]_0$") | |
| conc_b = mo.ui.text(value="0.1",label="$[\mathrm{B}]_0$") | |
| keq = mo.ui.text(value="12",label="$K_{eq}$") | |
| step = mo.ui.slider(steps=np.logspace(-8,0,90),label="$\delta c$",show_value=True) | |
| tol = mo.ui.slider(steps=np.logspace(-8,0,90),label="Convergence Threshold",show_value=True) | |
| mo.md( | |
| f""" | |
| ##**Initial conditions** | |
| {conc_a} {conc_b} {keq}\n | |
| ##**Chemical Equilibrium Solver Parameters** | |
| {step} {tol} | |
| """ | |
| ) | |
| return Dict, NDArray, conc_a, conc_b, keq, mo, np, plt, step, tol | |
| def _(np): | |
| def compute_Q(conc,stoich): | |
| Q = 1 | |
| for c in conc: | |
| Q *= conc[c]**stoich[c] | |
| return Q | |
| def compute_force(conc,stoich,pkeq): | |
| Q = compute_Q(conc,stoich) | |
| return -np.log10(Q) - pkeq | |
| def update_concentrations(conc,stoich,force,dc): | |
| for c in conc: | |
| conc[c] += dc*stoich[c]*force | |
| return conc | |
| def solve_analytic(conc,keq): | |
| """ | |
| (b+x) / (a-2x)**2 = c | |
| """ | |
| a = conc["A"] | |
| b = conc["B"] | |
| c = keq | |
| x0 = (-np.sqrt(8*a*c + 16*b*c + 1) + 4*a*c + 1)/(8*c) | |
| x1 = (np.sqrt(8*a*c + 16*b*c + 1) + 4*a*c + 1)/(8*c) | |
| return x0,x1 | |
| return compute_Q, compute_force, solve_analytic, update_concentrations | |
| def _( | |
| Dict, | |
| NDArray, | |
| compute_Q, | |
| compute_force, | |
| conc_a, | |
| conc_b, | |
| keq, | |
| mo, | |
| np, | |
| plt, | |
| solve_analytic, | |
| step, | |
| tol, | |
| update_concentrations, | |
| ): | |
| conc = { | |
| "A": float(conc_a.value), | |
| "B": float(conc_b.value), | |
| } | |
| stoich = { | |
| "A":-2, | |
| "B":1, | |
| } | |
| pkeq = -np.log10(float(keq.value)) | |
| dc = float(step.value) | |
| rtol = float(tol.value) | |
| # print(conc,np.log10(compute_Q(conc,stoich)),pkeq) | |
| initial = mo.md( | |
| f""" | |
| ##**Initial conditions** | |
| $Q$ = {compute_Q(conc,stoich):.4e} | |
| Initial force = {compute_force(conc, stoich, pkeq):.4e} | |
| """ | |
| ) | |
| def solve_equilibrium( | |
| initial_conc: Dict[str, float], | |
| stoichiometry: Dict[str, float], | |
| pK_eq: float, | |
| dc: float, | |
| rtol: float = 1e-5, | |
| max_iterations: int = 10 | |
| ) -> NDArray: | |
| """ | |
| Solves chemical equilibrium equations using an iterative approach. | |
| Args: | |
| initial_conc: Dictionary of initial concentrations for each species | |
| stoichiometry: Dictionary of stoichiometric coefficients | |
| pK_eq: Negative log of equilibrium constant | |
| dc: Concentration step size for iterations | |
| rtol: Relative tolerance for convergence | |
| max_iterations: Maximum number of iterations before stopping | |
| Returns: | |
| NDArray: Array with columns [iteration, conc_A, conc_B, force] | |
| """ | |
| # Initialize arrays to store results | |
| iterations = np.zeros(max_iterations + 1) | |
| conc_A = np.zeros(max_iterations + 1) | |
| conc_B = np.zeros(max_iterations + 1) | |
| forces = np.zeros(max_iterations + 1) | |
| # Set initial values | |
| conc = initial_conc.copy() | |
| force_0 = compute_force(conc, stoichiometry, pK_eq) | |
| conc_A[0] = conc['A'] | |
| conc_B[0] = conc['B'] | |
| forces[0] = force_0 | |
| # Iterate until convergence or max iterations | |
| for i in range(max_iterations): | |
| # Update values | |
| conc = update_concentrations(conc, stoichiometry, forces[i], dc) | |
| force = compute_force(conc, stoichiometry, pK_eq) | |
| # if force*forces[i] < 0: | |
| # dc /=2 | |
| pQ = -np.log10(compute_Q(conc, stoichiometry)) | |
| # Store results | |
| iterations[i + 1] = i + 1 | |
| conc_A[i + 1] = conc['A'] | |
| conc_B[i + 1] = conc['B'] | |
| forces[i + 1] = force | |
| # Check convergence | |
| # if np.isclose(pQ, pK_eq, rtol=rtol): | |
| if np.abs(force) < rtol: | |
| # Trim unused array space if converged early | |
| return np.column_stack([ | |
| iterations[:i + 2], | |
| conc_A[:i + 2], | |
| conc_B[:i + 2], | |
| forces[:i + 2] | |
| ]) | |
| # Return all iterations if no convergence | |
| return np.column_stack([iterations, conc_A, conc_B, forces]) | |
| def plot(data,labels=None,refs=None,log=False,axes=None): | |
| ncols = data.shape[1] | |
| colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] | |
| plt.figure(figsize=(4,4)) | |
| for i in range(0,ncols-1): | |
| plt.plot(data[:,0],data[:,i+1],label=labels[i],color=colors[i]) | |
| if refs is not None: | |
| for i in range(len(refs)): | |
| plt.axhline(refs[i],linestyle='dashed',label=labels[i]+"$_{exact}$",color=colors[i]) | |
| if axes is not None: | |
| plt.xlabel(axes[0]) | |
| plt.ylabel(axes[1]) | |
| if log: | |
| plt.yscale("log") | |
| plt.legend() | |
| return plt.gca() | |
| data = solve_equilibrium(conc,stoich,pkeq,dc,rtol,max_iterations=1000) | |
| final_conc = {"A":data[-1,1] , "B":data[-1,2]} | |
| roots = solve_analytic(conc,float(keq.value)) | |
| analytic_solution = [ data[0,1] + stoich["A"]*roots[0] , data[0,2] + stoich["B"]*roots[0] ] | |
| plot_c = plot(data[:,0:3],labels=["[A]","[B]"],refs=analytic_solution,axes=["Cycles","Concentration"]) | |
| plot_f = plot( | |
| np.column_stack([data[:,0],np.abs(data[:,3])]), | |
| labels=["Force"],refs=[rtol],log=True,axes=["Cycles","Force"]) | |
| final = mo.md( | |
| f""" | |
| ##**Final conditions** | |
| $[A]_f$ = {final_conc["A"]:.4e} | |
| $[B]_f$ = {final_conc["B"]:.4e} | |
| $Q$ = {compute_Q(final_conc,stoich):.4e} | |
| $K_{{eq}}$ = {float(keq.value):.4e} \n | |
| Final force = {compute_force(final_conc,stoich,pkeq):.4e} | |
| Force threshold = {float(tol.value):.4e} | |
| """ | |
| ) | |
| mo.vstack([initial,final, | |
| mo.hstack([plot_c,plot_f]) | |
| ]) | |
| return ( | |
| analytic_solution, | |
| conc, | |
| data, | |
| dc, | |
| final, | |
| final_conc, | |
| initial, | |
| pkeq, | |
| plot, | |
| plot_c, | |
| plot_f, | |
| roots, | |
| rtol, | |
| solve_equilibrium, | |
| stoich, | |
| ) | |
| if __name__ == "__main__": | |
| app.run() | |