Lab Notebook Entry #13

The voltaic bulge of Zn-I appears
lab notebook
research
flow batteries
doi
Author

Kirk Pollard Smith

Published

April 3, 2026

Continuing from Lab Notebook Entry 12. Another file to add to this long test.

The test has continued for a couple more days—now cumulatively over 200 hours total of cycling, plus several (20+?) hours of stoppage, due to the power cuts.

Some interesting degradation has happened in the last few cycles. A bulge on the charging curve appears in the last few cycles, starting late in the cycle and gradually moving earlier. It is clearly degraded and hit 80% capacity so I stopped the test because I need the cell/channel back!

You can see this in the final few cycles of Figure 1

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
from plotly.subplots import make_subplots
import kaleido
from IPython.display import Image

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

sampling = True

MIN_POINTS0 = 500
DIFF_LIMIT = 0.1


# electrolyte component masses, in g
MASS_ZnCl2 = 1.40
MASS_NH4Cl = 1.08
MASS_KI = 3.32
MASS_H2O = 8.51
MASS_TriEG = 0.58

total_mass_kg = (MASS_ZnCl2 + MASS_KI + MASS_H2O + MASS_TriEG) / 1000.0

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.75  # g of electrolyte actually loaded into system, based on weighing syringe before/after loading reservoirs
# molecular weights in g/mol

density = MASS_TO_RESERVOIRS / TOTAL_VOLUME

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 = [
    "../lab-notebook-9/23-03-2026-KPS-5.zip",
    "../lab-notebook-11/23-03-2026-KPS-7.zip",
    "../lab-notebook-12/29-03-2026-KPS-8.zip",
    "../lab-notebook-12/29-03-2026-KPS-9.zip",
    "31-03-2026-KPS-10.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:")

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


# Color gradient for charge curves
charge_colors = [
    px.colors.sequential.Blues[int(i)]
    for i in np.linspace(3, len(px.colors.sequential.Blues) - 1, n_curves)
]
discharge_colors = [
    px.colors.sequential.Greys[int(i)]
    for i in np.linspace(3, len(px.colors.sequential.Greys) - 1, n_curves)
]


# 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=charge_colors[CN], dash="solid"),
            showlegend=False,
        )
    )
    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=discharge_colors[CN], dash="solid"),
            showlegend=False,
        )
    )
fig1.update_layout(
    xaxis_title="Capacity (Ah/L)",
    yaxis_title="Potential (V)",
    legend=dict(orientation="h", yanchor="bottom", y=0.02, xanchor="right", x=0.99),
    hoverlabel=dict(
        bgcolor="white",
    ),
    xaxis=dict(range=[-1, 10]),
    yaxis=dict(range=[-.49, 1.8]),


)

fig1.add_trace(
    go.Scatter(
        x=[None],
        y=[None],
        mode="lines",
        line=dict(color=charge_colors[0], dash="solid"),
        name="Charge (cycle 1)",
    )
)
fig1.add_trace(
    go.Scatter(
        x=[None],
        y=[None],
        mode="lines",
        line=dict(color=charge_colors[-1], dash="solid"),
        name=f"Charge (cycle {n_curves})",
    )
)
fig1.add_trace(
    go.Scatter(
        x=[None],
        y=[None],
        mode="lines",
        line=dict(color=discharge_colors[0], dash="solid"),
        name="Discharge (cycle 1)",
    )
)
fig1.add_trace(
    go.Scatter(
        x=[None],
        y=[None],
        mode="lines",
        line=dict(color=discharge_colors[-1], dash="solid"),
        name=f"Discharge (cycle {n_curves})",
    )
)

fig1.show()
Electrolyte Composition:
Molarities (moles/L solution): 0.93 M ZnCl~2~, 1.84 M NH~4~Cl, 1.82 M KI, 0.35 M triethylene glycol, 42.94 M H~2~O

Molalities (moles/kg solution): 0.74 m ZnCl~2~, 1.46 m NH~4~Cl, 1.45 m KI, 0.28 m triethylene glycol, 34.21 m H~2~O

Density approx. 1.3 g/mL

Experiment length: 204.1 hours

  0%|          | 0/94 [00:00<?, ?it/s]
