TableParser, DelimTxtParser, and NdaParser#

A TableParser reads tabular data from a file and normalises the column names to BDF standards. Two concrete parsers are shown here:

  • DelimTxtParser – reads delimiter-separated text files (CSV, TSV, etc.)

  • NdaParser – reads Neware’s binary .nda / .ndax format

Both expose the same interface: matches_ext(), read_column_headings(), normalizer_score(), and read().

from pathlib import Path
from bdf.table_parsers import DelimTxtParser, NdaParser
from bdf.table_normalizers import TableNormalizer, Syn, DateTimeSyn
from bdf.file_utils import resolve_source

BIOLOGIC_URL = (
    "https://zenodo.org/api/records/18986774/files/"
    "SINTEF__NaCR32140-MP10-04__2025-08-25__GITT_0p05C_25degC__BioLogic.mpt/content"
)
NEWARE_NDA_URL = (
    "https://zenodo.org/api/records/18986774/files/"
    "SINTEF__G20M7-202512-Gru6mV__20251228__C30__25degC__Neware.nda/content"
)

DelimTxtParser — delimited text files#

Construct a DelimTxtParser with a TableNormalizer that maps raw column names to BDF fields. The Biologic BT-Lab file has tab-separated columns including time/s, Ecell/V, I/mA, and cycle number. Unit-template syns like Syn(hdr='Ecell/{unit}') match any column whose name fits the pattern with a recognised unit. Matching is case-sensitive, so the syn root must match the source header’s casing exactly outside the {unit} slot.

biologic_file = resolve_source(BIOLOGIC_URL)

