The Plugin System#

A Plugin pairs a TableParser (reads tabular data) with a MetadataParser (reads file metadata). The PLUGINS dict maps plugin IDs to Plugin instances for all built-in formats.

Auto-detection runs in three stages:

  1. detect_from_ext_or_magic_bytes – filters by file extension, falling back to magic bytes

  2. detect_from_metadata – filters by magic token matching

  3. detect_from_columns – picks the highest-scoring normalizer match

The top-level detect() orchestrates all three stages and returns early when unambiguous.

import re
from pathlib import Path

from bdf.plugins import (
    Plugin,
    PLUGINS,
    detect,
    detect_from_ext_or_magic_bytes,
    detect_from_metadata,
    detect_from_columns,
)
from bdf.table_parsers import DelimTxtParser
from bdf.table_normalizers import TableNormalizer, Syn
from bdf.metadata_parsers import TxtPreambleParser, MetadataSchema
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"
)
# Each key is a plugin ID; the value is the Plugin instance
list(PLUGINS.keys())
['arbin_csv',
 'arbin_res',
 'basytec_txt',
 'biologic_mpt',
 'biologic_mpr',
 'digatron_csv',
 'landt_csv',
 'landt_txt',
 'maccor_csv',
 'neware_csv',
 'neware_xlsx',
 'arbin_xlsx',
 'novonix_csv',
 'neware_nda',
 'bdf_csv',
 'bdf_parquet',
 'bdf_json',
 'bdf_ndjson',
 'bdf_ipc',
 'bdf_xlsx']

Constructing a custom Plugin#

A Plugin takes a table_parser and a metadata_parser. The structure mirrors the built-in BIOLOGIC_MPT plugin: a DelimTxtParser for the tabular data and a TxtPreambleParser for the header metadata.

custom_plugin = Plugin(
    table_parser=DelimTxtParser(
        normalizer=TableNormalizer(
            test_time_second=(Syn(hdr="time/{unit}"),),
            voltage_volt=(Syn(hdr="Ecell/{unit}"),),
            current_ampere=(Syn(hdr="I/{unit}"),),
        )
    ),
    metadata_parser=TxtPreambleParser(
        magic=("BT-Lab ASCII FILE",),
        regex_patterns=MetadataSchema(
            start_time=re.compile(r"Acquisition started on\s*:\s*(.+)"),
        ),
    ),
)
custom_plugin
Plugin(table_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=None, 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'), metadata_parser=TxtPreambleParser(kind='txt_preamble', magic=('BT-Lab ASCII FILE',), encoding='utf-8', regex_patterns=MetadataSchema[Pattern[str]](start_time=re.compile('Acquisition started on\\s*:\\s*(.+)'))))

Stage 1 — detect_from_ext_or_magic_bytes()#

Extension filtering is cheap but imprecise: .txt is used by multiple formats. detect_from_ext_or_magic_bytes() returns all plugins that accept the file’s extension, falling back to magic-byte sniffing when the extension is missing or unmatched.

biologic_file = resolve_source(BIOLOGIC_URL)

ext_candidates = detect_from_ext_or_magic_bytes(biologic_file)
print("Candidates from extension (.txt):", list(ext_candidates.keys()))
Candidates from extension (.txt): ['biologic_mpt']

Stage 2 — detect_from_metadata()#

Magic token matching reads only the file’s head bytes and is much more precise. detect_from_metadata() filters the candidates to those whose metadata_parser matches.

meta_candidates = detect_from_metadata(biologic_file, ext_candidates)
print("Candidates after metadata filter:", list(meta_candidates.keys()))
Candidates after metadata filter: ['biologic_mpt']

Stage 3 — detect_from_columns()#

detect_from_columns() sniffs the column headings and returns the plugin with the highest normalizer score. It returns a (plugin_id, Plugin) tuple.

plugin_id, plugin = detect_from_columns(biologic_file, meta_candidates)
print("Resolved plugin:", plugin_id)
Resolved plugin: biologic_mpt

Top-level detect() shortcut#

detect() orchestrates all three stages and returns early when the candidates are already unambiguous.

plugin_id, plugin = detect(biologic_file)
print("Detected:", plugin_id)
Detected: biologic_mpt
# Read the normalised data using the detected plugin
plugin.table_parser.read(biologic_file).collect()
shape: (157_985, 12)
Test Time / sVoltage / VCurrent / ACycle Count / 1Step IDStep Time / sNet Capacity / AhCharging Energy / WhDischarging Energy / WhNet Energy / WhPower / WInternal Resistance / ohm
f64f64f64i64i64f64f64f64f64f64f64f64
0.01.71423040.0000.00.00.00.00.00.00.0
10.01.71415160.0000.00.00.00.00.00.00.0
20.0000011.71415160.0000.00.00.00.00.00.00.0
30.0000011.71423040.0000.00.00.00.00.00.00.0
40.0000021.71419110.0000.00.00.00.00.00.00.0
1.5676e61.68863460.0060.00.0187734.02451233.117670.9068420.00.0
1.5676e61.6886740.0060.00.0187734.02451233.117670.9068420.00.0
1.5677e61.68863460.0060.00.0187734.02451233.117670.9068420.00.0
1.5677e61.68859530.0060.00.0187734.02451233.117670.9068420.00.0
1.5677e61.68871330.0070.00.0187734.02451233.117670.9068420.00.0