100%|██████████| 94/94 [00:00<00:00, 4513.26it/s]
    Number         CE         VE         EE  Charge_potential  \
0        1  74.482785  91.514740  68.162728          1.219876   
1        2  76.057640  91.287835  69.431373          1.337808   
2        3  80.398304  91.041153  73.195543          1.310536   
3        4  82.489012  90.817625  74.914562          1.330686   
4        5  83.504995  90.819266  75.838624          1.334909   
5        6  78.649689  91.369703  71.861987          1.307750   
6        7  76.014861  91.975045  69.914702          1.078977   
7        8  79.179486  91.789044  72.678093          1.071497   
8        9  81.089259  91.543128  74.231645          1.179064   
9       10  81.718298  91.490315  74.764328          1.200599   
10      11  78.985390  91.602210  72.352362          1.202085   
11      12  75.600501  92.261102  69.749855          0.952621   
12      13  78.416923  92.047430  72.180763          0.953425   
13      14  80.291292  91.727367  73.649088          1.092331   
14      15  80.818271  91.640164  74.061997          1.167660   
15      16  81.209914  91.368872  74.200582          1.201080   
16      17  73.750216  93.347814  68.844215          1.092778   
17      18  75.419887  92.269942  69.589886          0.963043   
18      19  78.502418  91.934127  72.170512          0.965378   
19      20  80.423453  91.586542  73.657060          1.084120   
20      21  81.209333  91.396678  74.222633          1.197147   
21      22  79.734594  91.584608  73.024615          1.177615   
22      23  78.839839  91.827309  72.396502          1.070984   
23      24  79.687233  91.746605  73.110331          1.082747   
24      25  45.468588  93.163917  42.360318          1.139180   
25      26  72.167256  91.615293  66.116244          0.981077   
26      27  73.821036  93.185771  68.790702          0.961848   
27      28  78.112220  92.213360  72.029902          0.977552   
28      29  80.177918  91.750840  73.563913          1.013357   
29      30  76.949255  92.164732  70.920075          1.170159   
30      31  75.881578  91.230287  69.226981          1.406522   
31      32  76.710080  91.232448  69.984484          1.061643   
32      33  80.406593  90.611889  72.857933          1.279086   
33      34  82.209905  90.169895  74.128585          1.366272   
34      35  83.296498  89.838526  74.832346          1.396354   
35      36  79.393958  90.863701  72.140289          1.392455   
36      37  75.747611  90.933336  68.879830          1.230943   
37      38  78.178643  89.780077  70.188846          1.285283   
38      39  79.723063  89.075561  71.013765          1.376051   
39      40  81.389585  89.521364  72.861066          1.434518   
40      41  82.436043  90.149658  74.315811          1.459566   
41      42  80.131736  90.795532  72.756036          1.437257   
42      43  75.772247  90.114285  68.281619          1.451073   
43      44  76.073144  89.501879  68.086893          1.455988   
44      45  65.730510  88.713114  58.311582          1.494345   
45      46  77.303978  89.152376  68.918334          1.495257   

    Discharge_potential  Charge_stored  Energy_density_discharge  
0              1.090403       6.774124                  8.408325  
1              1.039399       6.916363                  8.581756  
2              1.002273       7.310162                  9.069552  
3              1.022430       7.499779                  9.293009  
4              0.960267       7.592855                  9.406033  
5              0.857036       7.150641                  8.894655  
6              0.928136       6.911126                  8.619261  
7              0.969761       7.199492                  8.973811  
8              1.000690       7.372062                  9.189018  
9              0.991483       7.429035                  9.261136  
10             0.857750       7.180639                  8.961631  
11             0.855881       6.872906                  8.589460  
12             0.918868       7.129086                  8.913703  
13             1.011342       7.299398                  9.138276  
14             1.036508       7.347255                  9.205803  
15             0.961993       7.382736                  9.237786  
16             1.263445       6.704726                  8.538774  
17             0.861824       6.860035                  8.632968  
18             0.864010       7.136807                  8.978421  
19             0.941096       7.311341                  9.204148  
20             0.936329       7.382838                  9.298572  
21             0.865257       7.248738                  9.148923  
22             0.914972       7.167454                  9.063182  
23             0.899728       7.244397                  9.170445  
24             1.291594       4.133608                  5.327436  
25             0.874352       6.562902                  8.253644  
26             0.875791       6.711673                  8.548177  
27             0.878835       7.101303                  9.076712  
28             0.890311       7.289004                  9.313183  
29             1.292476       6.995557                  9.011091  
30             0.884403       6.898714                  8.866026  
31             0.906657       6.973890                  8.940676  
32             0.940431       7.309825                  9.362325  
33             0.961175       7.473841                  9.557091  
34             0.916254       7.572610                  9.671710  
35             0.924614       7.218071                  9.322722  
36             0.902087       6.886407                  8.894104  
37             0.895082       7.107328                  9.117921  
38             0.893772       7.247741                  9.282203  
39             0.912384       7.399128                  9.581066  
40             0.922150       7.494292                  9.839085  
41             1.122366       7.284853                  9.659140  
42             1.160292       6.888560                  9.092748  
43             1.227836       6.915872                  9.095070  
44             1.144951       5.975619                  7.824207  
45             1.240542       6.519662                  8.614187  

