feat: add charge_profile for SoC-dependent charge power limits#97
feat: add charge_profile for SoC-dependent charge power limits#971va13n7in wants to merge 3 commits into
Conversation
Add CC-CV taper modeling to the optimizer. Batteries can now specify a charge_profile (e.g. lifepo4_conservative) that limits charge power as a function of SoC, matching real-world BMS behavior. The MILP solver uses iterative constraint tightening: solve, extract SoC trajectory, compute per-slot P_max(SoC) from the profile, add upper bound constraints, re-solve until convergence (max 5 iterations). New API fields in BatteryConfig: - charge_profile: named profile (e.g. lifepo4_conservative) - charge_knee: SoC% where taper begins (override) - charge_k: exponential decay constant (override) - charge_c_rate_float: minimum C-rate at float (override) New module: charging_profiles.py with profile definitions and max_charge_power() calculation.
|
Imho we had discussed that this is not worth the cost/benefit ratio? |
|
The cost is 4 optional API fields and ~65 lines of tightening logic. The benefit: without it, the optimizer plans charge power above 80% SoC that the BMS will reject. The schedule becomes infeasible in the last 20-30%. With charge_profile, the schedule matches what the battery actually accepts. This is also a prerequisite for solar-first (#98) to produce realistic results. If the concern is about parameter complexity for end users: the only required field is charge_profile (a single string, e.g. "lifepo4_conservative"). The three override fields are optional and only relevant for custom setups. |
|
Some notes on the solver side of the iterative tightening. Cost when unused is zero — Cost when used is 3–6 full MILP solves instead of 1. The loop only exits early on Worth noting The model grows monotonically. Constraint names carry The part I would flag beyond performance: constraints are never relaxed. A bound derived from iteration 1's SoC trajectory keeps binding even after later iterations produce a lower SoC at that slot, so the result can end up over-tightened and suboptimal with no signal that it happened. With hard Two directions that avoid the loop entirely:
If the iterative approach stays, rebuilding the 🤖 Generated with Claude Code |
I disagree. The cost is added complexity and x times solver cost.
i disagree. The schedule becomes inaccurate, but so is the MILP solution in any case. I don't see any indication that this added complexity really pays off. |
Replace the iterative constraint tightening (3-6 re-solves) with piecewise-linear upper bounds added at model build time. The CC-CV taper curve is convex, so tangent lines at 4 breakpoints (knee to 100% SoC) provide valid linear constraints on charge power as a function of s[i][t-1]. One solve, no stale bounds, bounded wall clock.
|
Valid points, all of them. The stale-bound issue and the unbounded Reworked to use static tangent-line constraints at build time. The Pushed the fix to this branch. |
Add CC-CV taper modeling to the optimizer. Batteries can now specify a charge_profile (e.g. lifepo4_conservative) that limits charge power as a function of SoC, matching real-world BMS behavior.
The MILP solver uses iterative constraint tightening: solve, extract SoC trajectory, compute per-slot P_max(SoC) from the profile, add upper bound constraints, re-solve until convergence (max 5 iterations).
New API fields in BatteryConfig:
New module: charging_profiles.py with profile definitions and max_charge_power() calculation.
Part of evcc-io/evcc#31963