Development Notes¶
Engine-implementation and contributor notes live here — the design rules a new engine or a change to an existing one must respect. User-facing behavior is documented in the reference guides (validation.md, sparql.md, …).
Engine model¶
The one rule that decides where automatic engine selection is allowed:
Auto-selection of the fastest available engine applies only to operations whose result is flavor-independent (parsed files, exported bytes/XML, query results, violation reports). Frame-in → frame-out operations always run in the input object’s engine — never auto-hop pandas → polars (measured: the round-trip conversion ≈16 ms/1M rows exceeds typical op savings ≈8 ms).
All engine dispatch goes through triplets._registry.EngineRegistry — do
not hand-roll module maps or local try-import blocks. Registries are
constructed at import time and probe availability with find_spec
(microseconds, imports nothing); the chosen module imports lazily on first
use. An engine module must raise ImportError on import when its backend is
missing — that is what makes "auto" fall through.
Kind names carry a role prefix when the subsystem is format-specific
(parser_/exporter_), so “cimxml” is never ambiguous between the parser
and the exporter:
Kind |
Policy |
Auto order |
Availability probes ( |
|---|---|---|---|
parser_cimxml |
auto |
cython_pugixml_arrow → python_lxml_arrow → python_lxml_pandas |
compiled ext, pyarrow |
sparql |
auto |
qlever → oxigraph → rdflib |
|
validation |
auto |
polars → pandas → pyshacl (duckdb explicit-only) |
polars, duckdb, pyshacl+rdflib |
exporter_nquads |
auto |
polars → pandas |
polars |
exporter_cimxml |
auto |
cython_pugixml → python_lxml |
compiled ext, pyarrow |
exporter_csv |
input |
— (engine = input flavor) |
polars |
tools |
input |
— (engine = input flavor) |
polars, duckdb |
policy="input" marks the frame-bound subsystems: csv exists as two engines
because each is fastest for its own input flavor, and tools operates on the
caller’s frame — neither may be steered by a global override, and their
engines() row reports engine: None, source: "input".
cgmes_tools is deliberately NOT a registry: its data functions run natively
for pandas and polars input, while duckdb/arrow input crosses the pandas
boundary (to_pandas(plain=True) → pandas engine → match_flavor back) —
the engines mutate VALUE in place, which needs a plain materialized frame.
That is a design decision, not an unfinished migration.
User-facing controls: triplets.engines() reports what "auto" resolved to
per subsystem (plus available alternatives); triplets.set_engine(parser_cimxml=..., sparql=..., ...) overrides it globally (loads eagerly, fails fast).
Precedence: per-call engine= > set_engine() > auto probe order.
set_engine is process-global startup configuration, not a per-thread
control — concurrent code passes engine= per call. Per-call capability
constraints still apply regardless of overrides (cimxml datatypes=True
requires python_lxml; validation keeps duckdb out of auto because it is the
explicit larger-than-memory choice).
Flavor conversion¶
Triplet data arrives as pandas, polars, pyarrow, or a DuckDB connection. Convert
at subsystem boundaries with triplets._engine_detect — do not add local
if polars / if duckdb materialization blocks:
Helper |
Role |
|---|---|
|
|
|
any → pandas ( |
|
any → pyarrow (DuckDB uses native arrow, no pandas) |
|
any → polars (Arrow path for duckdb/pyarrow) |
|
pandas/polars unchanged; arrow/duckdb → pandas |
|
pandas result → template’s flavor (cgmes dispatch) |
|
pandas frame → “pandas”/”polars”/”arrow” (explicit return_type params) |
DuckDB table/schema defaults stay in tools.duckdb_engine; converters pass
optional table / schema / table_name through _resolve_table.
Polars Engine Guidance¶
The lazy validation engine’s design rules. Speed always wins over memory:
Build one LazyFrame plan per IR constraint against a shared
.lazy()base; execute everything with a singlepolars.collect_all(plans)(parallel execution + common-subplan elimination). Pre-materialize shared indices once (per-Type row index, the set of all IDs) and reuse them across plans.Use expressions only (
polars.col),Categorical/Enumdtype for KEY and Type,.cast(strict=False)+ null-check for datatype casts,str.contains(literal=True)when no regex is needed, join-based membership for largesh:inlists (is_inonly for small ones).Avoid
map_elements/map_rows(Python UDFs serialize execution), per-constraint eager.collect(),.to_pandas()round-trips mid-pipeline,iter_rows, object dtype, eagerpivoton large frames.No streaming collect — that trades speed for memory, which is the duckdb engine’s job. Keep the base frame and indices materialized; rechunk once after load.