Repository navigation
Conversation
23233ef to
a092e23
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5769 +/- ##
==========================================
- Coverage 98.10% 98.10% -0.01%
==========================================
Files 346 346
Lines 35482 35478 -4
==========================================
- Hits 34809 34805 -4
Misses 673 673 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
BradyPlanden
left a comment
There was a problem hiding this comment.
Thanks @mleot - this looks good. One suggestion to tighten the phase summing.
| electrode_param = getattr(self.param, domain[0]) | ||
| num_phases = int(getattr(self.options, domain)["particle phases"]) | ||
| if num_phases == 1: | ||
| return self._phase_heat_of_mixing( | ||
| variables, domain, "", electrode_param.prim | ||
| ) | ||
| return sum( | ||
| self._phase_heat_of_mixing(variables, domain, phase_name, phase_param) | ||
| for phase_name, phase_param in zip( | ||
| ["primary ", "secondary "], | ||
| [electrode_param.prim, electrode_param.sec], | ||
| strict=True, | ||
| ) | ||
| ) | ||
|
|
There was a problem hiding this comment.
suggestion: the below would avoid hard-coding the ['primary', 'secondary'] and [prim, sec] while removing the multi-line branching.
| electrode_param = getattr(self.param, domain[0]) | |
| num_phases = int(getattr(self.options, domain)["particle phases"]) | |
| if num_phases == 1: | |
| return self._phase_heat_of_mixing( | |
| variables, domain, "", electrode_param.prim | |
| ) | |
| return sum( | |
| self._phase_heat_of_mixing(variables, domain, phase_name, phase_param) | |
| for phase_name, phase_param in zip( | |
| ["primary ", "secondary "], | |
| [electrode_param.prim, electrode_param.sec], | |
| strict=True, | |
| ) | |
| ) | |
| phases = self.options.phases[domain] | |
| names = [""] if len(phases) == 1 else [f"{p} " for p in phases] | |
| return sum( | |
| self._phase_heat_of_mixing(variables, domain, n, electrode_param.phase_params[p]) | |
| for n, p in zip(names, phases, strict=True) | |
| ) | |
There was a problem hiding this comment.
Thanks @BradyPlanden, I think I addressed this in d469838. Now the phase names now come from options.phases, with "" for a one-phase electrode, so there's no branching for the phases now.
`"heat of mixing": "true"` could not be used with a composite electrode.
`BaseThermal._heat_of_mixing` read the single-phase variable names
("Negative particle radius [m]", "X-averaged negative particle
concentration [mol.m-3]") and took its material properties from
`param.n.prim` / `param.p.prim` alone, so any model built with
`"particle phases": "2"` failed at build time with
ModelError: Missing variable for submodel 'thermal':
'Negative particle radius [m]'.
Richardson's term is per-particle-population, so the fix is to evaluate it
per phase and add the contributions, which is what the reaction, reversible
and hysteresis heat sources in the same class already do. The phase loop
reuses the `phase_names = ["primary ", "secondary "]` idiom used there.
The single-phase path is untouched: it takes the `num_phases == 1` branch,
which builds the same expression from the same unprefixed variable names as
before. A DFN with Chen2020 and "heat of mixing": "true" returns voltage,
temperature and "Volume-averaged heat of mixing [W.m-3]" bit-identical to
the previous implementation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The composite heat-of-mixing tests only built the model. Extend the graphite/graphite heat-source comparison from pybamm-team#5770 so that splitting one graphite phase into two identical halves must also reproduce the one-phase "Volume-averaged heat of mixing [W.m-3]": - run it with "heat of mixing": "true", for isothermal (heat source only) and lumped thermal models, comparing the temperature rise for the latter - give Chen2020 the Ai2020 entropic changes; with zero dU/dT the reversible heat and the temperature dependence of U inside the heat of mixing were compared as zero against zero - solve at rtol 1e-6: at the default rtol the ~300 K temperature state drifts by ~0.03 K, which is 1% of the rise and failed SPM + lumped A heat of mixing that sums only the primary phase fails all four cases. Add a Si/graphite smoke test (Chen2020_composite, lumped) checking the heat of mixing stays positive and heats the cell. It supplies the per-layer "Negative electrode density [kg.m-3]" the set lacks and swaps the cubic Interpolant graphite OCP for the analytic Chen2020 fit, whose derivative the solver can convert. Also look up the electrode and phase parameters through param.domain_params and options.phases instead of getattr. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
a092e23 to
4ba0a9e
Compare
Parametrise the graphite/graphite heat-source comparison over each value of the "thermal" option, for SPM and DFN: - isothermal (heat source only) and lumped, on Chen2020 with the Ai2020 entropic changes, as before - x-full, which resolves the temperature in x, so the heat of mixing reads the local electrode temperature instead of the x-averaged one - x-lumped with 1+1D current collectors The pouch cases use Marquis2019, which ships the tab and heat transfer parameters, a non-zero entropic change and an analytic, temperature dependent particle diffusivity. A heat of mixing that sums only the primary phase fails all eight cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Composite models default to "surface form": "algebraic" while one-phase models default to "false", so the comparison was between two electrolyte formulations. Pin "algebraic" for both, which makes it like with like and lets SPMe join SPM and DFN: with the defaults SPMe differs by 1.4e-3 in voltage from the formulation change alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…xing pybamm had no check that its heat sources conserve energy. The reaction and hysteresis heats sum to a j (phi_s - phi_e - U_eq) at the particle surface, so the stored chemical energy is -F * int U_eq dc over the particles, and integrating by parts leaves exactly the heat of mixing taken with U_eq. The new test integrates a 1C SPM discharge and checks that the fall in that stored energy equals electrical work plus heat to 0.5% of the heat, for a one-phase cell and for a composite positive electrode whose second phase has current-sigmoid hysteresis with branches tilted away from its OCP. The SPM keeps the electrolyte uniform and an isothermal model with zero entropic change keeps temperature out of the stored energy, so the particles are the only store. Residuals are about -0.1%. The heat of mixing is ~45% of the heat, and two plausible mistakes fail: summing only the primary phase (+19%) and taking the heat of mixing from the hysteresis-weighted branch instead of U_eq (-1.4%). The heat-of-mixing docstring now says it must share U_eq with the hysteresis heat. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The heat of mixing is the dissipation of the particle flux, -N . grad(mu) = F D |grad c|^2 dU_eq/dc, so it needs the D the flux uses. It re-evaluated the base `phase_param.D(c, T)` instead, which ignores the "current sigmoid" diffusivity branches and the stress-induced diffusion factor. Read the particle's "effective diffusivity" variable, x-averaged for the x-averaged models, as the particle submodels publish it. With lithiation and delithiation diffusivities of D/3 and 3D on one phase the heat sources missed the energy balance by 33% of the heat. The energy-balance test now gives the composite positive's hysteretic phase a current-sigmoid diffusivity (D/2, 2D): the old code fails it at -18%, the new code passes at -0.2%. A uniform particle profile publishes no diffusivity and has no gradient, so its heat of mixing is now zero outright. That also makes a DFN with "particle": "uniform profile" and heat of mixing build; it raised a shape error before. Default options give bit-identical voltage, temperature and heat of mixing for SPM, SPMe and DFN on Chen2020 and Marquis2019. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Apply the review suggestion: build the phase-name prefixes from options.phases, with "" for a one-phase electrode, and sum over them, so the one-phase case needs no branch of its own. One-phase results are bit-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@BradyPlanden thanks for the review! Since the review I rebased onto main (after #5770); the heat of mixing now uses the particle's effective diffusivity, which it needs to conserve energy under current-sigmoid or stress-induced diffusion. There are some addition tests including an energy-balance check and composite-vs-one-phase comparisons under every thermal option. I think it's ready for another review when you get the chance. |
|
| Project | PyBaMM |
| Branch | heat-of-mixing-composite-electrodes |
| Testbed | bare-metal |
🐰 View full continuous benchmarking report in Bencher
⚠️ WARNING: Truncated view!The full continuous benchmarking report exceeds the maximum length allowed on this platform.
Merging main put them under the released v26.9.0.0 section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rtimms
left a comment
There was a problem hiding this comment.
Thanks @mleot, this looks good. Summing per phase through options.phases and param.domain_params is clean, and switching to the particle's effective diffusivity is the right call. The energy-balance test is a great addition: it fails if only the primary phase is summed, so it really tests the fix.
I've pushed two commits to get this ready for the release:
8057d1a84mergesmain.49e02543fmoves both CHANGELOG bullets to# [Unreleased]. Since v26.9.0.0 was cut, git silently put them under the released section, with no conflict to flag it.
Checks I ran (model build and discretisation, main vs this branch):
- SPM, SPMe and DFN with lumped and x-full thermal, and stress-induced diffusion (Ai2020): unchanged.
- Composite
"2"and("2", "1"): now build (ModelErrorbefore). - DFN with
"particle": "uniform profile": now builds (ValueErrorbefore). - MPM and size distributions: still raise the clear
NotImplementedError.
I've filed the follow-ups mentioned in the description, plus one I found:
- #5876: heat of mixing with a quadratic or quartic particle profile gives a
ShapeError. This is already broken onmain, so it doesn't block this PR. - #5877:
Chen2020_compositegives the electrode density per phase, but the thermal model reads one value per electrode. - #5878:
"lumped"thermal doesn't discretise with 1+1D current collectors.
…lynomial # Conflicts: # CHANGELOG.md # packages/pybamm/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py
|
@mleot I've merged |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for your review, support, and fixes for this @rtimms ! Let me know if I need to attend to anything else before it's good to go. |
Description
Fixes #5792
"heat of mixing": "true"could not be used with a composite (multi-phase) electrode.BaseThermal._heat_of_mixingread the single-phase variable names and took itsmaterial properties from
param.n.prim/param.p.primonly, so any model built with"particle phases": "2"failed at build time:The same failure occurred for SPM, SPMe and DFN, and for a composite positive as well as
a composite negative.
Changes
_phase_heat_of_mixingevaluates Richardson's term for one phase;_electrode_heat_of_mixingsums it over the electrode's phases, looked up throughparam.domain_paramsandoptions.phases.so it now reads the particle's
"effective diffusivity"variable instead ofre-evaluating the base
phase_param.D. They differ under"diffusivity": "current sigmoid"and"stress-induced diffusion": "true"; with lithiation and delithiationdiffusivities of D/3 and 3D the heat sources were off the energy balance by 33% of the
heat.
is zero outright. A DFN with
"particle": "uniform profile"and heat of mixing nowbuilds; it raised a shape error before.
reaction and hysteresis heats sum to
a j (phi_s - phi_e - U_eq), so the heat ofmixing must use the same
U_eqfor energy to balance; the hysteresis-weighted OCPthat sets the voltage would not.
Default results are unchanged. SPM, SPMe and DFN with
"thermal": "lumped"and"heat of mixing": "true"give bit-identical voltage, temperature and heat of mixing onChen2020andMarquis2019(1800 s, 1C).Tests
test_thermal_models.py, new): over a 1C SPM discharge, the fallin stored chemical energy
-F * sum int U_eq dcmust equal electrical work plus heatto 0.5% of the heat, for a one-phase cell and for a composite positive whose second
phase has current-sigmoid hysteresis (branches tilted away from its OCP) and
current-sigmoid diffusivity. The SPM keeps the electrolyte uniform and an isothermal
model with zero entropic change keeps temperature out of the stored energy. Residuals
are about -0.1% while the heat of mixing is ~45% of the heat. Summing only the primary
phase fails (+28%), as do the base diffusivity (-18%) and the hysteresis-weighted OCP
(-2%).
test_compare_outputs_two_phase.py, extending fix: sum the electrochemical heat sources over every particle phase #5770'stest): a graphite electrode split into two identical halves must reproduce the
one-phase heat sources, including the heat of mixing, for SPM, SPMe and DFN under
every
"thermal"option (isothermal heat source, lumped, x-full, x-lumped with 1+1Dcurrent collectors). It uses non-zero entropic change (Ai2020 for Chen2020;
Marquis2019's own for the pouch cases), pins
"surface form": "algebraic"on bothsides (composite models default to it, one-phase models to
"false") and solves atrtol 1e-6 so the ~300 K temperature state does not drift by more than the differences
tested.
Chen2020_compositewith a lumped model heats the cellthrough a positive heat of mixing.
"particle phases""2",("2", "1"),("1", "2").Not addressed here
OCPs: a cubic
Interpolantcrashes in CasADi at solve time and a linear one at buildtime (issues to follow). The tests use analytic OCPs;
Chen2020_composite's graphiteOCP is swapped for the analytic Chen2020 fit where needed.
Chen2020_compositehas no"Negative electrode density [kg.m-3]", so no thermalmodel runs with it as shipped; the tests supply one (issue to follow).
"thermal": "lumped"with 1+1D current collectors does not discretise, with orwithout the heat of mixing (issue to follow), so the 1+1D case uses
"x-lumped".Type of change
Bug fix: two entries under
## Bug fixesinCHANGELOG.md(phase summation; effectivediffusivity and uniform profile).
Important checks:
nox -s pre-commit(every hook passes on the branch diff,including comment-slop and repo-review)
nox -s tests. Locally the lithium-ion model, thermal andtwo-phase tests pass (353 integration tests in
test_lithium_ion/andtest_submodels/test_thermal/). The only local failures need optional dependenciesmy environment lacks (
skfem,matplotlib,pybtex,bpx) and fail the same wayon
main; CI runs them.nox -s doctests(not run locally; no docs changed,and the readthedocs check passes)
🤖 Generated with Claude Code