"""Test manuscripts for the Journal Formatter (realistic content, every feature the formatter must handle).
python devtools/fmt_corpus.py [out_dir] -> numeric.docx, authoryear.docx, messy.docx
Each manuscript records what it contains (FACTS), so devtools/fmt_audit.py can check nothing was lost."""
import io
import os
import sys
from pathlib import Path
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import parse_xml
from docx.shared import Inches, Pt
M_NS = 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"'
W_NS = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
TITLE = "Thermal Management of Lithium-Ion Battery Modules Using Phase Change Materials and Liquid Cooling"
AUTHORS = [("Ravi Kumar", "1"), ("Anita Sharma", "2"), ("John Miller", "1,*")]
AFFILS = ["1 Department of Mechanical Engineering, National Institute of Technology, Warangal 506004, India",
"2 School of Energy Systems, University of Leeds, Leeds LS2 9JT, United Kingdom"]
CORR = "* Corresponding author: john.miller@example.ac.uk"
ABSTRACT = ("Effective thermal management is essential for the safety and service life of lithium-ion battery "
"modules in electric vehicles. This study integrates a paraffin-based phase change material (PCM) with a "
"serpentine liquid cooling plate and tests a 48 V module under 1C, 2C and 3C discharge. The hybrid system "
"kept the maximum cell temperature below 41.2 °C at 3C, which is 7.4 °C lower than liquid cooling alone, "
"and limited the temperature difference across the module to 2.1 °C. A coupled electro-thermal model "
"reproduced the measurements within 0.8 °C. The results show that a thin PCM layer delays the onset of "
"thermal runaway and reduces pump power by 23%.")
KEYWORDS = ["lithium-ion battery", "phase change material", "liquid cooling", "thermal management", "electric vehicle"]
REFS = [ # (key for author-year, numeric-style text, author-year text)
("Kumar et al., 2021",
"R. Kumar, S. Patel, and L. Chen, “Hybrid cooling of battery packs,” Applied Thermal Engineering, vol. 185, p. 116389, 2021, doi: 10.1016/j.applthermaleng.2020.116389.",
"Kumar, R., Patel, S., & Chen, L. (2021). Hybrid cooling of battery packs. Applied Thermal Engineering, 185, 116389. https://doi.org/10.1016/j.applthermaleng.2020.116389"),
("Wang and Li, 2019",
"Y. Wang and H. Li, “Phase change materials for thermal storage: A review,” Energy Conversion and Management, vol. 198, pp. 111–129, 2019.",
"Wang, Y., & Li, H. (2019). Phase change materials for thermal storage: A review. Energy Conversion and Management, 198, 111–129."),
("Bergman et al., 2011",
"T. L. Bergman, A. S. Lavine, F. P. Incropera, and D. P. DeWitt, Fundamentals of Heat and Mass Transfer, 7th ed. Hoboken, NJ, USA: Wiley, 2011.",
"Bergman, T. L., Lavine, A. S., Incropera, F. P., & DeWitt, D. P. (2011). Fundamentals of heat and mass transfer (7th ed.). Wiley."),
("Zhang et al., 2020",
"X. Zhang, M. Ahmed, and P. Rossi, “Liquid cold plates for prismatic cells,” in Proc. IEEE Transportation Electrification Conf. (ITEC), Chicago, IL, USA, 2020, pp. 45–50.",
"Zhang, X., Ahmed, M., & Rossi, P. (2020). Liquid cold plates for prismatic cells. In Proceedings of the IEEE Transportation Electrification Conference (pp. 45–50). IEEE."),
("Feng et al., 2018",
"X. Feng, M. Ouyang, X. Liu, L. Lu, Y. Xia, and X. He, “Thermal runaway mechanism of lithium ion battery for electric vehicles: A review,” Energy Storage Materials, vol. 10, pp. 246–267, 2018.",
"Feng, X., Ouyang, M., Liu, X., Lu, L., Xia, Y., & He, X. (2018). Thermal runaway mechanism of lithium ion battery for electric vehicles: A review. Energy Storage Materials, 10, 246–267."),
("Rao and Wang, 2011",
"Z. Rao and S. Wang, “A review of power battery thermal energy management,” Renewable and Sustainable Energy Reviews, vol. 15, no. 9, pp. 4554–4571, 2011.",
"Rao, Z., & Wang, S. (2011). A review of power battery thermal energy management. Renewable and Sustainable Energy Reviews, 15(9), 4554–4571."),
("ISO, 2018",
"International Organization for Standardization, ISO 12405-4:2018 Electrically propelled road vehicles — Test specification for lithium-ion traction battery packs and systems, Geneva, 2018.",
"International Organization for Standardization. (2018). ISO 12405-4:2018 Electrically propelled road vehicles — Test specification for lithium-ion traction battery packs and systems."),
("Battery University, 2023",
"Battery University, “BU-410: Charging at high and low temperatures,” 2023. [Online]. Available: https://batteryuniversity.com/article/bu-410",
"Battery University. (2023). BU-410: Charging at high and low temperatures. https://batteryuniversity.com/article/bu-410"),
]
# body: (kind, text); {c:i,j} = citation of references i, j (1-based); {f:n} = figure n; {t:n} = table n
BODY = [
("h1", "Introduction"),
("p", "Electric vehicles depend on lithium-ion batteries whose performance and safety are strongly temperature "
"dependent {c:1,2}. Above 45 °C, side reactions accelerate capacity fade, and at higher temperatures they "
"can trigger thermal runaway {c:5}. Air cooling is simple but cannot remove the heat generated at high "
"discharge rates {c:6}, which has motivated liquid cooling and phase change materials {c:1,2,4}."),
("p", "This paper makes three contributions:"),
("ul", "a hybrid PCM–liquid design that needs no additional pump capacity;"),
("ul", "experiments on a 48 V module at 1C, 2C and 3C following the ISO procedure {c:7};"),
("ul", "a validated electro-thermal model for design studies."),
("h1", "Materials and Methods"),
("h2", "Battery module and test bench"),
("p", "The module consists of twelve prismatic cells (50 Ah) connected in series. The test bench is shown in "
"{f:1}, and the cell properties are listed in {t:1}. Temperatures were recorded with 24 T-type "
"thermocouples at 1 Hz."),
("table", None),
("h2", "Electro-thermal model"),
("p", "The heat generated in each cell follows the simplified Bernardi equation {c:3}:"),
("eq", "1"),
("p", "where I is the current, U_oc the open-circuit voltage, U the terminal voltage and T the cell temperature. "
"The PCM is modelled with the enthalpy method, and the latent heat of 180 kJ/kg is released over a "
"melting range of 2 K{fn}."),
("ol", "Measure the open-circuit voltage at 10% state-of-charge steps."),
("ol", "Fit the entropic coefficient from the temperature sweep."),
("ol", "Validate the model against the 2C discharge."),
("h1", "Results"),
("p", "{f:1} compares the maximum cell temperature for the three cooling strategies. At 3C the hybrid system "
"stays below 41.2 °C, 7.4 °C lower than liquid cooling alone, in line with earlier reports {c:1,4}. "
"The temperature difference across the module never exceeded 2.1 °C (see {t:1})."),
("figure", None),
("h1", "Discussion"),
("p", "The PCM layer absorbs the heat peaks while the liquid loop removes the average load, so the pump can "
"run at a lower flow rate. This agrees with the review by {c:2} and the runaway analysis in {c:5}. A "
"limitation of this study is that ageing was not considered; manufacturer data suggest that charging "
"at low temperature remains critical {c:8}."),
("h1", "Conclusions"),
("p", "A hybrid PCM–liquid cooling system kept a 48 V module below 41.2 °C at 3C and reduced pump power by 23%. "
"Future work will address cycle ageing and the mass of the PCM layer."),
]
FOOTNOTE = "The melting range was measured by differential scanning calorimetry at 5 K/min."
TABLE = [["Property", "Value", "Unit"],
["Nominal capacity", "50", "Ah"],
["Nominal voltage", "3.7", "V"],
["Mass", "0.89", "kg"],
["Specific heat", "1100", "J/(kg·K)"]]
TABLE_CAPTION = "Properties of the prismatic cells used in the module."
FIG_CAPTION = "Maximum cell temperature at 3C for air, liquid and hybrid PCM–liquid cooling."
FACTS = {"refs": len(REFS), "figures": 1, "tables": 1, "equations": 1, "footnotes": 1,
"headings": sum(1 for k, _ in BODY if k in ("h1", "h2")), "list_items": sum(1 for k, _ in BODY if k in ("ul", "ol"))}
def _figure_png():
from PIL import Image, ImageDraw
img = Image.new("RGB", (900, 500), "white")
d = ImageDraw.Draw(img)
d.rectangle([60, 40, 860, 440], outline="black", width=2)
for x, h, c in ((180, 300, "#c0392b"), (420, 220, "#2980b9"), (660, 150, "#27ae60")):
d.rectangle([x, 440 - h, x + 120, 440], fill=c)
buf = io.BytesIO()
img.save(buf, "PNG")
return buf.getvalue()
def _omml_equation():
# Q = I (U_oc - U) - I T dU_oc/dT (display equation)
sub = lambda base, s: (f"{base}"
f"{s}")
frac = ("d" + sub("U", "oc") + ""
"dT")
return (f"Q=I(" + sub("U", "oc")
+ "−U)−IT" + frac + "")
def _add_footnote(doc, paragraph, text):
"""A real Word footnote (python-docx has no API for it): footnotes part + reference run."""
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.opc.packuri import PackURI
from docx.opc.part import Part
part = doc.part
xml = (f''
''
''
f' {text}'
'')
fpart = Part(PackURI("/word/footnotes.xml"),
"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", xml.encode(), part.package)
part.relate_to(fpart, RT.FOOTNOTES)
r = parse_xml(f'')
paragraph._p.append(r)
def build(kind="numeric"):
"""kind: numeric | authoryear | messy."""
doc = Document()
st = doc.styles["Normal"]
st.font.name, st.font.size = "Calibri", Pt(11)
messy = kind == "messy"
author_year = kind == "authoryear"
def heading(text, level, n):
if messy: # bold body text, typed numbers: no heading styles
p = doc.add_paragraph()
r = p.add_run(f"{n} {text}" if n else text)
r.bold = True
r.font.size = Pt(12 if level == 1 else 11)
else:
doc.add_heading(f"{n} {text}" if n else text, level)
t = doc.add_paragraph(style="Title")
t.add_run(TITLE)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
for i, (name, aff) in enumerate(AUTHORS):
p.add_run(name)
r = p.add_run(aff)
r.font.superscript = True
if i < len(AUTHORS) - 1:
p.add_run(", ")
for a in AFFILS:
q = doc.add_paragraph()
q.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = q.add_run(a[0])
r.font.superscript = True
q.add_run(a[1:])
doc.add_paragraph(CORR)
if messy:
p = doc.add_paragraph()
r = p.add_run("Abstract: ")
r.bold = True
p.add_run(ABSTRACT)
else:
doc.add_heading("Abstract", 1)
doc.add_paragraph(ABSTRACT)
p = doc.add_paragraph()
r = p.add_run("Keywords: ")
r.bold = True
p.add_run((", " if messy else "; ").join(KEYWORDS))
n1 = n2 = 0
eq_done = fn_done = False
fig_png = _figure_png()
for k, text in BODY:
if k == "h1":
n1 += 1
n2 = 0
heading(text, 1, f"{n1}.")
elif k == "h2":
n2 += 1
heading(text, 2, f"{n1}.{n2}")
elif k in ("p", "ul", "ol"):
style = {"ul": "List Bullet", "ol": "List Number"}.get(k)
par = doc.add_paragraph(style=style) if style else doc.add_paragraph()
import re
pos = 0
for m in re.finditer(r"\{(c|f|t):([\d,]+)\}|\{fn\}", text):
par.add_run(text[pos:m.start()])
pos = m.end()
if m.group(0) == "{fn}":
_add_footnote(doc, par, FOOTNOTE)
fn_done = True
continue
what, nums = m.group(1), [int(x) for x in m.group(2).split(",")]
if what == "c":
if author_year:
par.add_run("(" + "; ".join(REFS[i - 1][0] for i in nums) + ")")
else:
par.add_run("[" + ", ".join(str(i) for i in nums) + "]")
elif what == "f":
par.add_run(("Fig. " if messy else "Figure ") + str(nums[0]))
else:
par.add_run("Table " + str(nums[0]))
par.add_run(text[pos:])
elif k == "table":
doc.add_paragraph(("Table 1: " if messy else "Table 1. ") + TABLE_CAPTION)
tb = doc.add_table(rows=len(TABLE), cols=len(TABLE[0]))
tb.style = "Table Grid"
for i, row in enumerate(TABLE):
for j, v in enumerate(row):
tb.cell(i, j).text = v
if i == 0:
tb.cell(i, j).paragraphs[0].runs[0].bold = True
elif k == "figure":
doc.add_paragraph().add_run().add_picture(io.BytesIO(fig_png), width=Inches(5.5))
doc.add_paragraph(("Fig. 1: " if messy else "Figure 1. ") + FIG_CAPTION)
elif k == "eq":
p = doc.add_paragraph()
p._p.append(parse_xml(_omml_equation()))
p.add_run("\t(1)")
eq_done = True
heading("Acknowledgments", 1, "")
doc.add_paragraph("The authors thank the Centre for Energy Storage for access to the test bench.")
heading("References", 1, "")
refs = sorted(REFS, key=lambda r: r[2]) if author_year else REFS
for i, r in enumerate(refs, 1):
doc.add_paragraph(r[2] if author_year else (f"{i}. " if messy else f"[{i}] ") + r[1])
assert eq_done and fn_done
return doc
def main():
out = Path(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).resolve().parent / "logs" / "fmt_corpus")
out.mkdir(parents=True, exist_ok=True)
for kind in ("numeric", "authoryear", "messy"):
build(kind).save(str(out / f"{kind}.docx"))
print("written", out / f"{kind}.docx")
if __name__ == "__main__":
main()