Progress update on development kit

flow batteries
kit
zinc-iodide
Moving to improved second version, zinc-iodide preliminary results, future chemistries, large-format cell design…
Published

January 6, 2025

Time for some updates on our progress since our last post!

Development kit

We have finished the initial version of the flow cell that we used at the workshop last April (which we are calling version zero) including documentation. This has been marked as release v0.0.2 (we are learning how to version hardware using Git, see below re. contribution guidelines!). It is a minimal working example, however, and the upcoming version of the kit performs much better. The upcoming version development occurs on this branch for the curious.

Limitations of the first iteration

Daniel put the first version of the kit through many extended cycling tests, and it was found to lose sealing performance over the timescale of months. We suspect this is due to creep causing reduced sealing pressure in the middle of the cell due to bending moments over long timescales.

Another issue with this version is the tight, interference fit between the copper plate and the plastic body. This was a known trade-off, because we wanted to avoid the requirement to source and machine holes in bipolar plate material (hard to source and expensive). With this approach we were able to use grafoil (easy to get and cheap) and keep the machining required to just polypropylene (also easy to get). The fit needed to be very precise, however, which created a manufacturing constraint.

Illustration of the problems of the first cell

Improved performance with integrated barbs

A redesign of the cell addressed these shortcomings. Because a barb design had been successfully FDM-printed in polypropylene, for the reservoirs, we adopted this into the flow frame. This also eliminates the needs for drilling and tapping threads, source barbs, and several potentially leaky interfaces. Additionally, this design does not require any close interference fits or precise alignments, making manufacturing easier.

FreeCAD visualization of new barbed flow frame design

This approach is similar to that adopted by the Nockemann Group and coworkers, with a key difference. In that paper, they struggled to get polypropylene FDM prints to seal properly, and opted for ABS. Our initial chemistry, zinc-iodide, is chemically incompatible with ABS when charged, so we need to use polypropylene. Large FDM polypropylene prints are, however, not rigid, dimensionally accurate, or leakproof enough to seal the cell well.

In our new design we have decoupled the chemical and mechanical requirements of the flow frame-endplate assembly. The polypropylene barbed flow frames print easily and accurately. An FDM-printable endplate, in a rigid, yet chemically incompatible material like PLA or PETG, can fit over the current collector and flow frame barbs, providing rigidity while never contacting the electrolyte.

In this way, we have reliably FDM-printed leakproof cells with polypropylene as the only wetted polymer material.

The new version of the cell with barbed fittings printed directly into the flow frames, and an FDM-printable endplate (in red) that fits over the flow frame

The cell is undergoing development and the first release will be marked as v1.0.0. Also, we plan to write a paper on this comparing the cell to the literature to establish how it is and isn’t different from previous approaches.

Preliminary results from first chemistry: zinc-iodide

Our first testing has been done with zinc-iodide chemistries. Why?

  • Easy to source, low-cost reagents (vs. vanadium, for example)
  • Compatible with cheap microporous membranes, such as paper
  • Resistant to dendrites
  • No detectable hydrogen evolution (unlike all-iron systems)
  • Acceptable energy density (>20Wh/L)
  • No strong acids or bases needed
  • Low toxicity (but don’t drink it!)

Here are the reactions taking place, for the battery as it discharges:

  • Negative Terminal (Anode): \(\ce{Zn_{(s)} -> Zn^2+ + 2e-}\)
  • Positive Terminal (Cathode): \(\ce{I3- + 2e- -> 3I- }\)
  • Overall: \(\ce{Zn_{(s)} + I3- -> Zn^2+ + 3I-}, E^\ominus = 1.3 V\)
  • Parasitic reaction: \(\ce{6I- + O2 + 2 H2O -> 2I3- + 4OH- }\)

Naming the battery terminals after their reactions on discharge is standard convention, even though this battery exists, after assembly, in the discharged state initially. Also, technically, the anode and cathode of the battery swap when the battery is charging vs. discharging, because the flow of electrons reverses causing the reduction and oxidation reactions to swap sides. We will usually just refer to the anode side for the zinc side—“the negative terminal”—and likewise for the cathode/positive terminal/iodide electrode.

NoteHybrid RFBs

Because metal plates/strips on the negative side in this setup, it is technically a “hybrid” flow battery, so the energy and power of the system are not fully decoupled. The reactor and systems engineering are similar, however, and the systems we develop will work for other traditional all-liquid chemistries with only minor modifications.

Preliminary results

Daniel has been running several tests to iterate towards a stable platform chemistry/cell configuration. Here is a representative test, complete with a failure of one peristaltic pump and what that looks like. This test didn’t have the right tubing material, and so some charged electrolyte leaked through the cathode pump causing corrosion on the pump shaft and pump failure, leading to a loss of flow and rapid performance decay. We present it here, because the initial performance was motivating, and because this is what research looks like!