Number                      23.500000
CE                          77.685979
VE                          91.212314
EE                          70.842816
Charge_potential             1.213924
Discharge_potential          0.980636
Charge_stored                7.051836
Energy_density_discharge     8.957156
dtype: float64
(a) Charge/discharge curves
(b)
Figure 1

This bulge is usually interpreted to be of solid iodine formation—which we want to avoid. We shouldn’t have charged to high enough SOC to form solid iodine (when there is too much \(\ce{I3-}\)), and we also have the triethylene glycol which is supposed to complex it.

It’s also worth noting there was a small leak from one reservoir barb that seemed to have stopped early on but could have affected the results.

Talking with Daniel helped the following interpretation.

If there is too much \(\ce{I3-}\), it can mean the positive electrolyte is not being fully discharged back completely into \(\ce{I-}\) each cycle, which can lead to a buildup of \(\ce{I3-}\) and then \(\ce{I2}\). This can happen if not all the zinc is stripped each cycle, like if there are spots of “dead Zn” where the Zn separates from the current collector and is no longer electrochemically active and accessible.

It can also happen as iodide gets oxidized by ambient oxygen in the system—a process that also increases the pH and can lead to passivation of zinc.

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

There is another reaction, however, that we have conveniently been ignoring up till now:

This is because the reaction is sufficiently slow as to not noticeably influence our testing on our usual timescales, on the order of a couple of days. Perhaps it becomes a problem after 200+ hours. We have been avoiding purging and blanketing with inert gas, because we are in very bare-bones labs and that costs money… but we do have the capability and my next test will have identical conditions and sparge/blanket with argon to investigate this hypothesis. It’s over 15 EUR/test at the small scale, because small argon cylinders are expensive.

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


fig2.update_layout(
    yaxis=dict(range=[-10, 100]),
    legend=dict(
        orientation="v",
        yanchor="bottom",
        y=0.02,
        xanchor="right",
        x=0.99
    )
)

fig2.add_annotation(
    x=25, y=45.46,
    text="Power Cut",
    showarrow=True,
    arrowhead=1
)


fig2.show()
Figure 2: Charge/discharge efficiencies

In Figure 3 we can see the cell hits 80% of its maximum capacity, n80, as the voltaic bulge hits. The cell could have probably gone a few more cycles but it was clear it was tanking.

Code
fig5 = px.scatter(
    results_df,
    x="Number",
    y="Energy_density_discharge",
    labels={
        "Energy_density_discharge": "Energy Density on Discharge (Wh/L)",
        "Number": "Cycle Number",
    },
    range_y=[0,1.2*max(results_df["Energy_density_discharge"])]
)

max_val = results_df["Energy_density_discharge"].max()
mean_val = results_df["Energy_density_discharge"].mean()

fig5.add_hline(y=max_val, line_dash="dash", line_color="black", annotation_text="Max", annotation_position="top right",annotation_font_size = 10)
fig5.add_hline(y=mean_val, line_dash="dash", line_color="blue", annotation_text="Mean", annotation_position="top right",annotation_font_size = 10)
fig5.add_hline(y=0.8*max_val, line_dash="dash", line_color="red", annotation_text="80% Max", annotation_position="top right",annotation_font_size = 10)

