import pandas as pd
from tqdm import tqdm, notebook
import numpy as np
import scipy
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import kaleido
from IPython.display import Image
tqdm_disabled = True # True for website, change to False for local work
electrolyte = "iron"
sampling = True
MIN_POINTS0 = 500
DIFF_LIMIT = 0.1
TOTAL_VOLUME = 11 # electrolyte volume in mL, approx, measured by taking as much electrolyte as possible up into a 12 mL syringe
MASS_TO_RESERVOIRS = 14.62 # g of electrolyte actually loaded into system, based on weighing syringe before/after loading reservoirs
density = MASS_TO_RESERVOIRS / TOTAL_VOLUME
# molecular weights in g/mol
# electrolyte component masses, in g
### ALL-IRON ###
MASS_FeCl2_2H2O = 1.60
MASS_MgCl2 = 4.30
MASS_AscorbicAcid = 0.06 # wasn't able to properly record it
MASS_HCl_15pct = 3*.05 # estimation of 3 drops of 15% wt. solution
MASS_H2O = 9.50
total_mass_kg = (MASS_FeCl2_2H2O + MASS_MgCl2 + MASS_AscorbicAcid + MASS_HCl_15pct + MASS_H2O) / 1000.0
MW_FeCl2_2H2O = 162.78
MW_MgCl2 = 95.211
MW_AscorbicAcid = 176.124
MW_HCl = 36.46
MW_H2O = 18.01528
molality_FeCl2 = MASS_FeCl2_2H2O / MW_FeCl2_2H2O / total_mass_kg
molality_MgCl2 = MASS_MgCl2 / MW_MgCl2 / total_mass_kg
molality_AscorbicAcid = MASS_AscorbicAcid / MW_AscorbicAcid / total_mass_kg
molality_HCl = MASS_HCl_15pct*0.15 / MW_HCl / total_mass_kg
molality_H2O = (MASS_H2O + 0.85*MASS_HCl_15pct) / MW_H2O / total_mass_kg
molarity_FeCl2 = MASS_FeCl2_2H2O / MW_FeCl2_2H2O / TOTAL_VOLUME * 1000.0
molarity_MgCl2 = MASS_MgCl2 / MW_MgCl2 / TOTAL_VOLUME * 1000.0
molarity_AscorbicAcid = MASS_AscorbicAcid / MW_AscorbicAcid / TOTAL_VOLUME * 1000.0
molarity_HCl = MASS_HCl_15pct*0.15 / MW_HCl / TOTAL_VOLUME * 1000.0
molarity_H2O = (MASS_H2O + 0.85*MASS_HCl_15pct) / MW_H2O / TOTAL_VOLUME * 1000.0
### ZINC-IODIDE ###
# MASS_ZnCl2 = 1.37
# MASS_NH4Cl = 1.06
# MASS_KI = 3.31
# MASS_H2O = 8.52
# MASS_TriEG = 0.63
# total_mass_kg = (MASS_ZnCl2 + MASS_KI + MASS_H2O + MASS_TriEG) / 1000.0
# MW_ZnCl2 = 136.315
# MW_NH4Cl = 53.49
# MW_KI = 166.0028
# MW_H2O = 18.01528
# MW_TriEG = 150.174
# molality_ZnCl2 = MASS_ZnCl2 / MW_ZnCl2 / total_mass_kg
# molality_NH4Cl = MASS_NH4Cl / MW_NH4Cl / total_mass_kg
# molality_KI = MASS_KI / MW_KI / total_mass_kg
# molality_TriEG = MASS_TriEG / MW_TriEG / total_mass_kg
# molality_H2O = MASS_H2O / MW_H2O / total_mass_kg
# molarity_ZnCl2 = MASS_ZnCl2 / MW_ZnCl2 / TOTAL_VOLUME * 1000.0
# molarity_NH4Cl = MASS_NH4Cl / MW_NH4Cl / TOTAL_VOLUME * 1000.0
# molarity_KI = MASS_KI / MW_KI / TOTAL_VOLUME * 1000.0
# molarity_TriEG = MASS_TriEG / MW_TriEG / TOTAL_VOLUME * 1000.0
# molarity_H2O = MASS_H2O / MW_H2O / TOTAL_VOLUME * 1000.0
filenames = [
"20-05-2026-KPS-25.zip",
"21-05-2026-KPS-26.zip"
]
all_data = []
for f in filenames:
if len(all_data) == 0:
all_data.append(pd.read_csv(f, delimiter="\t").dropna())
else:
df0 = pd.read_csv(f, delimiter="\t").dropna()
df0["Elapsed time(s)"] += all_data[-1]["Elapsed time(s)"].iat[-1]
all_data.append(df0)
df = pd.concat(all_data, ignore_index=True)
if not tqdm_disabled:
print("Electrolyte Composition:")
if electrolyte=="zinc":
print(
"Molarities (moles/L solution): {:.2f} M ZnCl~2~, {:.2f} M NH~4~Cl, {:.2f} M KI, {:.2f} M triethylene glycol, {:.2f} M H~2~O\n".format(
molarity_ZnCl2, molarity_NH4Cl, molarity_KI, molarity_TriEG, molarity_H2O
)
)
print(
"Molalities (moles/kg solution): {:.2f} m ZnCl~2~, {:.2f} m NH~4~Cl, {:.2f} m KI, {:.2f} m triethylene glycol, {:.2f} m H~2~O\n".format(
molality_ZnCl2, molality_NH4Cl, molality_KI, molality_TriEG, molality_H2O
)
)
elif electrolyte=="iron":
print(
"Molarities (moles/L solution): {:.2f} M FeCl~2~, {:.2f} M MgCl~2~, {:.2f} M ascorbic acid, {:.2f} M HCl, {:.2f} M H~2~O\n".format(
molarity_FeCl2, molarity_MgCl2, molarity_AscorbicAcid, molarity_HCl, molarity_H2O
)
)
print(
"Molalities (moles/kg solution): {:.2f} m FeCl~2~, {:.2f} m MgCl~2~, {:.2f} m ascorbic acid, {:.2f} m HCl, {:.2f} m H~2~O\n".format(
molality_FeCl2, molarity_MgCl2, molarity_AscorbicAcid, molarity_HCl, molarity_H2O
)
)
else:
print("WARNING: Specify the type of electrolyte")
print("Density approx. {:.1f} g/mL\n".format(density))
print(
"Experiment length: {:.1f} hours".format(
all_data[-1]["Elapsed time(s)"].iat[-1] / 3600.0
)
)
df["mean_current"] = df["Current(A)"].rolling(3).mean()
df["prev_current"] = df["mean_current"].shift(-1)
df["VChange"] = df["Potential(V)"].diff().abs()
df["is_OCP"] = (abs(df["mean_current"])<0.0001).astype(int)
df["is_change"] = (
(((df["mean_current"] > 0) & (df["prev_current"] < 0))
| ((df["mean_current"] < 0) & (df["prev_current"] > 0)))
# & df["is_OCP"]==False #current must be greater than 0.5 mA to avoid OCP holds
).astype(int)
df["is_start_charge"] = ((df["prev_current"] < -0.001) & (df["is_OCP"]== 1)).astype(int)
df["is_start_discharge"] = ((df["prev_current"] > 0.001) & (df["is_OCP"]== 1)).astype(int)
idx_changes = list(df[(df["is_start_charge"] == 1) | (df["is_start_discharge"] == 1)].index)
idx_changes.append(len(df) - 1)
all_curves = []
idx_start = 0
for idx in tqdm(idx_changes, disable=tqdm_disabled):
if len(df.iloc[idx_start:idx, :]) > 50:
all_curves.append(df.iloc[idx_start:idx, :])
idx_start = idx
results = []
n_curves = np.max([1, int(np.floor(len(all_curves) / 2))])
n_curves = 150
for CN in notebook.tnrange(n_curves, disable=tqdm_disabled):
CURVE_N1 = CN * 2
CURVE_N2 = CN * 2 + 1
# Process charge data
if sampling:
N_TERM_POINTS = int(np.min([MIN_POINTS0, len(all_curves[CURVE_N1]) / 2.0]))
MIN_POINTS = int(
np.min([MIN_POINTS0, len(all_curves[CURVE_N1]) - N_TERM_POINTS * 2])
)
df0 = pd.concat(
[
all_curves[CURVE_N1].iloc[:N_TERM_POINTS],
all_curves[CURVE_N1]
.iloc[N_TERM_POINTS:-N_TERM_POINTS]
.sample(n=MIN_POINTS),
all_curves[CURVE_N1].iloc[-N_TERM_POINTS:],
]
).sort_values("Elapsed time(s)", ascending=True)
df0 = df0[df0["VChange"] < DIFF_LIMIT]
else:
df0 = all_curves[CURVE_N1].copy()
df0["mAh"] = np.abs(
scipy.integrate.cumulative_trapezoid(
df0["Current(A)"], df0["Elapsed time(s)"], initial=0
)
* 1000.0
/ 3600.0
)
total_energy0 = scipy.integrate.cumulative_trapezoid(
df0["Current(A)"].abs() * df0["Potential(V)"],
df0["Elapsed time(s)"],
initial=0.0,
)[-1]
# Process discharge data
if sampling:
N_TERM_POINTS = int(np.min([MIN_POINTS0, len(all_curves[CURVE_N2]) / 2.0]))
MIN_POINTS = int(
np.min([MIN_POINTS0, len(all_curves[CURVE_N2]) - N_TERM_POINTS * 2])
)
df1 = pd.concat(
[
all_curves[CURVE_N2].iloc[:N_TERM_POINTS],
all_curves[CURVE_N2]
.iloc[N_TERM_POINTS:-N_TERM_POINTS]
.sample(n=MIN_POINTS),
all_curves[CURVE_N2].iloc[-N_TERM_POINTS:],
]
).sort_values("Elapsed time(s)", ascending=True)
df1 = df1[df1["VChange"] < DIFF_LIMIT]
else:
df1 = all_curves[CURVE_N2].copy()
df1["mAh"] = np.abs(
scipy.integrate.cumulative_trapezoid(
df1["Current(A)"], df1["Elapsed time(s)"], initial=0.0
)
* 1000.0
/ 3600.0
)
total_energy1 = scipy.integrate.cumulative_trapezoid(
df1["Current(A)"].abs() * df1["Potential(V)"],
df1["Elapsed time(s)"],
initial=0.0,
)[-1]
CE = 100.0 * (df1["mAh"].iloc[-1] / df0["mAh"].iloc[-1])
EE = 100.0 * (total_energy1 / total_energy0)
VE = 100.0 * EE / CE
results.append(
{
"Number": CN + 1,
"CE": CE,
"VE": VE,
"EE": EE,
"Charge_potential": df0["Potential(V)"].mean(),
"Discharge_potential": df1["Potential(V)"].mean(),
"Charge_stored": df1["mAh"].iloc[-1] / TOTAL_VOLUME,
"Energy_density_discharge": total_energy1 / TOTAL_VOLUME / 3600.0 * 1000,
}
)
# Save the modified DataFrames back to the all_curves list
all_curves[CURVE_N1] = df0
all_curves[CURVE_N2] = df1
results_df = pd.DataFrame(results)
if not tqdm_disabled:
print(results_df)
print("")
print(results_df.mean())