Table 1: Test conditions
Electrolyte Composition 4M KI, 2M ZnCl2, 1 M NH4Cl in deionized H2O
Electrolyte Volume 5 mL (anolyte) + 5 mL (catholyte)
Cell Geometric Area 2 cm2
Separator 3 layers matte photopaper
Anode Configuration Grafoil + polypropylene felt (facing separator)
Cathode Configuration Grafoil + graphite felt + polypropylene felt (facing separator)
Current Density 30 mA/cm2
Charging Conditions To 25 Ah/L or 1.7 V
Discharging Conditions To 0 V
Flow Conditions Kamoer KHPP50 24 V peristaltic pumps at 100% duty cycle with 3x5 mm BPT tubing

Here is the raw data (62 MB) of the test from the open-source MYSTAT potentiostat we use, generated with the conditions shown in Table 1. There are three columns for charge/discharge data: time, voltage, and current.

Below are some plots we can generate by processing the raw data in order to determine cell performance. The code block is expandable if you’d like to view it or run it yourself.

Code
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
import kaleido
from IPython.display import Image


tqdm_disabled = True  # for website, change to False for local work

sampling = True

MIN_POINTS0 = 500
DIFF_LIMIT = 0.1

TOTAL_VOLUME = 10  # electrolyte volume in mL

filenames = ["Dec27_Ki4_NH4Cl2_nonCondFelt_3LayerPhoto_3mmFeltplusnoncond_2.csv"]

all_data = []
for f in filenames:
    if len(all_data) == 0:
        if "Potentiostat_project" in f:
            all_data.append(pd.read_csv(f))
        else:
            all_data.append(pd.read_csv(f, delimiter="\t"))
    else:
        df0 = pd.read_csv(f, delimiter="\t")
        df0["Elapsed time(s)"] += all_data[-1]["Elapsed time(s)"].iat[-1]
        all_data.append(df0)

df = pd.concat(all_data, ignore_index=True)

df["mean_current"] = df["Current(A)"].rolling(1).mean()
df["prev_current"] = df["mean_current"].shift(1)
df["VChange"] = df["Potential(V)"].diff().abs()
df["is_change"] = (
    ((df["mean_current"] > 0) & (df["prev_current"] < 0))
    | ((df["mean_current"] < 0) & (df["prev_current"] > 0))
).astype(int)

idx_changes = list(df[df["is_change"] == 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))])

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())

# Plot charge/discharge curves
fig1 = go.Figure()
for CN in range(n_curves):
    CURVE_N1 = CN * 2
    CURVE_N2 = CN * 2 + 1
    fig1.add_trace(
        go.Scatter(
            x=all_curves[CURVE_N1]["mAh"] / TOTAL_VOLUME,
            y=all_curves[CURVE_N1]["Potential(V)"],
            mode="lines",
            name=f"Charge {CN+1}",
            line=dict(color="blue", dash="solid"),
        )
    )
    fig1.add_trace(
        go.Scatter(
            x=all_curves[CURVE_N2]["mAh"] / TOTAL_VOLUME,
            y=all_curves[CURVE_N2]["Potential(V)"],
            mode="lines",
            name=f"Discharge {CN+1}",
            line=dict(color="grey", dash="solid"),
        )
    )
fig1.update_layout(
    title="Charge and Discharge Curves",
    xaxis_title="Capacity (Ah/L)",
    yaxis_title="Potential (V)",
)

fig1.show()

# fig1.write_image('fig1.pdf')
Figure 1: Charge/discharge curves of zinc-iodide chemistry showing pump failure during the discharge of the 8th cycle

Here in Figure 1 you can see representative charge/discharge curves for the first 7 cycles. In cycle 8, the cathode pump fails during discharge, and negatively impacts all subsequent cycling.

Code
# Plot efficiency
fig2 = px.scatter(
    results_df,
    x="Number",
    y=["CE", "VE", "EE"],
    labels={"value": "Efficiency (%)", "variable": "Metric"},
    title="Efficiency by Cycle",
)
fig2.update_traces(mode="lines+markers")
fig2.show()

# Plot potential
# fig3 = px.scatter(results_df, x="Number", y=["Charge_potential", "Discharge_potential"],
#                labels={"value": "Potential (V)", "variable": "Metric", "Number": "Cycle Number"},
#                title="Mean Potential by Cycle")
# fig3.show()
Figure 2: Charge/discharge efficiencies of zinc-iodide chemistry showing pump failure during the discharge of the 8th cycle

Now in Figure 2 we determine coulombic, energy, and voltaic efficiencies for the cycles.

Energy efficiency (\(EE\)) is how much energy we got out of the cell on discharge divided by how much went into charging it.

Coulombic efficiency (\(CE\)) is how much electrical charge (mAh, coulombs, electrons…) we got out of the cell on discharge divided by how much went into charging it. In this system, we have less than 100% coulombic efficiency because of self-discharge. Because we are using a porous, nonselective separator (paper) charged triiodide can pass through and recombine with zinc on the anode. This hurts our coulombic and energy efficiencies on each cycle but does not permanently degrade the battery.