fig5.add_annotation(
    x=25, y=5.32,
    text="Power Cut",
    showarrow=True,
    arrowhead=1
)

fig5.show()
Figure 3: Discharge energy densities
Code
norm_charge = results_df["Charge_potential"] / results_df["Charge_potential"].iloc[0]
norm_discharge = results_df["Discharge_potential"] / results_df["Discharge_potential"].iloc[0]

fig6 = go.Figure()
fig6.add_trace(go.Scatter(
    x=results_df["Number"],
    y=norm_charge,
    mode="lines+markers",
    name="Charge Potential"
))
fig6.add_trace(go.Scatter(
    x=results_df["Number"],
    y=norm_discharge,
    mode="lines+markers",
    name="Discharge Potential"
))
fig6.update_layout(
    xaxis_title="Cycle Number",
    yaxis_title="Normalized Potential",
    legend=dict(
        orientation="v",
        yanchor="top",
        y=0.95,
        xanchor="left",
        x=0.05
    )
)

fig6.show()
Figure 4: Average charge and discharge potentials normalized to first cycle

I am removing the power cut cycle from the following analysis.

Code
table_df = (
    results_df.drop(24).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)",
        }
    )
)

# Bar chart of efficiencies with error bars
eff_cols = ["Coulombic Efficiency (%)", "Voltaic Efficiency (%)", "Energy Efficiency (%)"]
eff_labels = ["Coulombic", "Voltaic", "Energy"]

means = table_df.loc["mean", eff_cols].values
stds = table_df.loc["std", eff_cols].values
Code
fig6 = go.Figure()
fig6.add_trace(go.Bar(
    x=eff_labels,
    y=means,
    error_y=dict(type="data", array=stds, visible=True),
    name="Efficiency",
    marker_color=["#1f77b4", "#ff7f0e", "#2ca02c"]
))
fig6.update_layout(
    yaxis_title="Efficiency (%)",
    yaxis=dict(range=[0, 100])
)
fig6.show()
Figure 5: Mean efficiency values with standard deviation
Code
cap_cols = ["Discharge Capacity (Ah/L)"]
cap_labels = ["Discharge Capacity"]

cap_means = table_df.loc["mean", cap_cols].values
cap_stds = table_df.loc["std", cap_cols].values

ed_cols = ["Energy Density (Wh/L)"]
ed_labels = ["Energy Density"]

ed_means = table_df.loc["mean", ed_cols].values
ed_stds = table_df.loc["std", ed_cols].values

fig_combined = make_subplots(rows=1, cols=2)

fig_combined.add_trace(
    go.Bar(
        x=cap_labels,
        y=cap_means,
        error_y=dict(type="data", array=cap_stds, visible=True),
        name="Capacity",
        marker_color="#1f77b4"
    ),
    row=1, col=1
)

fig_combined.add_trace(
    go.Bar(
        x=ed_labels,
        y=ed_means,
        error_y=dict(type="data", array=ed_stds, visible=True),
        name="Energy Density",
        marker_color="#2ca02c"
    ),
    row=1, col=2
)
top = max(max(cap_means + cap_stds), max(ed_means + ed_stds))*1.1
fig_combined.update_layout(
    yaxis=dict(title="Discharge Capacity (Ah/L)"),
    yaxis2=dict(title="Energy Density (Wh/L)", anchor="x2", overlaying="y", side="left"),
    yaxis_range=[0, top],
    yaxis2_range=[0, top],
    showlegend=False
)
fig_combined.show()
Figure 6: Mean discharge capacity and energy density values with standard deviation

Well, those are the results for this test. I will repeat it soon in an identical fashion but with argon sparging/blanketing of the reservoirs to see if this changes the onset of the bulge. I also plan to increase the current density and charge capacity, to go to higher SOCs more quickly, and push towards more practical energy densities, but for now I want to only change one variable at a time.

Citation

BibTeX citation:
@online{smith2026,
  author = {Smith, Kirk Pollard},
  title = {Lab {Notebook} {Entry} \#13},
  date = {2026-04-03},
  url = {https://dualpower.supply/posts/lab-notebook-13/},
  langid = {en}
}
For attribution, please cite this work as:
K.P. Smith, Lab Notebook Entry #13, (2026). https://dualpower.supply/posts/lab-notebook-13/.