biologic_parser = DelimTxtParser(
    normalizer=TableNormalizer(
        test_time_second=(Syn(hdr="time/{unit}"),),
        voltage_volt=(Syn(hdr="Ecell/{unit}"),),
        current_ampere=(Syn(hdr="I/{unit}"),),
        cycle_count=(Syn(hdr="cycle number"),),
    )
)
biologic_parser
DelimTxtParser(normalizer=TableNormalizer(test_time_second=(Syn(hdr='time/{unit}', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), voltage_volt=(Syn(hdr='Ecell/{unit}', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), current_ampere=(Syn(hdr='I/{unit}', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), unix_time_second=None, cycle_count=(Syn(hdr='cycle number', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), step_count=None, step_id=None, step_type=None, ambient_temperature_celsius=None, step_record_index=None, record_index=None, step_time_second=None, charging_capacity_ah=None, step_charging_capacity_ah=None, cycle_charging_capacity_ah=None, schedule_charging_capacity_ah=None, discharging_capacity_ah=None, step_discharging_capacity_ah=None, cycle_discharging_capacity_ah=None, schedule_discharging_capacity_ah=None, net_capacity_ah=None, step_net_capacity_ah=None, cycle_net_capacity_ah=None, cumulative_capacity_ah=None, step_cumulative_capacity_ah=None, cycle_cumulative_capacity_ah=None, charging_energy_wh=None, step_charging_energy_wh=None, cycle_charging_energy_wh=None, schedule_charging_energy_wh=None, discharging_energy_wh=None, step_discharging_energy_wh=None, cycle_discharging_energy_wh=None, schedule_discharging_energy_wh=None, net_energy_wh=None, step_net_energy_wh=None, cycle_net_energy_wh=None, cumulative_energy_wh=None, step_cumulative_energy_wh=None, cycle_cumulative_energy_wh=None, power_watt=None, internal_resistance_ohm=None, dc_internal_resistance_ohm=None, ac_internal_resistance_ohm=None, real_impedance_ohm=None, imaginary_impedance_ohm=None, absolute_impedance_ohm=None, phase_degree=None, frequency_hertz=None, ambient_pressure_pa=None, applied_pressure_pa=None, surface_pressure_pa=None, temperature_t1_celsius=None, temperature_t2_celsius=None, temperature_t3_celsius=None, temperature_t4_celsius=None, temperature_t5_celsius=None, surface_temperature_celsius=None), unique_exts=frozenset(), kind='txt', separator=None, skip_rows=None, has_header=True, decimal_comma=None, truncate_ragged_lines=False, encoding='utf-8')

matches_ext() checks the file extension against the parser’s accepted types

print("matches .txt: ", biologic_parser.matches_ext(".txt"))
print("matches .xlsx:", biologic_parser.matches_ext(".xlsx"))
matches .txt:  True
matches .xlsx: False

read_column_headings() sniffs only the header row — no data rows loaded

biologic_parser.read_column_headings(biologic_file)
['mode',
 'ox/red',
 'error',
 'control changes',
 'Ns changes',
 'counter inc.',
 'Ns',
 'I Range',
 'time/s',
 'control/mA',
 'Ecell/V',
 'I/mA',
 'dq/mA.h',
 '(Q-Qo)/mA.h',
 'Q charge/discharge/mA.h',
 'half cycle',
 'Temperature/�C',
 'Energy/W.h',
 'Energy charge/W.h',
 'Energy discharge/W.h',
 'Capacitance charge/�F',
 'Capacitance discharge/�F',
 'step time/s',
 'x',
 'Q discharge/mA.h',
 'Q charge/mA.h',
 'Capacity/mA.h',
 'Efficiency/%',
 'cycle number',
 'P/W',
 'R/Ohm',
 '']

normalizer_score() counts how many columns the normalizer can resolve

biologic_parser.normalizer_score(biologic_file)
4

read() returns a normalised polars LazyFrame with BDF-standard column names

biologic_parser.read(biologic_file).collect()
shape: (157_985, 4)
Test Time / sVoltage / VCurrent / ACycle Count / 1
f64f64f64i64
0.01.71423040.00
10.01.71415160.00
20.0000011.71415160.00
30.0000011.71423040.00
40.0000021.71419110.00
1.5676e61.68863460.00
1.5676e61.6886740.00
1.5677e61.68863460.00
1.5677e61.68859530.00
1.5677e61.68871330.00

Graceful degradation — empty normalizer preserves source column names#

When a TableNormalizer() with no synonyms is supplied, read() still works but keeps the original column names from the file. This is useful for exploring an unfamiliar file.

raw_parser = DelimTxtParser(normalizer=TableNormalizer())
raw_parser.read(biologic_file, validate=False).collect()
/home/runner/work/battery-data-format/battery-data-format/src/bdf/_df_compat.py:90: UserWarning: Missing required BDF columns: ['Current / A', 'Test Time / s', 'Voltage / V']; unrecognized columns present: ['', '(Q-Qo)/mA.h', 'Capacitance charge/�F', 'Capacitance discharge/�F', 'Capacity/mA.h', 'Ecell/V', 'Efficiency/%', 'Energy charge/W.h', 'Energy discharge/W.h', 'Energy/W.h', 'I Range', 'I/mA', 'Ns', 'Ns changes', 'P/W', 'Q charge/discharge/mA.h', 'Q charge/mA.h', 'Q discharge/mA.h', 'R/Ohm', 'Temperature/�C', 'control changes', 'control/mA', 'counter inc.', 'cycle number', 'dq/mA.h', 'error', 'half cycle', 'mode', 'ox/red', 'step time/s', 'time/s', 'x']
  result = fn(self, _to_polars_lazy(df), *args, **kwargs)
shape: (157_985, 32)
modeox/rederrorcontrol changesNs changescounter inc.NsI Rangetime/scontrol/mAEcell/VI/mAdq/mA.h(Q-Qo)/mA.hQ charge/discharge/mA.hhalf cycleTemperature/�CEnergy/W.hEnergy charge/W.hEnergy discharge/W.hCapacitance charge/�FCapacitance discharge/�Fstep time/sxQ discharge/mA.hQ charge/mA.hCapacity/mA.hEfficiency/%cycle numberP/WR/Ohm
strstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstr
"3""0""0""1""0""0""0""115""0.000000000000000E+000""0.0000000E+000""1.7142304E+000""0.0000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0""2.3509834E+001""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""0""0""115""1.000000047497451E+001""0.0000000E+000""1.7141516E+000""0.0000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0""2.3162155E+001""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""0""0""115""2.000000094994903E+001""0.0000000E+000""1.7141516E+000""0.0000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0""2.3383406E+001""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""0""0""115""3.000000142492354E+001""0.0000000E+000""1.7142304E+000""0.0000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0""2.3328093E+001""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""0""0""115""4.000000189989805E+001""0.0000000E+000""1.7141911E+000""0.0000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0""2.3375504E+001""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.000000000000000E+000""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""1""6""115""1.567639381988852E+006""0.0000000E+000""1.6886346E+000""0.0000000E+000""0.000000000000000E+000""1.876953939076742E+001""-1.107314653570800E+004""1""2.3280682E+001""9.068416111857436E-001""3.402451190351142E+001""3.311767029232572E+001""1.790446324298840E+010""1.636656669549456E+010""0.000000000000000E+000""-7.003167505836396E-001""1.107314653570800E+004""0.000000000000000E+000""1.107314653570800E+004""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""1""6""115""1.567649381990044E+006""0.0000000E+000""1.6886740E+000""0.0000000E+000""0.000000000000000E+000""1.876953939076742E+001""-1.107314653570800E+004""1""2.3241173E+001""9.068416111857436E-001""3.402451190351142E+001""3.311767029232572E+001""1.790446324298840E+010""1.636656669549456E+010""0.000000000000000E+000""-7.003167505836396E-001""1.107314653570800E+004""0.000000000000000E+000""1.107314653570800E+004""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""1""6""115""1.567659381991236E+006""0.0000000E+000""1.6886346E+000""0.0000000E+000""0.000000000000000E+000""1.876953939076742E+001""-1.107314653570800E+004""1""2.3280682E+001""9.068416111857436E-001""3.402451190351142E+001""3.311767029232572E+001""1.790446324298840E+010""1.636656669549456E+010""0.000000000000000E+000""-7.003167505836396E-001""1.107314653570800E+004""0.000000000000000E+000""1.107314653570800E+004""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""0""1""6""115""1.567659705991272E+006""0.0000000E+000""1.6885953E+000""0.0000000E+000""0.000000000000000E+000""1.876953939076742E+001""-1.107314653570800E+004""1""2.3193762E+001""9.068416111857436E-001""3.402451190351142E+001""3.311767029232572E+001""1.790446324298840E+010""1.636656669549456E+010""0.000000000000000E+000""-7.003167505836396E-001""1.107314653570800E+004""0.000000000000000E+000""1.107314653570800E+004""0.0000000E+000""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null
"3""0""0""1""1""1""7""115""1.567659707991272E+006""0.0000000E+000""1.6887133E+000""0.0000000E+000""0.000000000000000E+000""1.876953939076742E+001""-1.107314653570800E+004""1""2.3193762E+001""9.068416111857436E-001""3.402451190351142E+001""3.311767029232572E+001""1.790446324298840E+010""1.636656669549456E+010""0.000000000000000E+000""-7.003167505836396E-001""1.107314653570800E+004""0.000000000000000E+000""1.107314653570800E+004""9.9830780E+001""0.000000000000000E+000""0.0000000E+000""0.0000000E+000"null

NdaParser — Neware binary files#

NdaParser reads Neware’s binary .nda / .ndax format via fastnda. Its extension handling targets the binary format, the opposite of DelimTxtParser: .nda returns True, .csv returns False.

neware_file = resolve_source(NEWARE_NDA_URL)

neware_parser = NdaParser(
    normalizer=TableNormalizer(
        test_time_second=(Syn(hdr="total_time_{unit}"),),
        voltage_volt=(Syn(hdr="voltage_{unit}"),),
        current_ampere=(Syn(hdr="current_{unit}"),),
        cycle_count=(Syn(hdr="cycle_count"),),
    ),
)
neware_parser
NdaParser(normalizer=TableNormalizer(test_time_second=(Syn(hdr='total_time_{unit}', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), voltage_volt=(Syn(hdr='voltage_{unit}', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), current_ampere=(Syn(hdr='current_{unit}', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), unix_time_second=None, cycle_count=(Syn(hdr='cycle_count', assumed=False, source_unit=None, legacy=False, reverse_sign=False),), step_count=None, step_id=None, step_type=None, ambient_temperature_celsius=None, step_record_index=None, record_index=None, step_time_second=None, charging_capacity_ah=None, step_charging_capacity_ah=None, cycle_charging_capacity_ah=None, schedule_charging_capacity_ah=None, discharging_capacity_ah=None, step_discharging_capacity_ah=None, cycle_discharging_capacity_ah=None, schedule_discharging_capacity_ah=None, net_capacity_ah=None, step_net_capacity_ah=None, cycle_net_capacity_ah=None, cumulative_capacity_ah=None, step_cumulative_capacity_ah=None, cycle_cumulative_capacity_ah=None, charging_energy_wh=None, step_charging_energy_wh=None, cycle_charging_energy_wh=None, schedule_charging_energy_wh=None, discharging_energy_wh=None, step_discharging_energy_wh=None, cycle_discharging_energy_wh=None, schedule_discharging_energy_wh=None, net_energy_wh=None, step_net_energy_wh=None, cycle_net_energy_wh=None, cumulative_energy_wh=None, step_cumulative_energy_wh=None, cycle_cumulative_energy_wh=None, power_watt=None, internal_resistance_ohm=None, dc_internal_resistance_ohm=None, ac_internal_resistance_ohm=None, real_impedance_ohm=None, imaginary_impedance_ohm=None, absolute_impedance_ohm=None, phase_degree=None, frequency_hertz=None, ambient_pressure_pa=None, applied_pressure_pa=None, surface_pressure_pa=None, temperature_t1_celsius=None, temperature_t2_celsius=None, temperature_t3_celsius=None, temperature_t4_celsius=None, temperature_t5_celsius=None, surface_temperature_celsius=None), unique_exts=frozenset(), kind='nda')
print("matches .nda: ", neware_parser.matches_ext(".nda"))
print("matches .csv: ", neware_parser.matches_ext(".csv"))
matches .nda:  True
matches .csv:  False
neware_parser.read_column_headings(neware_file)
['index',
 'voltage_V',
 'current_mA',
 'unix_time_s',
 'step_time_s',
 'total_time_s',
 'cycle_count',
 'step_count',
 'step_index',
 'step_type',
 'capacity_mAh',
 'energy_mWh']
neware_parser.read(neware_file)
naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)

SELECT [col("total_time_s").cast(Float64).alias("Test Time / s"), col("voltage_V").cast(Float64).alias("Voltage / V"), [(col("current_mA").cast(Float64)) * (0.001)].alias("Current / A"), col("cycle_count").cast(Float64).cast(Int64).alias("Cycle Count / 1")]

DF ["index", "voltage_V", "current_mA", "unix_time_s", ...]; PROJECT */12 COLUMNS