Voltaic efficiency (\(VE\)) is akin to energy efficiency if we assume 100% coulombic efficiency; another way of looking at it is the average level of energy of electrons that entered the cell on charge vs. the average energy of electrons leaving on discharge, ignoring the total amount of electrons.

The relationship between the three efficiencies is straightforward: energy efficiency is the product of coulombic and voltaic efficiencies:

\[ EE = CE * VE \]

NotePumping energy

The energy efficiency calculated here ignores the pumps, which is conventional in the literature for this scale. A real flow battery uses centrifugal pumps to circulate liquid electrolyte; in a well-designed system this costs a few % of round-trip energy efficiency. Peristaltic and diaphragm pumps are much less efficient than centrifugal pumps but work better for small-scale benchtop testing.

Code
# Plot discharge charge capacity
# fig4 = px.scatter(results_df, x="Number", y="Charge_stored",
#                labels={"Charge_stored": "Discharge Capacity (Ah/L)", "Number": "Cycle Number"},
#                title="Discharge Capacity by Cycle")
# fig4.show()

# Plot discharge energy capacity
fig5 = px.scatter(
    results_df,
    x="Number",
    y="Energy_density_discharge",
    labels={
        "Energy_density_discharge": "Energy Density on Discharge (Wh/L)",
        "Number": "Cycle Number",
    },
    title="Energy Density by Cycle",
)
fig5.show()
Figure 3: Discharge energy densities of zinc-iodide chemistry showing pump failure during the discharge of the 8th cycle

Then, we can calculate the energy density accessible on discharge for each cycle.

Code
table_df = (
    results_df[:7]
    .describe()
    .loc[["mean", "std"]]
    .round(decimals=1)
    .drop(columns=["Number", "Charge_potential", "Discharge_potential"])
    .rename(
        columns={
            "CE": "Coulombic Efficiency (%)",
            "EE": "Energy Efficiency (%)",
            "VE": "Voltaic Efficiency (%)",
            "Charge_stored": "Discharge Capacity (Ah/L)",
            "Energy_density_discharge": "Energy Density (Wh/L)",
        }
    )
    .transpose()
)
table_df = (
    table_df["mean"].astype("str") + " ± " + table_df["std"].astype("str")
)
if not tqdm_disabled:
    print(table_df.to_markdown())
Table 2: Results from test, for the first 7 cycles before pump failure
Coulombic Efficiency (%) 86.2 ± 0.7
Voltaic Efficiency (%) 74.3 ± 0.7
Energy Efficiency (%) 64.0 ± 0.3
Discharge Capacity (Ah/L) 21.6 ± 0.2
Energy Density (Wh/L) 23.7 ± 0.1

The means and standard deviations of these metrics for the first 7 cycles are in Table 2.

Note

It’s important to note that we are using simple matte photopaper as our battery separator, a component that was absolutely not designed for this use. The conductivity and hydraulic properties of the separator are not ideal for this application, but it is cheap, available, and stable enough. If we switched to a selective ion-exchange membrane, we could improve our coulombic efficiency by over 13% and see gains in energy efficiency and discharge capacity/energy density.

We are still working on selecting the correct tubing and internal cell components for this system. An important component is the polypropylene felt spacer included on the anode side where zinc plates/strips. You can see a cross-section of this felt in the charged state, showing zinc plating into the felt from left to right.

Zinc anode polypropylene felt after being removed from the cell at 24 Ah/L. Grafoil current collector would be on the left, separator on the right.

We gave a short talk

Kirk presented the project at the annual French network meeting for RFB research, slides are here and below:

What’s next?

We plan to finalize this upcoming version of the development kit and begin starting on the design of the large-format cell. There are other chemistries to explore with the development kit, like an all-soluble iron-manganese system that Daniel has explored with voltammetry.

Other tasks coming up include:

  • computational design optimization of shunt current and pumping loss in large format cell/stack

  • design of power electronics system for centrifugal pump control

Please get in touch on the forum if you’d like to contribute on these issues!

Contribution guidelines

We’re trying to make it easier to contribute, for all the wonderful folks are reaching out and asking how to help. We have drafted some initial contribution guidelines which can be found here.

Comments

Citation

BibTeX citation:
@online{smith2025,
  author = {Smith, Kirk Pollard and Pinto, Daniel Fernandez},
  title = {Progress Update on Development Kit},
  date = {2025-01-06},
  url = {https://fbrc.dev/posts/progress-update-dev-kit/},
  doi = {10.5281/zenodo.14607250},
  langid = {en}
}
For attribution, please cite this work as:
Smith, Kirk Pollard, and Daniel Fernandez Pinto. 2025. “Progress Update on Development Kit.” January 6, 2025. https://doi.org/10.5281/zenodo.14607250.