Cell Grading and Binning with TofuPilot
A battery pack is only as good as its weakest cell. Cell grading sorts production cells by measured performance so you can build packs from matched cells. TofuPilot stores every cell's test data and lets you query, filter, and grade cells based on actual measurements.
Why Cell Grading Matters
Cells from the same production line have natural variation. Capacity varies by 2-5%. Impedance varies by 10-20%. If you build a pack from unmatched cells:
- The weakest cell limits pack capacity
- Cells age at different rates, causing imbalance
- BMS has to work harder to keep cells balanced
- Pack lifetime is shorter than it needs to be
Grading cells before assembly ensures packs perform consistently and last longer.
Grading Criteria
| Parameter | How it's measured | Why it matters |
|---|---|---|
| Capacity (Ah) | Full charge/discharge cycle | Determines pack energy |
| Internal impedance (mohm) | AC impedance at 1kHz | Affects power delivery and heat |
| Open-circuit voltage (V) | Rest voltage after formation | Indicates state of charge consistency |
| Self-discharge rate (mV/day) | Voltage drop over 7-14 day rest | Catches internal micro-shorts |
| Weight (g) | Scale measurement | Catches underfilled or overfilled cells |
Setting Up Cell Grading in TofuPilot
Step 1: Define the Grading Test Procedure
from tofupilot import TofuPilotClient
client = TofuPilotClient()
def grade_cell(serial, capacity_ah, impedance_mohm, ocv_v, self_discharge_mv_day, weight_g):
# Determine grade
if capacity_ah >= 3.1 and impedance_mohm <= 30 and self_discharge_mv_day <= 0.5:
grade = "A"
passed = True
elif capacity_ah >= 2.9 and impedance_mohm <= 40 and self_discharge_mv_day <= 1.0:
grade = "B"
passed = True
elif capacity_ah >= 2.7 and impedance_mohm <= 50:
grade = "C"
passed = True
else:
grade = "REJECT"
passed = False
client.create_run(
procedure_id="CELL-GRADING",
unit_under_test={
"serial_number": serial,
"part_number": "CELL-21700-NMC",
},
run_passed=passed,
steps=[{
"name": "Capacity Test",
"step_type": "measurement",
"status": capacity_ah >= 2.7,
"measurements": [
{"name": "capacity_ah", "value": capacity_ah, "unit": "Ah", "limit_low": 2.7},
],
}, {
"name": "Impedance Test",
"step_type": "measurement",
"status": impedance_mohm <= 50,
"measurements": [
{"name": "impedance_mohm", "value": impedance_mohm, "unit": "mohm", "limit_high": 50},
],
}, {
"name": "Self-Discharge",
"step_type": "measurement",
"status": self_discharge_mv_day <= 1.0,
"measurements": [
{"name": "self_discharge_mv_day", "value": self_discharge_mv_day, "unit": "mV/day", "limit_high": 1.0},
{"name": "ocv_v", "value": ocv_v, "unit": "V", "limit_low": 3.5, "limit_high": 4.2},
],
}, {
"name": "Physical",
"step_type": "measurement",
"status": 68.0 <= weight_g <= 72.0,
"measurements": [
{"name": "weight_g", "value": weight_g, "unit": "g", "limit_low": 68.0, "limit_high": 72.0},
{"name": "grade", "value": grade, "unit": ""},
],
}],
)
return gradeStep 2: Analyze Grade Distribution
After grading a production batch, TofuPilot's dashboard shows the distribution:
| Grade | Count | Percentage | Capacity range | Impedance range |
|---|---|---|---|---|
| A | 850 | 85% | 3.10-3.25 Ah | 22-30 mohm |
| B | 120 | 12% | 2.90-3.09 Ah | 31-40 mohm |
| C | 20 | 2% | 2.70-2.89 Ah | 41-50 mohm |
| REJECT | 10 | 1% | < 2.70 Ah | > 50 mohm |
Track this distribution over time. If Grade A yield drops from 85% to 75%, the manufacturing process is drifting.
Step 3: Pack Assembly with Matched Cells
Query TofuPilot for Grade A cells with capacity within a tight window for pack assembly.
from tofupilot import TofuPilotClient
client = TofuPilotClient()
# Get all Grade A cells from the latest batch
runs = client.get_runs(
procedure_id="CELL-GRADING",
run_passed=True,
limit=1000,
)
# Filter for Grade A cells within a 50mAh capacity window
grade_a_cells = []
for run in runs:
for step in run.get("steps", []):
for m in step.get("measurements", []):
if m["name"] == "capacity_ah" and m["value"] >= 3.1:
grade_a_cells.append({
"serial": run["unit_under_test"]["serial_number"],
"capacity": m["value"],
})
# Sort by capacity and group into matched sets
grade_a_cells.sort(key=lambda c: c["capacity"])
# Create packs of 12 cells with capacity spread < 50mAh
pack_size = 12
for i in range(0, len(grade_a_cells) - pack_size + 1, pack_size):
pack = grade_a_cells[i:i + pack_size]
spread = pack[-1]["capacity"] - pack[0]["capacity"]
if spread <= 0.05:
print(f"Pack: {[c['serial'] for c in pack]}, spread: {spread*1000:.0f} mAh")Self-Discharge Screening
Self-discharge is the most important safety screen. Cells with elevated self-discharge rates may have internal micro-shorts that can lead to thermal events.
The test requires a long rest period (7-14 days), which makes it the bottleneck in cell production. Track self-discharge data in TofuPilot to:
- Set data-driven pass/fail thresholds based on production distributions
- Identify cells that need extended screening
- Correlate self-discharge with other parameters (formation data, impedance)
Traceability from Cell to Pack
When a pack fails in the field, trace back to the individual cells:
- Look up the pack serial in TofuPilot
- Find which cells were assembled into that pack
- Pull each cell's grading data
- Check if any cell was marginal at grading time
This traceability is required by most automotive OEMs and is becoming standard across the battery industry.