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/.ndaxformat
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()
| Test Time / s | Voltage / V | Current / A | Cycle Count / 1 |
|---|---|---|---|
| f64 | f64 | f64 | i64 |
| 0.0 | 1.7142304 | 0.0 | 0 |
| 10.0 | 1.7141516 | 0.0 | 0 |
| 20.000001 | 1.7141516 | 0.0 | 0 |
| 30.000001 | 1.7142304 | 0.0 | 0 |
| 40.000002 | 1.7141911 | 0.0 | 0 |
| … | … | … | … |
| 1.5676e6 | 1.6886346 | 0.0 | 0 |
| 1.5676e6 | 1.688674 | 0.0 | 0 |
| 1.5677e6 | 1.6886346 | 0.0 | 0 |
| 1.5677e6 | 1.6885953 | 0.0 | 0 |
| 1.5677e6 | 1.6887133 | 0.0 | 0 |
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)
| 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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str |
| "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)